Merge branch 'main' into issue/42-chats-de-la-ia-en-segundo-plano

This commit is contained in:
2026-03-09 20:19:26 +00:00
24 changed files with 11079 additions and 68 deletions
+163 -16
View File
@@ -81,6 +81,56 @@ export type Database = {
},
]
}
asignatura_mensajes_ia: {
Row: {
campos: string[]
conversacion_asignatura_id: string
enviado_por: string
estado: Database["public"]["Enums"]["estado_mensaje_ia"]
fecha_actualizacion: string
fecha_creacion: string
id: string
is_refusal: boolean
mensaje: string
propuesta: Json | null
respuesta: string | null
}
Insert: {
campos?: string[]
conversacion_asignatura_id: string
enviado_por?: string
estado?: Database["public"]["Enums"]["estado_mensaje_ia"]
fecha_actualizacion?: string
fecha_creacion?: string
id?: string
is_refusal?: boolean
mensaje: string
propuesta?: Json | null
respuesta?: string | null
}
Update: {
campos?: string[]
conversacion_asignatura_id?: string
enviado_por?: string
estado?: Database["public"]["Enums"]["estado_mensaje_ia"]
fecha_actualizacion?: string
fecha_creacion?: string
id?: string
is_refusal?: boolean
mensaje?: string
propuesta?: Json | null
respuesta?: string | null
}
Relationships: [
{
foreignKeyName: "asignatura_mensajes_ia_conversacion_asignatura_id_fkey"
columns: ["conversacion_asignatura_id"]
isOneToOne: false
referencedRelation: "conversaciones_asignatura"
referencedColumns: ["id"]
},
]
}
asignaturas: {
Row: {
actualizado_en: string
@@ -91,6 +141,7 @@ export type Database = {
creado_en: string
creado_por: string | null
creditos: number
criterios_de_evaluacion: Json
datos: Json
estado: Database["public"]["Enums"]["estado_asignatura"]
estructura_id: string | null
@@ -115,6 +166,7 @@ export type Database = {
creado_en?: string
creado_por?: string | null
creditos: number
criterios_de_evaluacion?: Json
datos?: Json
estado?: Database["public"]["Enums"]["estado_asignatura"]
estructura_id?: string | null
@@ -139,6 +191,7 @@ export type Database = {
creado_en?: string
creado_por?: string | null
creditos?: number
criterios_de_evaluacion?: Json
datos?: Json
estado?: Database["public"]["Enums"]["estado_asignatura"]
estructura_id?: string | null
@@ -176,6 +229,13 @@ export type Database = {
referencedRelation: "estructuras_asignatura"
referencedColumns: ["id"]
},
{
foreignKeyName: "asignaturas_estructura_id_fkey"
columns: ["estructura_id"]
isOneToOne: false
referencedRelation: "plantilla_asignatura"
referencedColumns: ["estructura_id"]
},
{
foreignKeyName: "asignaturas_linea_plan_fk_compuesta"
columns: ["linea_plan_id", "plan_estudio_id"]
@@ -241,6 +301,13 @@ export type Database = {
referencedRelation: "asignaturas"
referencedColumns: ["id"]
},
{
foreignKeyName: "bibliografia_asignatura_asignatura_id_fkey"
columns: ["asignatura_id"]
isOneToOne: false
referencedRelation: "plantilla_asignatura"
referencedColumns: ["asignatura_id"]
},
{
foreignKeyName: "bibliografia_asignatura_creado_por_fkey"
columns: ["creado_por"]
@@ -295,6 +362,13 @@ export type Database = {
referencedRelation: "asignaturas"
referencedColumns: ["id"]
},
{
foreignKeyName: "cambios_asignatura_asignatura_id_fkey"
columns: ["asignatura_id"]
isOneToOne: false
referencedRelation: "plantilla_asignatura"
referencedColumns: ["asignatura_id"]
},
{
foreignKeyName: "cambios_asignatura_cambiado_por_fkey"
columns: ["cambiado_por"]
@@ -441,6 +515,13 @@ export type Database = {
referencedRelation: "asignaturas"
referencedColumns: ["id"]
},
{
foreignKeyName: "conversaciones_asignatura_asignatura_id_fkey"
columns: ["asignatura_id"]
isOneToOne: false
referencedRelation: "plantilla_asignatura"
referencedColumns: ["asignatura_id"]
},
{
foreignKeyName: "conversaciones_asignatura_creado_por_fkey"
columns: ["creado_por"]
@@ -552,7 +633,8 @@ export type Database = {
definicion: Json
id: string
nombre: string
version: string | null
template_id: string | null
tipo: Database["public"]["Enums"]["tipo_estructura_plan"] | null
}
Insert: {
actualizado_en?: string
@@ -560,7 +642,8 @@ export type Database = {
definicion?: Json
id?: string
nombre: string
version?: string | null
template_id?: string | null
tipo?: Database["public"]["Enums"]["tipo_estructura_plan"] | null
}
Update: {
actualizado_en?: string
@@ -568,7 +651,8 @@ export type Database = {
definicion?: Json
id?: string
nombre?: string
version?: string | null
template_id?: string | null
tipo?: Database["public"]["Enums"]["tipo_estructura_plan"] | null
}
Relationships: []
}
@@ -692,6 +776,13 @@ export type Database = {
referencedRelation: "asignaturas"
referencedColumns: ["id"]
},
{
foreignKeyName: "interacciones_ia_asignatura_id_fkey"
columns: ["asignatura_id"]
isOneToOne: false
referencedRelation: "plantilla_asignatura"
referencedColumns: ["asignatura_id"]
},
{
foreignKeyName: "interacciones_ia_plan_estudio_id_fkey"
columns: ["plan_estudio_id"]
@@ -798,6 +889,56 @@ export type Database = {
},
]
}
plan_mensajes_ia: {
Row: {
campos: string[]
conversacion_plan_id: string
enviado_por: string
estado: Database["public"]["Enums"]["estado_mensaje_ia"]
fecha_actualizacion: string
fecha_creacion: string
id: string
is_refusal: boolean
mensaje: string
propuesta: Json | null
respuesta: string | null
}
Insert: {
campos?: string[]
conversacion_plan_id: string
enviado_por?: string
estado?: Database["public"]["Enums"]["estado_mensaje_ia"]
fecha_actualizacion?: string
fecha_creacion?: string
id?: string
is_refusal?: boolean
mensaje: string
propuesta?: Json | null
respuesta?: string | null
}
Update: {
campos?: string[]
conversacion_plan_id?: string
enviado_por?: string
estado?: Database["public"]["Enums"]["estado_mensaje_ia"]
fecha_actualizacion?: string
fecha_creacion?: string
id?: string
is_refusal?: boolean
mensaje?: string
propuesta?: Json | null
respuesta?: string | null
}
Relationships: [
{
foreignKeyName: "plan_mensajes_ia_conversacion_plan_id_fkey"
columns: ["conversacion_plan_id"]
isOneToOne: false
referencedRelation: "conversaciones_plan"
referencedColumns: ["id"]
},
]
}
planes_estudio: {
Row: {
activo: boolean
@@ -934,6 +1075,13 @@ export type Database = {
referencedRelation: "asignaturas"
referencedColumns: ["id"]
},
{
foreignKeyName: "responsables_asignatura_asignatura_id_fkey"
columns: ["asignatura_id"]
isOneToOne: false
referencedRelation: "plantilla_asignatura"
referencedColumns: ["asignatura_id"]
},
{
foreignKeyName: "responsables_asignatura_usuario_id_fkey"
columns: ["usuario_id"]
@@ -1199,6 +1347,14 @@ export type Database = {
}
}
Views: {
plantilla_asignatura: {
Row: {
asignatura_id: string | null
estructura_id: string | null
template_id: string | null
}
Relationships: []
}
plantilla_plan: {
Row: {
estructura_id: string | null
@@ -1221,13 +1377,9 @@ export type Database = {
unaccent_immutable: { Args: { "": string }; Returns: string }
}
Enums: {
estado_asignatura:
| "borrador"
| "revisada"
| "aprobada"
| "generando"
| "fallida"
estado_asignatura: "borrador" | "revisada" | "aprobada" | "generando"
estado_conversacion: "ACTIVA" | "ARCHIVANDO" | "ARCHIVADA" | "ERROR"
estado_mensaje_ia: "PROCESANDO" | "COMPLETADO" | "ERROR"
estado_tarea_revision: "PENDIENTE" | "COMPLETADA" | "OMITIDA"
fuente_cambio: "HUMANO" | "IA"
nivel_plan_estudio:
@@ -1400,14 +1552,9 @@ export const Constants = {
},
public: {
Enums: {
estado_asignatura: [
"borrador",
"revisada",
"aprobada",
"generando",
"fallida",
],
estado_asignatura: ["borrador", "revisada", "aprobada", "generando"],
estado_conversacion: ["ACTIVA", "ARCHIVANDO", "ARCHIVADA", "ERROR"],
estado_mensaje_ia: ["PROCESANDO", "COMPLETADO", "ERROR"],
estado_tarea_revision: ["PENDIENTE", "COMPLETADA", "OMITIDA"],
fuente_cambio: ["HUMANO", "IA"],
nivel_plan_estudio: [
@@ -414,10 +414,9 @@ Deno.serve(async (req: Request): Promise<Response> => {
estructura_id: resolved.estructura_id as string,
nombre: resolved.nombre as string,
codigo: resolved.codigo ?? null,
tipo:
(resolved.tipo ?? undefined) as Database["public"]["Tables"][
"asignaturas"
]["Insert"]["tipo"],
tipo: (resolved.tipo ?? undefined) as Database["public"]["Tables"][
"asignaturas"
]["Insert"]["tipo"],
creditos: resolved.creditos as number,
horas_academicas: resolved.horas_academicas ?? null,
horas_independientes: resolved.horas_independientes ?? null,
@@ -509,10 +508,11 @@ Deno.serve(async (req: Request): Promise<Response> => {
iaConfig.instruccionesAdicionalesIA ?? "(ninguna)"
}` +
archivosAdjuntosTexto +
`\n\nREGLA 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 (${
`\n\nREGLAS ESTRICTAS MATEMÁTICAS:\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.`;
}). No te pases ni te falten horas.\n` +
`- Si generas los 'criterios_de_evaluacion', la suma total de los 'porcentajes' de todos los criterios DEBE ser exactamente 100%. No te pases ni te falten porcentajes.`;
const schemaDef: Record<string, unknown> =
typeof estructura?.definicion === "object" &&
@@ -658,4 +658,39 @@ const definicionesDeEstructurasDeColumnas = {
],
"additionalProperties": false,
},
criterios_de_evaluacion: {
"type": "object",
"properties": {
"x-column": {
"type": "string",
"enum": [
"criterios_de_evaluacion",
],
},
"x-definicion": {
"type": "array",
"items": {
"type": "object",
"properties": {
"criterio": {
"type": "string",
},
"porcentaje": {
"type": "integer",
},
},
"required": [
"criterio",
"porcentaje",
],
"additionalProperties": false,
},
},
},
"required": [
"x-column",
"x-definicion",
],
"additionalProperties": false,
},
};
@@ -0,0 +1,3 @@
# Configuration for private npm package dependencies
# For more information on using private registries with Edge Functions, see:
# https://supabase.com/docs/guides/functions/import-maps#importing-from-private-registries
@@ -0,0 +1,6 @@
{
"imports": {
"zod": "https://deno.land/x/zod@v3.22.4/mod.ts",
"@supabase/functions-js": "jsr:@supabase/functions-js@^2"
}
}
@@ -0,0 +1,192 @@
import "@supabase/functions-js/edge-runtime.d.ts";
import { z } from "zod";
import { corsHeaders } from "../_shared/cors.ts";
import { HttpError, sendError, sendSuccess } from "../_shared/utils.ts";
console.log("Starting buscar-bibliografia function");
type GoogleBooksVolume = Record<string, unknown>;
interface GoogleBooksVolumesListResponse {
kind?: string;
totalItems?: number;
items?: GoogleBooksVolume[];
}
const SearchTermsSchema = z
.object({
q: z.string().min(1, "q es requerido"),
maxResults: z.number().int().min(0).max(40),
orderBy: z.enum(["newest", "relevance"]).optional(),
})
.passthrough();
const BodySchema = z
.object({
searchTerms: SearchTermsSchema,
})
.strict();
type BuscarBibliografiaRequest = z.infer<typeof BodySchema>;
function formatZodIssues(issues: z.ZodIssue[]): string {
return issues
.map((issue, i) => {
const path = issue.path.length ? issue.path.join(".") : "(root)";
return `${i + 1}. ${path}: ${issue.message}`;
})
.join("\n");
}
function buildUrlWithSearchTerms(
baseUrl: string,
searchTerms: Record<string, unknown>,
): string {
const url = new URL(baseUrl);
for (const [key, value] of Object.entries(searchTerms)) {
if (value === undefined || value === null) continue;
if (Array.isArray(value)) {
for (const v of value) {
if (v === undefined || v === null) continue;
url.searchParams.append(key, String(v));
}
continue;
}
url.searchParams.set(key, String(value));
}
return url.toString();
}
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 {
if (req.method !== "POST") {
throw new HttpError(405, "Método no permitido.", "METHOD_NOT_ALLOWED", {
method: req.method,
});
}
const contentType = (req.headers.get("content-type") || "").toLowerCase();
if (!contentType.includes("application/json")) {
throw new HttpError(
415,
"Content-Type no soportado.",
"UNSUPPORTED_MEDIA_TYPE",
{ contentType, expected: "application/json" },
);
}
let rawBody: unknown;
try {
rawBody = await req.json();
} catch (e) {
throw new HttpError(400, "Body JSON inválido.", "INVALID_JSON", {
cause: e,
});
}
const parsed = BodySchema.safeParse(rawBody);
if (!parsed.success) {
throw new HttpError(
422,
formatZodIssues(parsed.error.issues),
"VALIDATION_ERROR",
parsed.error,
);
}
const body: BuscarBibliografiaRequest = parsed.data;
const GOOGLE_API_KEY = Deno.env.get("GOOGLE_API_KEY");
if (!GOOGLE_API_KEY) {
throw new HttpError(
500,
"Configuración del servidor incompleta.",
"MISSING_ENV",
{ missing: ["GOOGLE_API_KEY"] },
);
}
const baseUrl = "https://www.googleapis.com/books/v1/volumes";
const requestUrl = buildUrlWithSearchTerms(baseUrl, {
...body.searchTerms,
key: GOOGLE_API_KEY,
});
const googleResp = await fetch(requestUrl, {
method: "GET",
headers: {
Accept: "application/json",
},
});
if (!googleResp.ok) {
const text = await googleResp.text().catch(() => "");
throw new HttpError(
502,
"Error al consultar Google Books.",
"GOOGLE_BOOKS_REQUEST_FAILED",
{
status: googleResp.status,
statusText: googleResp.statusText,
body: text || null,
},
);
}
const data = (await googleResp.json()) as GoogleBooksVolumesListResponse;
const items = Array.isArray(data?.items) ? data.items : [];
return sendSuccess(items);
} 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",
);
}
});
/* To invoke locally:
1. Run `supabase start` (see: https://supabase.com/docs/reference/cli/supabase-start)
2. Make an HTTP request:
curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/buscar-bibliografia' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \
--header 'Content-Type: application/json' \
--data '{"name":"Functions"}'
*/