wip
This commit is contained in:
@@ -137,7 +137,7 @@ Deno.serve(async (req) => {
|
|||||||
- Descripción del enfoque: ${payload.iaConfig.descripcionEnfoque}
|
- Descripción del enfoque: ${payload.iaConfig.descripcionEnfoque}
|
||||||
- Notas adicionales: ${payload.iaConfig.notasAdicionales ?? "Ninguna"}`;
|
- Notas adicionales: ${payload.iaConfig.notasAdicionales ?? "Ninguna"}`;
|
||||||
const aiStructuredPayload = {
|
const aiStructuredPayload = {
|
||||||
model: "gpt-5-nano",
|
model: "gpt-5-mini",
|
||||||
input: [
|
input: [
|
||||||
{ role: "system", content: systemPrompt },
|
{ role: "system", content: systemPrompt },
|
||||||
{ role: "user", content: userPrompt },
|
{ role: "user", content: userPrompt },
|
||||||
@@ -145,7 +145,7 @@ Deno.serve(async (req) => {
|
|||||||
text: {
|
text: {
|
||||||
format: {
|
format: {
|
||||||
type: "json_schema",
|
type: "json_schema",
|
||||||
name: `${estructuraPlan?.nombre}`,
|
name: "plan_de_estudios_standard",
|
||||||
schema: estructuraPlan?.definicion,
|
schema: estructuraPlan?.definicion,
|
||||||
strict: true,
|
strict: true,
|
||||||
},
|
},
|
||||||
@@ -277,6 +277,21 @@ Deno.serve(async (req) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Este helper recibe un esquema (ej. DatosBasicosSchema) y devuelve un validador
|
||||||
|
// que acepta un string JSON y lo valida contra ese esquema.
|
||||||
|
const jsonFromString = <T extends z.ZodTypeAny>(schema: T) =>
|
||||||
|
z.string().transform((str, ctx) => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(str);
|
||||||
|
} catch (e) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "El formato no es un JSON válido",
|
||||||
|
});
|
||||||
|
return z.NEVER; // Detiene la ejecución aquí si falla el parseo
|
||||||
|
}
|
||||||
|
}).pipe(schema); // Si el parseo es exitoso, pasa los datos al esquema real
|
||||||
|
|
||||||
// --- VALIDACIÓN ESTRICTA DE DATOS BÁSICOS ---
|
// --- VALIDACIÓN ESTRICTA DE DATOS BÁSICOS ---
|
||||||
const DatosBasicosSchema: z.ZodType<AIGeneratePlanInput["datosBasicos"]> = z
|
const DatosBasicosSchema: z.ZodType<AIGeneratePlanInput["datosBasicos"]> = z
|
||||||
.object({
|
.object({
|
||||||
@@ -297,112 +312,44 @@ const IAConfigSchema: z.ZodType<AIGeneratePlanInput["iaConfig"]> = z.object({
|
|||||||
usarMCP: z.boolean().optional(),
|
usarMCP: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const SolicitudSchema = z.object({
|
||||||
|
// Usamos el helper aquí. Zod recibe string -> parsea -> valida estructura
|
||||||
|
datosBasicos: jsonFromString(DatosBasicosSchema),
|
||||||
|
|
||||||
|
iaConfig: jsonFromString(IAConfigSchema),
|
||||||
|
|
||||||
|
// Validamos directamente que sea un array de Archivos
|
||||||
|
// z.instanceof(File) funciona en entornos Web/Deno
|
||||||
|
archivosAdjuntos: z.array(z.instanceof(File)).optional().default([]),
|
||||||
|
});
|
||||||
|
|
||||||
function parseAndValidate(
|
function parseAndValidate(
|
||||||
formData: FormData,
|
formData: FormData,
|
||||||
): { success: true; data: AIGeneratePlanInput } | {
|
): { success: true; data: AIGeneratePlanInput } | {
|
||||||
success: false;
|
success: false;
|
||||||
errors: string[];
|
errors: string[];
|
||||||
} {
|
} {
|
||||||
const errors: string[] = [];
|
// 1. Convertimos el FormData a un objeto plano de JS para que Zod lo lea
|
||||||
|
const rawInput = {
|
||||||
|
datosBasicos: formData.get("datosBasicos"),
|
||||||
|
iaConfig: formData.get("iaConfig"),
|
||||||
|
// getAll es clave para obtener el array de archivos
|
||||||
|
archivosAdjuntos: formData.getAll("archivosAdjuntos"),
|
||||||
|
};
|
||||||
|
|
||||||
// --- A. Extracción de Strings ---
|
// 2. Dejamos que Zod haga TODO el trabajo sucio (null check, json parse, validación)
|
||||||
const rawBasicos = formData.get("datosBasicos");
|
const result = SolicitudSchema.safeParse(rawInput);
|
||||||
const rawIA = formData.get("iaConfig");
|
|
||||||
|
|
||||||
if (!rawBasicos) {
|
if (!result.success) {
|
||||||
return {
|
// Aplanamos los errores para devolver un array de strings limpio
|
||||||
success: false,
|
const errors = result.error.errors.map((issue) => {
|
||||||
errors: ["Falta el campo 'datosBasicos' en el FormData"],
|
const path = issue.path.join(".");
|
||||||
};
|
return `${path}: ${issue.message}`;
|
||||||
}
|
|
||||||
if (!rawIA) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
errors: ["Falta el campo 'iaConfig' en el FormData"],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- B. Parsing de JSON (Usamos 'unknown' en lugar de 'any') ---
|
|
||||||
let jsonBasicos: unknown;
|
|
||||||
let jsonIA: unknown;
|
|
||||||
|
|
||||||
try {
|
|
||||||
jsonBasicos = JSON.parse(rawBasicos as string);
|
|
||||||
} catch (_error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
errors: ["El JSON de 'datosBasicos' tiene un formato inválido"],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
jsonIA = JSON.parse(rawIA as string);
|
|
||||||
} catch (_error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
errors: ["El JSON de 'iaConfig' tiene un formato inválido"],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- C. Validación con Zod ---
|
|
||||||
const parsedBasicos = DatosBasicosSchema.safeParse(jsonBasicos);
|
|
||||||
const parsedIA = IAConfigSchema.safeParse(jsonIA);
|
|
||||||
|
|
||||||
// Recolectamos errores de Zod si existen
|
|
||||||
if (!parsedBasicos.success) {
|
|
||||||
parsedBasicos.error.errors.forEach((issue) => {
|
|
||||||
errors.push(
|
|
||||||
`Datos Básicos (${issue.path.join(". ")}): ${issue.message}`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (!parsedIA.success) {
|
|
||||||
parsedIA.error.errors.forEach((issue) => {
|
|
||||||
errors.push(`IA Config (${issue.path.join(". ")}): ${issue.message}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- D. Validación Manual de Archivos ---
|
|
||||||
const filesRaw = formData.getAll("archivosAdjuntos");
|
|
||||||
const filesClean: File[] = [];
|
|
||||||
|
|
||||||
filesRaw.forEach((entry, index) => {
|
|
||||||
if (entry instanceof File) {
|
|
||||||
filesClean.push(entry);
|
|
||||||
} else {
|
|
||||||
errors.push(
|
|
||||||
`El adjunto en la posición ${index} no es un archivo válido.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- E. Retorno de Errores ---
|
|
||||||
if (errors.length > 0) {
|
|
||||||
console.error("❌ Errores de validación:", errors);
|
|
||||||
return { success: false, errors };
|
return { success: false, errors };
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- F. Retorno de Éxito (Narrowing de Tipos) ---
|
// 3. Retorno exitoso con tipos inferidos automáticamente
|
||||||
|
return { success: true, data: result.data };
|
||||||
// TypeScript necesita esta verificación explícita.
|
|
||||||
// Aunque lógicamente sabemos que si errors.length === 0, entonces success es true,
|
|
||||||
// TS no conecta el array de errores con el resultado de Zod automáticamente.
|
|
||||||
if (!parsedBasicos.success || !parsedIA.success) {
|
|
||||||
// Este caso teóricamente es inalcanzable si la lógica de arriba es correcta,
|
|
||||||
// pero satisface al compilador para acceder a .data abajo.
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
errors: ["Error interno de estado inconsistente."],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ahora sí, TS sabe que parsedBasicos.data existe y es del tipo correcto
|
|
||||||
const finalInput: AIGeneratePlanInput = {
|
|
||||||
datosBasicos: parsedBasicos.data,
|
|
||||||
iaConfig: parsedIA.data,
|
|
||||||
archivosAdjuntos: filesClean,
|
|
||||||
};
|
|
||||||
|
|
||||||
return { success: true, data: finalInput };
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,11 @@ Deno.serve(async (req) => {
|
|||||||
// 1) Upload a Supabase Storage
|
// 1) Upload a Supabase Storage
|
||||||
const supabase_paths: string[] = [];
|
const supabase_paths: string[] = [];
|
||||||
for (const file of inputForm.files ?? []) {
|
for (const file of inputForm.files ?? []) {
|
||||||
const path = `${crypto.randomUUID()}-${(file.name)}`;
|
const safeName = file.name
|
||||||
|
.normalize("NFD") // 1. Descompone letras de tildes (í -> i + ´)
|
||||||
|
.replace(/[\u0300-\u036f]/g, "") // 2. Borra las tildes
|
||||||
|
.replace(/[^a-zA-Z0-9.-]/g, "_"); // 3. Reemplaza espacios y símbolos raros por "_"
|
||||||
|
const path = `${crypto.randomUUID()}-${safeName}`;
|
||||||
const { data, error } = await supabase.storage
|
const { data, error } = await supabase.storage
|
||||||
.from(SUPABASE_BUCKET)
|
.from(SUPABASE_BUCKET)
|
||||||
.upload(path, file, {
|
.upload(path, file, {
|
||||||
@@ -119,7 +123,7 @@ Deno.serve(async (req) => {
|
|||||||
|
|
||||||
type SuccessResponse = {
|
type SuccessResponse = {
|
||||||
ok: true;
|
ok: true;
|
||||||
openai_raw: unknown;
|
openai_raw: OpenAITypes.OpenAI.Responses.Response;
|
||||||
supabase_paths: string[];
|
supabase_paths: string[];
|
||||||
openai_file_ids: string[];
|
openai_file_ids: string[];
|
||||||
options: unknown;
|
options: unknown;
|
||||||
@@ -132,6 +136,7 @@ Deno.serve(async (req) => {
|
|||||||
openai_file_ids, // IDs en OpenAI
|
openai_file_ids, // IDs en OpenAI
|
||||||
options: inputForm.options, // opciones usadas
|
options: inputForm.options, // opciones usadas
|
||||||
};
|
};
|
||||||
|
response.openai_raw.output.
|
||||||
|
|
||||||
return json(response);
|
return json(response);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user