Se genera el contenido temático con IA #32
@@ -460,6 +460,7 @@ export type Database = {
|
||||
estado: Database["public"]["Enums"]["estado_conversacion"]
|
||||
id: string
|
||||
intento_archivado: number
|
||||
nombre: string | null
|
||||
openai_conversation_id: string
|
||||
plan_estudio_id: string
|
||||
}
|
||||
@@ -472,6 +473,7 @@ export type Database = {
|
||||
estado?: Database["public"]["Enums"]["estado_conversacion"]
|
||||
id?: string
|
||||
intento_archivado?: number
|
||||
nombre?: string | null
|
||||
openai_conversation_id: string
|
||||
plan_estudio_id: string
|
||||
}
|
||||
@@ -484,6 +486,7 @@ export type Database = {
|
||||
estado?: Database["public"]["Enums"]["estado_conversacion"]
|
||||
id?: string
|
||||
intento_archivado?: number
|
||||
nombre?: string | null
|
||||
openai_conversation_id?: string
|
||||
plan_estudio_id?: string
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@ import {
|
||||
type StructuredResponseOptions,
|
||||
} from "../_shared/openai-service.ts";
|
||||
|
||||
addEventListener("beforeunload", (ev: any) => {
|
||||
type BeforeUnloadWithDetail = Event & { detail?: { reason?: unknown } };
|
||||
|
||||
addEventListener("beforeunload", (ev: BeforeUnloadWithDetail) => {
|
||||
console.error(
|
||||
"ALERTA: La función se va a apagar. Razón:",
|
||||
ev?.detail?.reason,
|
||||
@@ -67,6 +69,55 @@ const JsonUpdateSchema = z
|
||||
|
||||
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 {
|
||||
return issues
|
||||
.map((issue, i) => {
|
||||
@@ -313,7 +364,11 @@ Deno.serve(async (req: Request): Promise<Response> => {
|
||||
}\n` +
|
||||
`- Descripción del enfoque académico (sobre el contenido de la respuesta generada): ${
|
||||
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> =
|
||||
typeof estructura?.definicion === "object" &&
|
||||
@@ -321,6 +376,8 @@ Deno.serve(async (req: Request): Promise<Response> => {
|
||||
? (estructura.definicion as Record<string, unknown>)
|
||||
: {};
|
||||
|
||||
const schemaForAI = withColumnDefsAndRefs(schemaDef);
|
||||
|
||||
const aiStructuredPayload: StructuredResponseOptions = {
|
||||
model: AI_GENERATE_SUBJECT_UPDATE_MODELO,
|
||||
input: [
|
||||
@@ -331,7 +388,7 @@ Deno.serve(async (req: Request): Promise<Response> => {
|
||||
format: {
|
||||
type: "json_schema",
|
||||
name: "asignatura_contenido_tematico",
|
||||
schema: schemaDef,
|
||||
schema: schemaForAI,
|
||||
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"] =
|
||||
{
|
||||
@@ -389,6 +447,17 @@ Deno.serve(async (req: Request): Promise<Response> => {
|
||||
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)
|
||||
if (payload.plan_estudio_id !== undefined) {
|
||||
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>)
|
||||
: {};
|
||||
|
||||
const schemaForAI = withColumnDefsAndRefs(schemaDef);
|
||||
|
||||
const aiStructuredPayload: StructuredResponseOptions = {
|
||||
model: AI_GENERATE_SUBJECT_INSERT_MODELO,
|
||||
input: [
|
||||
@@ -571,7 +642,7 @@ Deno.serve(async (req: Request): Promise<Response> => {
|
||||
format: {
|
||||
type: "json_schema",
|
||||
name: "asignatura_contenido_tematico",
|
||||
schema: schemaDef,
|
||||
schema: schemaForAI,
|
||||
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:
|
||||
Database["public"]["Tables"]["asignaturas"]["Insert"] = {
|
||||
@@ -661,6 +734,15 @@ Deno.serve(async (req: Request): Promise<Response> => {
|
||||
} 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
|
||||
.from("asignaturas")
|
||||
.insert(asignaturaInsert)
|
||||
@@ -798,3 +880,63 @@ function parseAndValidate(
|
||||
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user