Merge pull request 'Se genera el contenido temático con IA' (#32) from issue/31-generacin-de-columnas-de-tabla-asignaturas-con-ia into main
Deploy Function / deploy (push) Successful in 21s
Deploy Migrations to Production / deploy (push) Successful in 11s

Reviewed-on: AlexRG/genesis-2#32
This commit was merged in pull request #32.
This commit is contained in:
2026-02-23 23:08:41 +00:00
2 changed files with 151 additions and 6 deletions
@@ -460,6 +460,7 @@ export type Database = {
estado: Database["public"]["Enums"]["estado_conversacion"] estado: Database["public"]["Enums"]["estado_conversacion"]
id: string id: string
intento_archivado: number intento_archivado: number
nombre: string | null
openai_conversation_id: string openai_conversation_id: string
plan_estudio_id: string plan_estudio_id: string
} }
@@ -472,6 +473,7 @@ export type Database = {
estado?: Database["public"]["Enums"]["estado_conversacion"] estado?: Database["public"]["Enums"]["estado_conversacion"]
id?: string id?: string
intento_archivado?: number intento_archivado?: number
nombre?: string | null
openai_conversation_id: string openai_conversation_id: string
plan_estudio_id: string plan_estudio_id: string
} }
@@ -484,6 +486,7 @@ export type Database = {
estado?: Database["public"]["Enums"]["estado_conversacion"] estado?: Database["public"]["Enums"]["estado_conversacion"]
id?: string id?: string
intento_archivado?: number intento_archivado?: number
nombre?: string | null
openai_conversation_id?: string openai_conversation_id?: string
plan_estudio_id?: string plan_estudio_id?: string
} }
+148 -6
View File
@@ -9,7 +9,9 @@ import {
type StructuredResponseOptions, type StructuredResponseOptions,
} from "../_shared/openai-service.ts"; } from "../_shared/openai-service.ts";
addEventListener("beforeunload", (ev: any) => { type BeforeUnloadWithDetail = Event & { detail?: { reason?: unknown } };
addEventListener("beforeunload", (ev: BeforeUnloadWithDetail) => {
console.error( console.error(
"ALERTA: La función se va a apagar. Razón:", "ALERTA: La función se va a apagar. Razón:",
ev?.detail?.reason, ev?.detail?.reason,
@@ -67,6 +69,55 @@ const JsonUpdateSchema = z
type EdgeAIGenerateSubjectJsonUpdateInput = z.infer<typeof JsonUpdateSchema>; type EdgeAIGenerateSubjectJsonUpdateInput = z.infer<typeof JsonUpdateSchema>;
function withColumnDefsAndRefs(
schemaDef: Record<string, unknown>,
): Record<string, unknown> {
const nextSchema: Record<string, unknown> = {
...schemaDef,
$defs: definicionesDeEstructurasDeColumnas,
};
const props = nextSchema["properties"];
if (!props || typeof props !== "object" || Array.isArray(props)) {
return nextSchema;
}
const nextProps: Record<string, unknown> = {
...props as Record<string, unknown>,
};
for (const [key, value] of Object.entries(nextProps)) {
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
const xColumn = (value as Record<string, unknown>)["x-column"];
if (typeof xColumn !== "string" || !xColumn.length) continue;
nextProps[key] = { $ref: `#/$defs/${xColumn}` };
}
nextSchema["properties"] = nextProps;
return nextSchema;
}
function splitAiOutputStringsAndColumns(
aiOutput: unknown,
): { aiOutputJson: Json; columnasGeneradas: Record<string, unknown> } {
if (!aiOutput || typeof aiOutput !== "object" || Array.isArray(aiOutput)) {
return { aiOutputJson: aiOutput as unknown as Json, columnasGeneradas: {} };
}
const record = aiOutput as Record<string, unknown>;
const stringsOnly: Record<string, string> = {};
const columnasGeneradas: Record<string, unknown> = {};
for (const [key, value] of Object.entries(record)) {
if (typeof value === "string") {
stringsOnly[key] = value;
} else {
columnasGeneradas[key] = value;
}
}
return { aiOutputJson: stringsOnly as unknown as Json, columnasGeneradas };
}
function formatZodIssues(issues: z.ZodIssue[]): string { function formatZodIssues(issues: z.ZodIssue[]): string {
return issues return issues
.map((issue, i) => { .map((issue, i) => {
@@ -313,7 +364,11 @@ Deno.serve(async (req: Request): Promise<Response> => {
}\n` + }\n` +
`- Descripción del enfoque académico (sobre el contenido de la respuesta generada): ${ `- Descripción del enfoque académico (sobre el contenido de la respuesta generada): ${
payload.descripcionEnfoqueAcademico ?? "(ninguna)" payload.descripcionEnfoqueAcademico ?? "(ninguna)"
}`; }\n\n` +
`REGLA ESTRICTA MATEMÁTICA:\n` +
`Si generas el 'contenido_tematico', la suma total de las 'horasEstimadas' de todos los temas en todas las unidades DEBE coincidir exactamente con el total de Horas académicas indicadas arriba (${
resolved.horas_academicas ?? 0
}). No te pases ni te falten horas.`;
const schemaDef: Record<string, unknown> = const schemaDef: Record<string, unknown> =
typeof estructura?.definicion === "object" && typeof estructura?.definicion === "object" &&
@@ -321,6 +376,8 @@ Deno.serve(async (req: Request): Promise<Response> => {
? (estructura.definicion as Record<string, unknown>) ? (estructura.definicion as Record<string, unknown>)
: {}; : {};
const schemaForAI = withColumnDefsAndRefs(schemaDef);
const aiStructuredPayload: StructuredResponseOptions = { const aiStructuredPayload: StructuredResponseOptions = {
model: AI_GENERATE_SUBJECT_UPDATE_MODELO, model: AI_GENERATE_SUBJECT_UPDATE_MODELO,
input: [ input: [
@@ -331,7 +388,7 @@ Deno.serve(async (req: Request): Promise<Response> => {
format: { format: {
type: "json_schema", type: "json_schema",
name: "asignatura_contenido_tematico", name: "asignatura_contenido_tematico",
schema: schemaDef, schema: schemaForAI,
strict: true, strict: true,
}, },
}, },
@@ -380,7 +437,8 @@ Deno.serve(async (req: Request): Promise<Response> => {
); );
} }
const aiOutputJson: Json = aiOutput as unknown as Json; const { aiOutputJson, columnasGeneradas } =
splitAiOutputStringsAndColumns(aiOutput);
const updatePatch: Database["public"]["Tables"]["asignaturas"]["Update"] = const updatePatch: Database["public"]["Tables"]["asignaturas"]["Update"] =
{ {
@@ -389,6 +447,17 @@ Deno.serve(async (req: Request): Promise<Response> => {
estado: "borrador", estado: "borrador",
}; };
for (const value of Object.values(columnasGeneradas)) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
continue;
}
const xColumn = (value as Record<string, unknown>)["x-column"];
const xDef = (value as Record<string, unknown>)["x-definicion"];
if (typeof xColumn !== "string" || !xColumn.length) continue;
(updatePatch as unknown as Record<string, unknown>)[xColumn] =
xDef as unknown as Json;
}
// Apply only provided JSON fields (do not overwrite if missing) // Apply only provided JSON fields (do not overwrite if missing)
if (payload.plan_estudio_id !== undefined) { if (payload.plan_estudio_id !== undefined) {
updatePatch.plan_estudio_id = payload.plan_estudio_id; updatePatch.plan_estudio_id = payload.plan_estudio_id;
@@ -561,6 +630,8 @@ Deno.serve(async (req: Request): Promise<Response> => {
? (estructura.definicion as Record<string, unknown>) ? (estructura.definicion as Record<string, unknown>)
: {}; : {};
const schemaForAI = withColumnDefsAndRefs(schemaDef);
const aiStructuredPayload: StructuredResponseOptions = { const aiStructuredPayload: StructuredResponseOptions = {
model: AI_GENERATE_SUBJECT_INSERT_MODELO, model: AI_GENERATE_SUBJECT_INSERT_MODELO,
input: [ input: [
@@ -571,7 +642,7 @@ Deno.serve(async (req: Request): Promise<Response> => {
format: { format: {
type: "json_schema", type: "json_schema",
name: "asignatura_contenido_tematico", name: "asignatura_contenido_tematico",
schema: schemaDef, schema: schemaForAI,
strict: true, strict: true,
}, },
}, },
@@ -623,7 +694,9 @@ Deno.serve(async (req: Request): Promise<Response> => {
); );
} }
const aiOutputJson: Json = aiOutput as unknown as Json; const { aiOutputJson, columnasGeneradas } = splitAiOutputStringsAndColumns(
aiOutput,
);
const asignaturaInsert: const asignaturaInsert:
Database["public"]["Tables"]["asignaturas"]["Insert"] = { Database["public"]["Tables"]["asignaturas"]["Insert"] = {
@@ -661,6 +734,15 @@ Deno.serve(async (req: Request): Promise<Response> => {
} as unknown as Json, } as unknown as Json,
}; };
for (const value of Object.values(columnasGeneradas)) {
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
const xColumn = (value as Record<string, unknown>)["x-column"];
const xDef = (value as Record<string, unknown>)["x-definicion"];
if (typeof xColumn !== "string" || !xColumn.length) continue;
(asignaturaInsert as unknown as Record<string, unknown>)[xColumn] =
xDef as unknown as Json;
}
const { data: asignatura, error: asignaturaError } = await supabaseService const { data: asignatura, error: asignaturaError } = await supabaseService
.from("asignaturas") .from("asignaturas")
.insert(asignaturaInsert) .insert(asignaturaInsert)
@@ -798,3 +880,63 @@ function parseAndValidate(
return { success: true, data: result.data }; return { success: true, data: result.data };
} }
const definicionesDeEstructurasDeColumnas = {
contenido_tematico: {
"type": "object",
"properties": {
"x-column": {
"type": "string",
"enum": [
"contenido_tematico",
],
},
"x-definicion": {
"type": "array",
"items": {
"type": "object",
"properties": {
"unidad": {
"type": "integer",
},
"titulo": {
"type": "string",
},
"temas": {
"type": "array",
"items": {
"type": "object",
"properties": {
"nombre": {
"type": "string",
},
"horasEstimadas": {
"type": "integer",
"description":
"Horas del tema. La suma de todas las horasEstimadas debe ser igual a las horas académicas del prompt.",
},
},
"required": [
"nombre",
"horasEstimadas",
],
"additionalProperties": false,
},
},
},
"required": [
"unidad",
"titulo",
"temas",
],
"additionalProperties": false,
},
},
},
"required": [
"x-column",
"x-definicion",
],
"additionalProperties": false,
},
};