diff --git a/notebooks/background-openai.ipynb b/notebooks/background-openai.ipynb
index 4f33a78..4450372 100644
--- a/notebooks/background-openai.ipynb
+++ b/notebooks/background-openai.ipynb
@@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 1,
"id": "bb61ec88",
"metadata": {},
"outputs": [],
@@ -14,12 +14,12 @@
" export: true,\n",
"});\n",
"\n",
- "const openai = new OpenAI();"
+ "const openai = new OpenAI();\n"
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 2,
"id": "8baca5f2",
"metadata": {},
"outputs": [],
@@ -27,7 +27,170 @@
"import { createClient } from \"@supabase/supabase-js\";\n",
"const supabaseUrl = Deno.env.get(\"SUPABASE_URL\") || env.SUPABASE_URL;\n",
"const supabaseKey = Deno.env.get(\"SUPABASE_ANON_KEY\") || env.SUPABASE_ANON_KEY;\n",
- "const supabase = createClient(supabaseUrl, supabaseKey);"
+ "const supabase = createClient(supabaseUrl, supabaseKey);\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "923b8e08",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Esperando a que la IA termine con la asignatura fe5eb09c-501b-4db6-b6d3-f02bdcfdc4b4...\n",
+ "Conexión WebSocket establecida. La celda está esperando cambios...\n"
+ ]
+ },
+ {
+ "ename": "",
+ "evalue": "",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[1;31mThe Kernel crashed while executing code in the current cell or a previous cell. \n",
+ "\u001b[1;31mPlease review the code in the cell(s) to identify a possible cause of the failure. \n",
+ "\u001b[1;31mClick here for more info. \n",
+ "\u001b[1;31mView Jupyter log for further details."
+ ]
+ }
+ ],
+ "source": [
+ "const ID_ASIGNATURA = \"fe5eb09c-501b-4db6-b6d3-f02bdcfdc4b4\";\n",
+ "console.log(\n",
+ " `Esperando a que la IA termine con la asignatura ${ID_ASIGNATURA}...`,\n",
+ ");\n",
+ "\n",
+ "if (!supabase) {\n",
+ " console.log(\"No hay supabase\");\n",
+ "} else {\n",
+ " // 2. Armas la \"trampa\" (el listener)\n",
+ " await new Promise((resolve) => {\n",
+ " const canal = supabase\n",
+ " .channel(\"test-asignatura\")\n",
+ " .on(\n",
+ " \"postgres_changes\",\n",
+ " {\n",
+ " event: \"UPDATE\",\n",
+ " schema: \"public\",\n",
+ " table: \"asignaturas\",\n",
+ " filter: `id=eq.${ID_ASIGNATURA}`,\n",
+ " },\n",
+ " (payload) => {\n",
+ " const nuevoEstado = payload.new.estado;\n",
+ "\n",
+ " if (nuevoEstado !== \"generando\") {\n",
+ " console.log(\n",
+ " `¡Proceso completado! El nuevo estado es: ${nuevoEstado}`,\n",
+ " );\n",
+ "\n",
+ " supabase.removeChannel(canal);\n",
+ " resolve(true); // ¡Aquí destrabamos la celda de Jupyter!\n",
+ " } else {\n",
+ " console.log(\"Hubo un update, pero sigue generando...\");\n",
+ " }\n",
+ " },\n",
+ " )\n",
+ " .subscribe((status) => {\n",
+ " if (status === \"SUBSCRIBED\") {\n",
+ " console.log(\n",
+ " \"Conexión WebSocket establecida. La celda está esperando cambios...\",\n",
+ " );\n",
+ " }\n",
+ " });\n",
+ " });\n",
+ "\n",
+ " console.log(\"Script finalizado correctamente.\");\n",
+ "}\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8a1509d8",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Iniciando conexión abierta y sin filtros...\n",
+ "Conectado. Esperando cualquier cambio en la tabla asignaturas...\n"
+ ]
+ },
+ {
+ "ename": "",
+ "evalue": "",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[1;31mThe Kernel crashed while executing code in the current cell or a previous cell. \n",
+ "\u001b[1;31mPlease review the code in the cell(s) to identify a possible cause of the failure. \n",
+ "\u001b[1;31mClick here for more info. \n",
+ "\u001b[1;31mView Jupyter log for further details."
+ ]
+ }
+ ],
+ "source": [
+ "console.log(`Iniciando conexión abierta y sin filtros...`);\n",
+ "\n",
+ "await new Promise((resolve) => {\n",
+ " const canal = supabase\n",
+ " .channel(\"test-global-asignaturas\")\n",
+ " .on(\n",
+ " \"postgres_changes\",\n",
+ " {\n",
+ " event: \"*\", // Escuchamos absolutamente TODO\n",
+ " schema: \"public\",\n",
+ " table: \"asignaturas\",\n",
+ " },\n",
+ " (payload) => {\n",
+ " // Imprimimos todo el objeto tal como llega del servidor\n",
+ " console.log(\"🚨 ¡EVENTO RECIBIDO EN CRUDO!\", payload);\n",
+ "\n",
+ " // Cerramos la conexión para liberar la celda\n",
+ " supabase.removeChannel(canal);\n",
+ " resolve(true);\n",
+ " },\n",
+ " )\n",
+ " .subscribe((status, err) => {\n",
+ " if (status === \"SUBSCRIBED\") {\n",
+ " console.log(\n",
+ " \"Conectado. Esperando cualquier cambio en la tabla asignaturas...\",\n",
+ " );\n",
+ " }\n",
+ " if (err) {\n",
+ " console.error(\"Error en el socket:\", err);\n",
+ " }\n",
+ " });\n",
+ "});\n",
+ "\n",
+ "console.log(\"Script finalizado.\");\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "3602b930",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import(\"https://esm.sh/@supabase/supabase-js\").then(({ createClient }) => {\n",
+ " const supabase = createClient(\n",
+ " \"https://bxkskdxwppdlplrkidcz.supabase.co\",\n",
+ " \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJ4a3NrZHh3cHBkbHBscmtpZGN6Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzIwMzU2NDMsImV4cCI6MjA4NzYxMTY0M30.RIyvmB8Ij_qrsiMn0f65awiG5VaCUA5dvb117wuU3jo\",\n",
+ " );\n",
+ "\n",
+ " supabase.channel(\"prueba-consola\")\n",
+ " .on(\"postgres_changes\", {\n",
+ " event: \"*\",\n",
+ " schema: \"public\",\n",
+ " table: \"asignaturas\",\n",
+ " }, (payload) => {\n",
+ " console.log(\"🔥 ¡LLEGÓ EL EVENTO DESDE SUPABASE!\", payload);\n",
+ " })\n",
+ " .subscribe((status) => console.log(\"Estado de la conexión:\", status));\n",
+ "});\n"
]
},
{
@@ -55,7 +218,7 @@
},
{
"cell_type": "code",
- "execution_count": 21,
+ "execution_count": null,
"id": "8abb080e",
"metadata": {},
"outputs": [
@@ -71,12 +234,12 @@
}
],
"source": [
- "plan.id"
+ "plan.id;\n"
]
},
{
"cell_type": "code",
- "execution_count": 22,
+ "execution_count": null,
"id": "a382bdc6",
"metadata": {},
"outputs": [
@@ -144,7 +307,6 @@
}
],
"source": [
- "\n",
"let response = await openai.responses.create({\n",
" model: \"gpt-5-mini\",\n",
" input: \"Hola mundo\",\n",
@@ -183,7 +345,7 @@
},
{
"cell_type": "code",
- "execution_count": 23,
+ "execution_count": null,
"id": "6b2470ed",
"metadata": {},
"outputs": [
@@ -293,12 +455,12 @@
" response = await openai.responses.retrieve(response.id);\n",
"}\n",
"\n",
- "response;"
+ "response;\n"
]
},
{
"cell_type": "code",
- "execution_count": 24,
+ "execution_count": null,
"id": "ebb67bd8",
"metadata": {},
"outputs": [
@@ -314,12 +476,12 @@
}
],
"source": [
- "response.metadata"
+ "response.metadata;\n"
]
},
{
"cell_type": "code",
- "execution_count": 27,
+ "execution_count": null,
"id": "cd18fc2d",
"metadata": {},
"outputs": [
@@ -365,7 +527,7 @@
"source": [
"await supabase.from(response.metadata.tabla).update({\n",
" datos: JSON.parse(response.output_text),\n",
- "}).eq(\"id\", response.metadata.id).select(\"*\").single();"
+ "}).eq(\"id\", response.metadata.id).select(\"*\").single();\n"
]
},
{
diff --git a/supabase/functions/_shared/database.types.ts b/supabase/functions/_shared/database.types.ts
index 3e1d507..e69de29 100644
--- a/supabase/functions/_shared/database.types.ts
+++ b/supabase/functions/_shared/database.types.ts
@@ -1,1446 +0,0 @@
-export type Json =
- | string
- | number
- | boolean
- | null
- | { [key: string]: Json | undefined }
- | Json[]
-
-export type Database = {
- graphql_public: {
- Tables: {
- [_ in never]: never
- }
- Views: {
- [_ in never]: never
- }
- Functions: {
- graphql: {
- Args: {
- extensions?: Json
- operationName?: string
- query?: string
- variables?: Json
- }
- Returns: Json
- }
- }
- Enums: {
- [_ in never]: never
- }
- CompositeTypes: {
- [_ in never]: never
- }
- }
- public: {
- Tables: {
- archivos: {
- Row: {
- bytes: number | null
- id: string
- mime_type: string | null
- nombre: string
- notas: string | null
- openai_file_id: string | null
- ruta_storage: string
- subido_en: string
- subido_por: string | null
- temporal: boolean
- }
- Insert: {
- bytes?: number | null
- id?: string
- mime_type?: string | null
- nombre: string
- notas?: string | null
- openai_file_id?: string | null
- ruta_storage: string
- subido_en?: string
- subido_por?: string | null
- temporal?: boolean
- }
- Update: {
- bytes?: number | null
- id?: string
- mime_type?: string | null
- nombre?: string
- notas?: string | null
- openai_file_id?: string | null
- ruta_storage?: string
- subido_en?: string
- subido_por?: string | null
- temporal?: boolean
- }
- Relationships: [
- {
- foreignKeyName: "archivos_subido_por_fkey"
- columns: ["subido_por"]
- isOneToOne: false
- referencedRelation: "usuarios_app"
- referencedColumns: ["id"]
- },
- ]
- }
- asignaturas: {
- Row: {
- actualizado_en: string
- actualizado_por: string | null
- asignatura_hash: string | null
- codigo: string | null
- contenido_tematico: Json
- creado_en: string
- creado_por: string | null
- creditos: number
- datos: Json
- estado: Database["public"]["Enums"]["estado_asignatura"]
- estructura_id: string | null
- horas_academicas: number | null
- horas_independientes: number | null
- id: string
- linea_plan_id: string | null
- meta_origen: Json
- nombre: string
- numero_ciclo: number | null
- orden_celda: number | null
- plan_estudio_id: string
- tipo: Database["public"]["Enums"]["tipo_asignatura"]
- tipo_origen: Database["public"]["Enums"]["tipo_origen"] | null
- }
- Insert: {
- actualizado_en?: string
- actualizado_por?: string | null
- asignatura_hash?: string | null
- codigo?: string | null
- contenido_tematico?: Json
- creado_en?: string
- creado_por?: string | null
- creditos: number
- datos?: Json
- estado?: Database["public"]["Enums"]["estado_asignatura"]
- estructura_id?: string | null
- horas_academicas?: number | null
- horas_independientes?: number | null
- id?: string
- linea_plan_id?: string | null
- meta_origen?: Json
- nombre: string
- numero_ciclo?: number | null
- orden_celda?: number | null
- plan_estudio_id: string
- tipo?: Database["public"]["Enums"]["tipo_asignatura"]
- tipo_origen?: Database["public"]["Enums"]["tipo_origen"] | null
- }
- Update: {
- actualizado_en?: string
- actualizado_por?: string | null
- asignatura_hash?: string | null
- codigo?: string | null
- contenido_tematico?: Json
- creado_en?: string
- creado_por?: string | null
- creditos?: number
- datos?: Json
- estado?: Database["public"]["Enums"]["estado_asignatura"]
- estructura_id?: string | null
- horas_academicas?: number | null
- horas_independientes?: number | null
- id?: string
- linea_plan_id?: string | null
- meta_origen?: Json
- nombre?: string
- numero_ciclo?: number | null
- orden_celda?: number | null
- plan_estudio_id?: string
- tipo?: Database["public"]["Enums"]["tipo_asignatura"]
- tipo_origen?: Database["public"]["Enums"]["tipo_origen"] | null
- }
- Relationships: [
- {
- foreignKeyName: "asignaturas_actualizado_por_fkey"
- columns: ["actualizado_por"]
- isOneToOne: false
- referencedRelation: "usuarios_app"
- referencedColumns: ["id"]
- },
- {
- foreignKeyName: "asignaturas_creado_por_fkey"
- columns: ["creado_por"]
- isOneToOne: false
- referencedRelation: "usuarios_app"
- referencedColumns: ["id"]
- },
- {
- foreignKeyName: "asignaturas_estructura_id_fkey"
- columns: ["estructura_id"]
- isOneToOne: false
- referencedRelation: "estructuras_asignatura"
- referencedColumns: ["id"]
- },
- {
- foreignKeyName: "asignaturas_linea_plan_fk_compuesta"
- columns: ["linea_plan_id", "plan_estudio_id"]
- isOneToOne: false
- referencedRelation: "lineas_plan"
- referencedColumns: ["id", "plan_estudio_id"]
- },
- {
- foreignKeyName: "asignaturas_plan_estudio_id_fkey"
- columns: ["plan_estudio_id"]
- isOneToOne: false
- referencedRelation: "planes_estudio"
- referencedColumns: ["id"]
- },
- {
- foreignKeyName: "asignaturas_plan_estudio_id_fkey"
- columns: ["plan_estudio_id"]
- isOneToOne: false
- referencedRelation: "plantilla_plan"
- referencedColumns: ["plan_estudio_id"]
- },
- ]
- }
- bibliografia_asignatura: {
- Row: {
- actualizado_en: string
- asignatura_id: string
- biblioteca_item_id: string | null
- cita: string
- creado_en: string
- creado_por: string | null
- id: string
- tipo: Database["public"]["Enums"]["tipo_bibliografia"]
- tipo_fuente: Database["public"]["Enums"]["tipo_fuente_bibliografia"]
- }
- Insert: {
- actualizado_en?: string
- asignatura_id: string
- biblioteca_item_id?: string | null
- cita: string
- creado_en?: string
- creado_por?: string | null
- id?: string
- tipo: Database["public"]["Enums"]["tipo_bibliografia"]
- tipo_fuente?: Database["public"]["Enums"]["tipo_fuente_bibliografia"]
- }
- Update: {
- actualizado_en?: string
- asignatura_id?: string
- biblioteca_item_id?: string | null
- cita?: string
- creado_en?: string
- creado_por?: string | null
- id?: string
- tipo?: Database["public"]["Enums"]["tipo_bibliografia"]
- tipo_fuente?: Database["public"]["Enums"]["tipo_fuente_bibliografia"]
- }
- Relationships: [
- {
- foreignKeyName: "bibliografia_asignatura_asignatura_id_fkey"
- columns: ["asignatura_id"]
- isOneToOne: false
- referencedRelation: "asignaturas"
- referencedColumns: ["id"]
- },
- {
- foreignKeyName: "bibliografia_asignatura_creado_por_fkey"
- columns: ["creado_por"]
- isOneToOne: false
- referencedRelation: "usuarios_app"
- referencedColumns: ["id"]
- },
- ]
- }
- cambios_asignatura: {
- Row: {
- asignatura_id: string
- cambiado_en: string
- cambiado_por: string | null
- campo: string | null
- fuente: Database["public"]["Enums"]["fuente_cambio"] | null
- id: string
- interaccion_ia_id: string | null
- tipo: Database["public"]["Enums"]["tipo_cambio"]
- valor_anterior: Json | null
- valor_nuevo: Json | null
- }
- Insert: {
- asignatura_id: string
- cambiado_en?: string
- cambiado_por?: string | null
- campo?: string | null
- fuente?: Database["public"]["Enums"]["fuente_cambio"] | null
- id?: string
- interaccion_ia_id?: string | null
- tipo: Database["public"]["Enums"]["tipo_cambio"]
- valor_anterior?: Json | null
- valor_nuevo?: Json | null
- }
- Update: {
- asignatura_id?: string
- cambiado_en?: string
- cambiado_por?: string | null
- campo?: string | null
- fuente?: Database["public"]["Enums"]["fuente_cambio"] | null
- id?: string
- interaccion_ia_id?: string | null
- tipo?: Database["public"]["Enums"]["tipo_cambio"]
- valor_anterior?: Json | null
- valor_nuevo?: Json | null
- }
- Relationships: [
- {
- foreignKeyName: "cambios_asignatura_asignatura_id_fkey"
- columns: ["asignatura_id"]
- isOneToOne: false
- referencedRelation: "asignaturas"
- referencedColumns: ["id"]
- },
- {
- foreignKeyName: "cambios_asignatura_cambiado_por_fkey"
- columns: ["cambiado_por"]
- isOneToOne: false
- referencedRelation: "usuarios_app"
- referencedColumns: ["id"]
- },
- ]
- }
- cambios_plan: {
- Row: {
- cambiado_en: string
- cambiado_por: string | null
- campo: string | null
- id: string
- plan_estudio_id: string
- response_id: string | null
- tipo: Database["public"]["Enums"]["tipo_cambio"]
- valor_anterior: Json | null
- valor_nuevo: Json | null
- }
- Insert: {
- cambiado_en?: string
- cambiado_por?: string | null
- campo?: string | null
- id?: string
- plan_estudio_id: string
- response_id?: string | null
- tipo: Database["public"]["Enums"]["tipo_cambio"]
- valor_anterior?: Json | null
- valor_nuevo?: Json | null
- }
- Update: {
- cambiado_en?: string
- cambiado_por?: string | null
- campo?: string | null
- id?: string
- plan_estudio_id?: string
- response_id?: string | null
- tipo?: Database["public"]["Enums"]["tipo_cambio"]
- valor_anterior?: Json | null
- valor_nuevo?: Json | null
- }
- Relationships: [
- {
- foreignKeyName: "cambios_plan_cambiado_por_fkey"
- columns: ["cambiado_por"]
- isOneToOne: false
- referencedRelation: "usuarios_app"
- referencedColumns: ["id"]
- },
- ]
- }
- carreras: {
- Row: {
- activa: boolean
- actualizado_en: string
- clave_sep: string | null
- creado_en: string
- facultad_id: string
- id: string
- nombre: string
- nombre_corto: string | null
- }
- Insert: {
- activa?: boolean
- actualizado_en?: string
- clave_sep?: string | null
- creado_en?: string
- facultad_id: string
- id?: string
- nombre: string
- nombre_corto?: string | null
- }
- Update: {
- activa?: boolean
- actualizado_en?: string
- clave_sep?: string | null
- creado_en?: string
- facultad_id?: string
- id?: string
- nombre?: string
- nombre_corto?: string | null
- }
- Relationships: [
- {
- foreignKeyName: "carreras_facultad_id_fkey"
- columns: ["facultad_id"]
- isOneToOne: false
- referencedRelation: "facultades"
- referencedColumns: ["id"]
- },
- ]
- }
- conversaciones_asignatura: {
- Row: {
- archivado_en: string | null
- archivado_por: string | null
- asignatura_id: string
- conversacion_json: Json
- creado_en: string
- creado_por: string | null
- estado: Database["public"]["Enums"]["estado_conversacion"]
- id: string
- intento_archivado: number
- openai_conversation_id: string
- }
- Insert: {
- archivado_en?: string | null
- archivado_por?: string | null
- asignatura_id: string
- conversacion_json?: Json
- creado_en?: string
- creado_por?: string | null
- estado?: Database["public"]["Enums"]["estado_conversacion"]
- id?: string
- intento_archivado?: number
- openai_conversation_id: string
- }
- Update: {
- archivado_en?: string | null
- archivado_por?: string | null
- asignatura_id?: string
- conversacion_json?: Json
- creado_en?: string
- creado_por?: string | null
- estado?: Database["public"]["Enums"]["estado_conversacion"]
- id?: string
- intento_archivado?: number
- openai_conversation_id?: string
- }
- Relationships: [
- {
- foreignKeyName: "conversaciones_asignatura_archivado_por_fkey"
- columns: ["archivado_por"]
- isOneToOne: false
- referencedRelation: "usuarios_app"
- referencedColumns: ["id"]
- },
- {
- foreignKeyName: "conversaciones_asignatura_asignatura_id_fkey"
- columns: ["asignatura_id"]
- isOneToOne: false
- referencedRelation: "asignaturas"
- referencedColumns: ["id"]
- },
- {
- foreignKeyName: "conversaciones_asignatura_creado_por_fkey"
- columns: ["creado_por"]
- isOneToOne: false
- referencedRelation: "usuarios_app"
- referencedColumns: ["id"]
- },
- ]
- }
- conversaciones_plan: {
- Row: {
- archivado_en: string | null
- archivado_por: string | null
- conversacion_json: Json
- creado_en: string
- creado_por: string | null
- estado: Database["public"]["Enums"]["estado_conversacion"]
- id: string
- intento_archivado: number
- nombre: string | null
- openai_conversation_id: string
- plan_estudio_id: string
- }
- Insert: {
- archivado_en?: string | null
- archivado_por?: string | null
- conversacion_json?: Json
- creado_en?: string
- creado_por?: string | null
- estado?: Database["public"]["Enums"]["estado_conversacion"]
- id?: string
- intento_archivado?: number
- nombre?: string | null
- openai_conversation_id: string
- plan_estudio_id: string
- }
- Update: {
- archivado_en?: string | null
- archivado_por?: string | null
- conversacion_json?: Json
- creado_en?: string
- creado_por?: string | null
- estado?: Database["public"]["Enums"]["estado_conversacion"]
- id?: string
- intento_archivado?: number
- nombre?: string | null
- openai_conversation_id?: string
- plan_estudio_id?: string
- }
- Relationships: [
- {
- foreignKeyName: "conversaciones_plan_archivado_por_fkey"
- columns: ["archivado_por"]
- isOneToOne: false
- referencedRelation: "usuarios_app"
- referencedColumns: ["id"]
- },
- {
- foreignKeyName: "conversaciones_plan_creado_por_fkey"
- columns: ["creado_por"]
- isOneToOne: false
- referencedRelation: "usuarios_app"
- referencedColumns: ["id"]
- },
- {
- foreignKeyName: "conversaciones_plan_plan_estudio_id_fkey"
- columns: ["plan_estudio_id"]
- isOneToOne: false
- referencedRelation: "planes_estudio"
- referencedColumns: ["id"]
- },
- {
- foreignKeyName: "conversaciones_plan_plan_estudio_id_fkey"
- columns: ["plan_estudio_id"]
- isOneToOne: false
- referencedRelation: "plantilla_plan"
- referencedColumns: ["plan_estudio_id"]
- },
- ]
- }
- estados_plan: {
- Row: {
- clave: string
- es_final: boolean
- etiqueta: string
- id: string
- orden: number
- }
- Insert: {
- clave: string
- es_final?: boolean
- etiqueta: string
- id?: string
- orden?: number
- }
- Update: {
- clave?: string
- es_final?: boolean
- etiqueta?: string
- id?: string
- orden?: number
- }
- Relationships: []
- }
- estructuras_asignatura: {
- Row: {
- actualizado_en: string
- creado_en: string
- definicion: Json
- id: string
- nombre: string
- version: string | null
- }
- Insert: {
- actualizado_en?: string
- creado_en?: string
- definicion?: Json
- id?: string
- nombre: string
- version?: string | null
- }
- Update: {
- actualizado_en?: string
- creado_en?: string
- definicion?: Json
- id?: string
- nombre?: string
- version?: string | null
- }
- Relationships: []
- }
- estructuras_plan: {
- Row: {
- actualizado_en: string
- creado_en: string
- definicion: Json
- id: string
- nombre: string
- template_id: string | null
- tipo: Database["public"]["Enums"]["tipo_estructura_plan"]
- }
- Insert: {
- actualizado_en?: string
- creado_en?: string
- definicion?: Json
- id?: string
- nombre: string
- template_id?: string | null
- tipo: Database["public"]["Enums"]["tipo_estructura_plan"]
- }
- Update: {
- actualizado_en?: string
- creado_en?: string
- definicion?: Json
- id?: string
- nombre?: string
- template_id?: string | null
- tipo?: Database["public"]["Enums"]["tipo_estructura_plan"]
- }
- Relationships: []
- }
- facultades: {
- Row: {
- actualizado_en: string
- color: string | null
- creado_en: string
- icono: string | null
- id: string
- nombre: string
- nombre_corto: string | null
- }
- Insert: {
- actualizado_en?: string
- color?: string | null
- creado_en?: string
- icono?: string | null
- id?: string
- nombre: string
- nombre_corto?: string | null
- }
- Update: {
- actualizado_en?: string
- color?: string | null
- creado_en?: string
- icono?: string | null
- id?: string
- nombre?: string
- nombre_corto?: string | null
- }
- Relationships: []
- }
- interacciones_ia: {
- Row: {
- aceptada: boolean
- asignatura_id: string | null
- conversacion_id: string | null
- creado_en: string
- id: string
- ids_archivos: Json
- ids_vector_store: Json
- modelo: string | null
- plan_estudio_id: string | null
- prompt: Json
- respuesta: Json
- rutas_storage: Json
- temperatura: number | null
- tipo: Database["public"]["Enums"]["tipo_interaccion_ia"]
- usuario_id: string | null
- }
- Insert: {
- aceptada?: boolean
- asignatura_id?: string | null
- conversacion_id?: string | null
- creado_en?: string
- id?: string
- ids_archivos?: Json
- ids_vector_store?: Json
- modelo?: string | null
- plan_estudio_id?: string | null
- prompt?: Json
- respuesta?: Json
- rutas_storage?: Json
- temperatura?: number | null
- tipo: Database["public"]["Enums"]["tipo_interaccion_ia"]
- usuario_id?: string | null
- }
- Update: {
- aceptada?: boolean
- asignatura_id?: string | null
- conversacion_id?: string | null
- creado_en?: string
- id?: string
- ids_archivos?: Json
- ids_vector_store?: Json
- modelo?: string | null
- plan_estudio_id?: string | null
- prompt?: Json
- respuesta?: Json
- rutas_storage?: Json
- temperatura?: number | null
- tipo?: Database["public"]["Enums"]["tipo_interaccion_ia"]
- usuario_id?: string | null
- }
- Relationships: [
- {
- foreignKeyName: "interacciones_ia_asignatura_id_fkey"
- columns: ["asignatura_id"]
- isOneToOne: false
- referencedRelation: "asignaturas"
- referencedColumns: ["id"]
- },
- {
- foreignKeyName: "interacciones_ia_plan_estudio_id_fkey"
- columns: ["plan_estudio_id"]
- isOneToOne: false
- referencedRelation: "planes_estudio"
- referencedColumns: ["id"]
- },
- {
- foreignKeyName: "interacciones_ia_plan_estudio_id_fkey"
- columns: ["plan_estudio_id"]
- isOneToOne: false
- referencedRelation: "plantilla_plan"
- referencedColumns: ["plan_estudio_id"]
- },
- {
- foreignKeyName: "interacciones_ia_usuario_id_fkey"
- columns: ["usuario_id"]
- isOneToOne: false
- referencedRelation: "usuarios_app"
- referencedColumns: ["id"]
- },
- ]
- }
- lineas_plan: {
- Row: {
- actualizado_en: string
- area: string | null
- creado_en: string
- id: string
- nombre: string
- orden: number
- plan_estudio_id: string
- }
- Insert: {
- actualizado_en?: string
- area?: string | null
- creado_en?: string
- id?: string
- nombre: string
- orden?: number
- plan_estudio_id: string
- }
- Update: {
- actualizado_en?: string
- area?: string | null
- creado_en?: string
- id?: string
- nombre?: string
- orden?: number
- plan_estudio_id?: string
- }
- Relationships: [
- {
- foreignKeyName: "lineas_plan_plan_estudio_id_fkey"
- columns: ["plan_estudio_id"]
- isOneToOne: false
- referencedRelation: "planes_estudio"
- referencedColumns: ["id"]
- },
- {
- foreignKeyName: "lineas_plan_plan_estudio_id_fkey"
- columns: ["plan_estudio_id"]
- isOneToOne: false
- referencedRelation: "plantilla_plan"
- referencedColumns: ["plan_estudio_id"]
- },
- ]
- }
- notificaciones: {
- Row: {
- creado_en: string
- id: string
- leida: boolean
- leida_en: string | null
- payload: Json
- tipo: Database["public"]["Enums"]["tipo_notificacion"]
- usuario_id: string
- }
- Insert: {
- creado_en?: string
- id?: string
- leida?: boolean
- leida_en?: string | null
- payload?: Json
- tipo: Database["public"]["Enums"]["tipo_notificacion"]
- usuario_id: string
- }
- Update: {
- creado_en?: string
- id?: string
- leida?: boolean
- leida_en?: string | null
- payload?: Json
- tipo?: Database["public"]["Enums"]["tipo_notificacion"]
- usuario_id?: string
- }
- Relationships: [
- {
- foreignKeyName: "notificaciones_usuario_id_fkey"
- columns: ["usuario_id"]
- isOneToOne: false
- referencedRelation: "usuarios_app"
- referencedColumns: ["id"]
- },
- ]
- }
- planes_estudio: {
- Row: {
- activo: boolean
- actualizado_en: string
- actualizado_por: string | null
- carrera_id: string
- creado_en: string
- creado_por: string | null
- datos: Json
- estado_actual_id: string | null
- estructura_id: string
- id: string
- meta_origen: Json
- nivel: Database["public"]["Enums"]["nivel_plan_estudio"]
- nombre: string
- nombre_search: string | null
- numero_ciclos: number
- plan_hash: string | null
- tipo_ciclo: Database["public"]["Enums"]["tipo_ciclo"]
- tipo_origen: Database["public"]["Enums"]["tipo_origen"] | null
- }
- Insert: {
- activo?: boolean
- actualizado_en?: string
- actualizado_por?: string | null
- carrera_id: string
- creado_en?: string
- creado_por?: string | null
- datos?: Json
- estado_actual_id?: string | null
- estructura_id: string
- id?: string
- meta_origen?: Json
- nivel: Database["public"]["Enums"]["nivel_plan_estudio"]
- nombre: string
- nombre_search?: string | null
- numero_ciclos: number
- plan_hash?: string | null
- tipo_ciclo: Database["public"]["Enums"]["tipo_ciclo"]
- tipo_origen?: Database["public"]["Enums"]["tipo_origen"] | null
- }
- Update: {
- activo?: boolean
- actualizado_en?: string
- actualizado_por?: string | null
- carrera_id?: string
- creado_en?: string
- creado_por?: string | null
- datos?: Json
- estado_actual_id?: string | null
- estructura_id?: string
- id?: string
- meta_origen?: Json
- nivel?: Database["public"]["Enums"]["nivel_plan_estudio"]
- nombre?: string
- nombre_search?: string | null
- numero_ciclos?: number
- plan_hash?: string | null
- tipo_ciclo?: Database["public"]["Enums"]["tipo_ciclo"]
- tipo_origen?: Database["public"]["Enums"]["tipo_origen"] | null
- }
- Relationships: [
- {
- foreignKeyName: "planes_estudio_actualizado_por_fkey"
- columns: ["actualizado_por"]
- isOneToOne: false
- referencedRelation: "usuarios_app"
- referencedColumns: ["id"]
- },
- {
- foreignKeyName: "planes_estudio_carrera_id_fkey"
- columns: ["carrera_id"]
- isOneToOne: false
- referencedRelation: "carreras"
- referencedColumns: ["id"]
- },
- {
- foreignKeyName: "planes_estudio_creado_por_fkey"
- columns: ["creado_por"]
- isOneToOne: false
- referencedRelation: "usuarios_app"
- referencedColumns: ["id"]
- },
- {
- foreignKeyName: "planes_estudio_estado_actual_id_fkey"
- columns: ["estado_actual_id"]
- isOneToOne: false
- referencedRelation: "estados_plan"
- referencedColumns: ["id"]
- },
- {
- foreignKeyName: "planes_estudio_estructura_id_fkey"
- columns: ["estructura_id"]
- isOneToOne: false
- referencedRelation: "estructuras_plan"
- referencedColumns: ["id"]
- },
- {
- foreignKeyName: "planes_estudio_estructura_id_fkey"
- columns: ["estructura_id"]
- isOneToOne: false
- referencedRelation: "plantilla_plan"
- referencedColumns: ["estructura_id"]
- },
- ]
- }
- responsables_asignatura: {
- Row: {
- asignatura_id: string
- creado_en: string
- id: string
- rol: Database["public"]["Enums"]["rol_responsable_asignatura"]
- usuario_id: string
- }
- Insert: {
- asignatura_id: string
- creado_en?: string
- id?: string
- rol?: Database["public"]["Enums"]["rol_responsable_asignatura"]
- usuario_id: string
- }
- Update: {
- asignatura_id?: string
- creado_en?: string
- id?: string
- rol?: Database["public"]["Enums"]["rol_responsable_asignatura"]
- usuario_id?: string
- }
- Relationships: [
- {
- foreignKeyName: "responsables_asignatura_asignatura_id_fkey"
- columns: ["asignatura_id"]
- isOneToOne: false
- referencedRelation: "asignaturas"
- referencedColumns: ["id"]
- },
- {
- foreignKeyName: "responsables_asignatura_usuario_id_fkey"
- columns: ["usuario_id"]
- isOneToOne: false
- referencedRelation: "usuarios_app"
- referencedColumns: ["id"]
- },
- ]
- }
- roles: {
- Row: {
- clave: string
- descripcion: string | null
- id: string
- nombre: string
- }
- Insert: {
- clave: string
- descripcion?: string | null
- id?: string
- nombre: string
- }
- Update: {
- clave?: string
- descripcion?: string | null
- id?: string
- nombre?: string
- }
- Relationships: []
- }
- tareas_revision: {
- Row: {
- asignado_a: string
- completado_en: string | null
- creado_en: string
- estado_id: string | null
- estatus: Database["public"]["Enums"]["estado_tarea_revision"]
- fecha_limite: string | null
- id: string
- plan_estudio_id: string
- rol_id: string | null
- }
- Insert: {
- asignado_a: string
- completado_en?: string | null
- creado_en?: string
- estado_id?: string | null
- estatus?: Database["public"]["Enums"]["estado_tarea_revision"]
- fecha_limite?: string | null
- id?: string
- plan_estudio_id: string
- rol_id?: string | null
- }
- Update: {
- asignado_a?: string
- completado_en?: string | null
- creado_en?: string
- estado_id?: string | null
- estatus?: Database["public"]["Enums"]["estado_tarea_revision"]
- fecha_limite?: string | null
- id?: string
- plan_estudio_id?: string
- rol_id?: string | null
- }
- Relationships: [
- {
- foreignKeyName: "tareas_revision_asignado_a_fkey"
- columns: ["asignado_a"]
- isOneToOne: false
- referencedRelation: "usuarios_app"
- referencedColumns: ["id"]
- },
- {
- foreignKeyName: "tareas_revision_estado_id_fkey"
- columns: ["estado_id"]
- isOneToOne: false
- referencedRelation: "estados_plan"
- referencedColumns: ["id"]
- },
- {
- foreignKeyName: "tareas_revision_plan_estudio_id_fkey"
- columns: ["plan_estudio_id"]
- isOneToOne: false
- referencedRelation: "planes_estudio"
- referencedColumns: ["id"]
- },
- {
- foreignKeyName: "tareas_revision_plan_estudio_id_fkey"
- columns: ["plan_estudio_id"]
- isOneToOne: false
- referencedRelation: "plantilla_plan"
- referencedColumns: ["plan_estudio_id"]
- },
- {
- foreignKeyName: "tareas_revision_rol_id_fkey"
- columns: ["rol_id"]
- isOneToOne: false
- referencedRelation: "roles"
- referencedColumns: ["id"]
- },
- ]
- }
- transiciones_estado_plan: {
- Row: {
- creado_en: string
- desde_estado_id: string
- hacia_estado_id: string
- id: string
- rol_permitido_id: string
- }
- Insert: {
- creado_en?: string
- desde_estado_id: string
- hacia_estado_id: string
- id?: string
- rol_permitido_id: string
- }
- Update: {
- creado_en?: string
- desde_estado_id?: string
- hacia_estado_id?: string
- id?: string
- rol_permitido_id?: string
- }
- Relationships: [
- {
- foreignKeyName: "transiciones_estado_plan_desde_estado_id_fkey"
- columns: ["desde_estado_id"]
- isOneToOne: false
- referencedRelation: "estados_plan"
- referencedColumns: ["id"]
- },
- {
- foreignKeyName: "transiciones_estado_plan_hacia_estado_id_fkey"
- columns: ["hacia_estado_id"]
- isOneToOne: false
- referencedRelation: "estados_plan"
- referencedColumns: ["id"]
- },
- {
- foreignKeyName: "transiciones_estado_plan_rol_permitido_id_fkey"
- columns: ["rol_permitido_id"]
- isOneToOne: false
- referencedRelation: "roles"
- referencedColumns: ["id"]
- },
- ]
- }
- usuarios_app: {
- Row: {
- actualizado_en: string
- creado_en: string
- email: string | null
- externo: boolean
- id: string
- nombre_completo: string | null
- }
- Insert: {
- actualizado_en?: string
- creado_en?: string
- email?: string | null
- externo?: boolean
- id: string
- nombre_completo?: string | null
- }
- Update: {
- actualizado_en?: string
- creado_en?: string
- email?: string | null
- externo?: boolean
- id?: string
- nombre_completo?: string | null
- }
- Relationships: []
- }
- usuarios_roles: {
- Row: {
- carrera_id: string | null
- creado_en: string
- facultad_id: string | null
- id: string
- rol_id: string
- usuario_id: string
- }
- Insert: {
- carrera_id?: string | null
- creado_en?: string
- facultad_id?: string | null
- id?: string
- rol_id: string
- usuario_id: string
- }
- Update: {
- carrera_id?: string | null
- creado_en?: string
- facultad_id?: string | null
- id?: string
- rol_id?: string
- usuario_id?: string
- }
- Relationships: [
- {
- foreignKeyName: "usuarios_roles_carrera_id_fkey"
- columns: ["carrera_id"]
- isOneToOne: false
- referencedRelation: "carreras"
- referencedColumns: ["id"]
- },
- {
- foreignKeyName: "usuarios_roles_facultad_id_fkey"
- columns: ["facultad_id"]
- isOneToOne: false
- referencedRelation: "facultades"
- referencedColumns: ["id"]
- },
- {
- foreignKeyName: "usuarios_roles_rol_id_fkey"
- columns: ["rol_id"]
- isOneToOne: false
- referencedRelation: "roles"
- referencedColumns: ["id"]
- },
- {
- foreignKeyName: "usuarios_roles_usuario_id_fkey"
- columns: ["usuario_id"]
- isOneToOne: false
- referencedRelation: "usuarios_app"
- referencedColumns: ["id"]
- },
- ]
- }
- vector_stores: {
- Row: {
- creado_en: string
- creado_por: string | null
- id: string
- nombre: string
- openai_vector_id: string | null
- }
- Insert: {
- creado_en?: string
- creado_por?: string | null
- id?: string
- nombre: string
- openai_vector_id?: string | null
- }
- Update: {
- creado_en?: string
- creado_por?: string | null
- id?: string
- nombre?: string
- openai_vector_id?: string | null
- }
- Relationships: [
- {
- foreignKeyName: "vector_stores_creado_por_fkey"
- columns: ["creado_por"]
- isOneToOne: false
- referencedRelation: "usuarios_app"
- referencedColumns: ["id"]
- },
- ]
- }
- }
- Views: {
- plantilla_plan: {
- Row: {
- estructura_id: string | null
- plan_estudio_id: string | null
- template_id: string | null
- }
- Relationships: []
- }
- }
- Functions: {
- unaccent: { Args: { "": string }; Returns: string }
- unaccent_immutable: { Args: { "": string }; Returns: string }
- }
- Enums: {
- estado_asignatura: "borrador" | "revisada" | "aprobada" | "generando"
- estado_conversacion: "ACTIVA" | "ARCHIVANDO" | "ARCHIVADA" | "ERROR"
- estado_tarea_revision: "PENDIENTE" | "COMPLETADA" | "OMITIDA"
- fuente_cambio: "HUMANO" | "IA"
- nivel_plan_estudio:
- | "Licenciatura"
- | "Maestría"
- | "Doctorado"
- | "Especialidad"
- | "Diplomado"
- | "Otro"
- puesto_tipo:
- | "vicerrector"
- | "director_facultad"
- | "secretario_academico"
- | "jefe_carrera"
- | "profesor"
- | "lci"
- rol_responsable_asignatura: "PROFESOR_RESPONSABLE" | "COAUTOR" | "REVISOR"
- tipo_asignatura: "OBLIGATORIA" | "OPTATIVA" | "TRONCAL" | "OTRA"
- tipo_bibliografia: "BASICA" | "COMPLEMENTARIA"
- tipo_cambio:
- | "ACTUALIZACION_CAMPO"
- | "ACTUALIZACION_MAPA"
- | "TRANSICION_ESTADO"
- | "OTRO"
- | "CREACION"
- | "ACTUALIZACION"
- tipo_ciclo: "Semestre" | "Cuatrimestre" | "Trimestre" | "Otro"
- tipo_estructura_plan: "CURRICULAR" | "NO_CURRICULAR"
- tipo_fuente_bibliografia: "MANUAL" | "BIBLIOTECA"
- tipo_interaccion_ia: "GENERAR" | "MEJORAR_SECCION" | "CHAT" | "OTRA"
- tipo_notificacion:
- | "PLAN_ASIGNADO"
- | "ESTADO_CAMBIADO"
- | "TAREA_ASIGNADA"
- | "COMENTARIO"
- | "OTRA"
- tipo_origen:
- | "MANUAL"
- | "IA"
- | "CLONADO_INTERNO"
- | "CLONADO_TRADICIONAL"
- | "OTRO"
- }
- CompositeTypes: {
- [_ in never]: never
- }
- }
-}
-
-type DatabaseWithoutInternals = Omit
-
-type DefaultSchema = DatabaseWithoutInternals[Extract]
-
-export type Tables<
- DefaultSchemaTableNameOrOptions extends
- | keyof (DefaultSchema["Tables"] & DefaultSchema["Views"])
- | { schema: keyof DatabaseWithoutInternals },
- TableName extends DefaultSchemaTableNameOrOptions extends {
- schema: keyof DatabaseWithoutInternals
- }
- ? keyof (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
- DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])
- : never = never,
-> = DefaultSchemaTableNameOrOptions extends {
- schema: keyof DatabaseWithoutInternals
-}
- ? (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
- DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends {
- Row: infer R
- }
- ? R
- : never
- : DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema["Tables"] &
- DefaultSchema["Views"])
- ? (DefaultSchema["Tables"] &
- DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends {
- Row: infer R
- }
- ? R
- : never
- : never
-
-export type TablesInsert<
- DefaultSchemaTableNameOrOptions extends
- | keyof DefaultSchema["Tables"]
- | { schema: keyof DatabaseWithoutInternals },
- TableName extends DefaultSchemaTableNameOrOptions extends {
- schema: keyof DatabaseWithoutInternals
- }
- ? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
- : never = never,
-> = DefaultSchemaTableNameOrOptions extends {
- schema: keyof DatabaseWithoutInternals
-}
- ? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
- Insert: infer I
- }
- ? I
- : never
- : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
- ? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
- Insert: infer I
- }
- ? I
- : never
- : never
-
-export type TablesUpdate<
- DefaultSchemaTableNameOrOptions extends
- | keyof DefaultSchema["Tables"]
- | { schema: keyof DatabaseWithoutInternals },
- TableName extends DefaultSchemaTableNameOrOptions extends {
- schema: keyof DatabaseWithoutInternals
- }
- ? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
- : never = never,
-> = DefaultSchemaTableNameOrOptions extends {
- schema: keyof DatabaseWithoutInternals
-}
- ? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
- Update: infer U
- }
- ? U
- : never
- : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
- ? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
- Update: infer U
- }
- ? U
- : never
- : never
-
-export type Enums<
- DefaultSchemaEnumNameOrOptions extends
- | keyof DefaultSchema["Enums"]
- | { schema: keyof DatabaseWithoutInternals },
- EnumName extends DefaultSchemaEnumNameOrOptions extends {
- schema: keyof DatabaseWithoutInternals
- }
- ? keyof DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"]
- : never = never,
-> = DefaultSchemaEnumNameOrOptions extends {
- schema: keyof DatabaseWithoutInternals
-}
- ? DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName]
- : DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"]
- ? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions]
- : never
-
-export type CompositeTypes<
- PublicCompositeTypeNameOrOptions extends
- | keyof DefaultSchema["CompositeTypes"]
- | { schema: keyof DatabaseWithoutInternals },
- CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
- schema: keyof DatabaseWithoutInternals
- }
- ? keyof DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
- : never = never,
-> = PublicCompositeTypeNameOrOptions extends {
- schema: keyof DatabaseWithoutInternals
-}
- ? DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
- : PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"]
- ? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
- : never
-
-export const Constants = {
- graphql_public: {
- Enums: {},
- },
- public: {
- Enums: {
- estado_asignatura: ["borrador", "revisada", "aprobada", "generando"],
- estado_conversacion: ["ACTIVA", "ARCHIVANDO", "ARCHIVADA", "ERROR"],
- estado_tarea_revision: ["PENDIENTE", "COMPLETADA", "OMITIDA"],
- fuente_cambio: ["HUMANO", "IA"],
- nivel_plan_estudio: [
- "Licenciatura",
- "Maestría",
- "Doctorado",
- "Especialidad",
- "Diplomado",
- "Otro",
- ],
- puesto_tipo: [
- "vicerrector",
- "director_facultad",
- "secretario_academico",
- "jefe_carrera",
- "profesor",
- "lci",
- ],
- rol_responsable_asignatura: [
- "PROFESOR_RESPONSABLE",
- "COAUTOR",
- "REVISOR",
- ],
- tipo_asignatura: ["OBLIGATORIA", "OPTATIVA", "TRONCAL", "OTRA"],
- tipo_bibliografia: ["BASICA", "COMPLEMENTARIA"],
- tipo_cambio: [
- "ACTUALIZACION_CAMPO",
- "ACTUALIZACION_MAPA",
- "TRANSICION_ESTADO",
- "OTRO",
- "CREACION",
- "ACTUALIZACION",
- ],
- tipo_ciclo: ["Semestre", "Cuatrimestre", "Trimestre", "Otro"],
- tipo_estructura_plan: ["CURRICULAR", "NO_CURRICULAR"],
- tipo_fuente_bibliografia: ["MANUAL", "BIBLIOTECA"],
- tipo_interaccion_ia: ["GENERAR", "MEJORAR_SECCION", "CHAT", "OTRA"],
- tipo_notificacion: [
- "PLAN_ASIGNADO",
- "ESTADO_CAMBIADO",
- "TAREA_ASIGNADA",
- "COMENTARIO",
- "OTRA",
- ],
- tipo_origen: [
- "MANUAL",
- "IA",
- "CLONADO_INTERNO",
- "CLONADO_TRADICIONAL",
- "OTRO",
- ],
- },
- },
-} as const
-
diff --git a/supabase/functions/_shared/openai-service.ts b/supabase/functions/_shared/openai-service.ts
index 733bafd..5ebc4f6 100644
--- a/supabase/functions/_shared/openai-service.ts
+++ b/supabase/functions/_shared/openai-service.ts
@@ -2,7 +2,6 @@
///
import OpenAI from "npm:openai@6.16.0";
import type * as OpenAITypes from "npm:openai@6.16.0";
-import { createClient, type SupabaseClient } from "npm:@supabase/supabase-js@2";
// Use non-streaming params to ensure `responses.create` returns a typed Response
export type StructuredResponseOptions =
OpenAITypes.OpenAI.Responses.ResponseCreateParamsNonStreaming;
@@ -15,7 +14,6 @@ export type StructuredResponseSuccess = {
responseId: string;
conversationId?: string | null;
references: {
- uploadedToStorage: string[]; // supabase storage paths
openaiFileIds: string[]; // file ids in OpenAI
};
openaiRaw: OpenAITypes.OpenAI.Responses.Response; // keep for advanced consumers
@@ -24,7 +22,6 @@ export type StructuredResponseFailure = {
ok: false;
code:
| "MissingEnv"
- | "StorageUploadFailed"
| "OpenAIFileUploadFailed"
| "OpenAIRequestFailed";
message: string;
@@ -34,40 +31,24 @@ export type StructuredResponseResult =
| StructuredResponseSuccess
| StructuredResponseFailure;
export interface OpenAIServiceConfig {
- supabaseUrl: string;
- serviceRoleKey: string;
openAIApiKey: string;
- bucket?: string; // default: ai-storage
}
export class OpenAIService {
- private readonly supabase: SupabaseClient;
private readonly openai: OpenAI;
- private readonly bucket: string;
- private constructor(
- client: SupabaseClient,
- openai: OpenAI,
- bucket: string,
- ) {
- this.supabase = client;
+ private constructor(openai: OpenAI) {
this.openai = openai;
- this.bucket = bucket;
}
static fromEnv(): StructuredResponseFailure | OpenAIService {
- const supabaseUrl = Deno.env.get("SUPABASE_URL") ?? "";
- const serviceRoleKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "";
const openAIApiKey = Deno.env.get("OPENAI_API_KEY") ?? "";
- const bucket = Deno.env.get("SUPABASE_BUCKET") ?? "ai-storage";
- if (!supabaseUrl || !serviceRoleKey || !openAIApiKey) {
+ if (!openAIApiKey) {
return {
ok: false,
code: "MissingEnv",
- message:
- "Required env vars missing: SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, OPENAI_API_KEY",
+ message: "Required env vars missing: OPENAI_API_KEY",
};
}
- const client = createClient(supabaseUrl, serviceRoleKey);
const openai = new OpenAI({ apiKey: openAIApiKey });
- return new OpenAIService(client, openai, bucket);
+ return new OpenAIService(openai);
}
async createConversation(metadata?: Record) {
const conversation = await this.openai.conversations.create({
@@ -80,9 +61,6 @@ export class OpenAIService {
files?: File[],
): Promise> {
try {
- const uploadedToStorage = await this.uploadFilesToStorage(
- files ?? [],
- );
const openaiFileIds = await this.uploadFilesToOpenAI(files ?? []);
const newOptions = { ...options };
@@ -136,7 +114,7 @@ export class OpenAIService {
conversationId: conversationId
? String(conversationId)
: null,
- references: { uploadedToStorage, openaiFileIds },
+ references: { openaiFileIds },
openaiRaw,
};
}
@@ -175,38 +153,18 @@ export class OpenAIService {
usage,
responseId: String(responseId),
conversationId: conversationId ? String(conversationId) : null,
- references: { uploadedToStorage, openaiFileIds },
+ references: { openaiFileIds },
openaiRaw,
};
} catch (err) {
const e = err as Error;
const message = e?.message ?? "Unknown error";
- const code = message.includes("Supabase upload failed")
- ? "StorageUploadFailed"
- : message.includes("OpenAI file upload failed")
+ const code = message.includes("OpenAI file upload failed")
? "OpenAIFileUploadFailed"
: "OpenAIRequestFailed";
return { ok: false, code, message, cause: err };
}
}
- private async uploadFilesToStorage(files: File[]): Promise {
- const paths: string[] = [];
- for (const file of files) {
- const safeName = this.sanitizeFilename(file.name);
- const path = `${crypto.randomUUID()}-${safeName}`;
- const { data, error } = await this.supabase.storage
- .from(this.bucket)
- .upload(path, file, {
- contentType: file.type || "application/octet-stream",
- upsert: false,
- });
- if (error) {
- throw new Error(`Supabase upload failed: ${error.message}`);
- }
- paths.push(data.path);
- }
- return paths;
- }
private async uploadFilesToOpenAI(files: File[]): Promise {
const ids: string[] = [];
for (const file of files) {
diff --git a/supabase/functions/ai-generate-subject/index.ts b/supabase/functions/ai-generate-subject/index.ts
index b83a63e..1d5c2fc 100644
--- a/supabase/functions/ai-generate-subject/index.ts
+++ b/supabase/functions/ai-generate-subject/index.ts
@@ -18,38 +18,13 @@ addEventListener("beforeunload", (ev: BeforeUnloadWithDetail) => {
);
});
-/**
- * JSON input signature (when `Content-Type: application/json`)
- *
- * Required:
- * - `id` (uuid) - corresponde a la columna `id` de `asignaturas`
- *
- * Optional patch fields (any subset is allowed):
- * - `nombre` (string)
- * - `codigo` (string | null)
- * - `tipo` (string | null) // must match DB enum values
- * - `creditos` (number)
- * - `horas_academicas` (number | null)
- * - `horas_independientes` (number | null)
- * - `numero_ciclo` (number | null)
- * - `estructura_id` (uuid | null)
- * - `plan_estudio_id` (uuid)
- * - `linea_plan_id` (uuid | null)
- * - `orden_celda` (number | null)
- *
- * IA config (optional):
- * - `descripcionEnfoqueAcademico` (string)
- *
- * Notes:
- * - This JSON flow does NOT accept `instruccionesAdicionalesIA`.
- * - Missing optional fields are ignored (the existing DB values remain).
- */
-
-const JsonUpdateSchema = z
+const DatosUpdateSchema = z
.object({
- id: z.string().uuid("id debe ser un UUID"),
+ id: z.string().uuid("id debe ser un UUID").optional(),
- // patch fields (all optional) — nombres coinciden con columnas DB
+ // campos de asignatura (nombres coinciden con columnas DB)
+ plan_estudio_id: z.string().uuid("plan_estudio_id debe ser un UUID"),
+ estructura_id: z.string().uuid("estructura_id debe ser un UUID").optional(),
nombre: z.string().min(1).optional(),
codigo: z.union([z.string().min(1), z.null()]).optional(),
tipo: z.union([z.string().min(1), z.null()]).optional(),
@@ -57,17 +32,70 @@ const JsonUpdateSchema = z
horas_academicas: z.number().int().nonnegative().nullable().optional(),
horas_independientes: z.number().int().nonnegative().nullable().optional(),
numero_ciclo: z.number().int().positive().nullable().optional(),
- estructura_id: z.string().uuid().nullable().optional(),
- plan_estudio_id: z.string().uuid().optional(),
linea_plan_id: z.string().uuid().nullable().optional(),
orden_celda: z.number().int().nonnegative().nullable().optional(),
-
- // IA config (no instruccionesAdicionalesIA)
- descripcionEnfoqueAcademico: z.string().optional(),
})
.strict();
-type EdgeAIGenerateSubjectJsonUpdateInput = z.infer;
+const IAConfigSchema = z
+ .object({
+ descripcionEnfoqueAcademico: z.string().optional(),
+ instruccionesAdicionalesIA: z.string().optional(),
+ archivosAdjuntos: z.array(z.string().min(1)).optional().default([]),
+ })
+ .strict()
+ .default({});
+
+const UnifiedJsonSchema = z
+ .object({
+ datosUpdate: DatosUpdateSchema,
+ iaConfig: IAConfigSchema,
+ })
+ .strict()
+ .superRefine((val, ctx) => {
+ // Si no hay id, es un insert => necesitamos campos mínimos
+ if (!val.datosUpdate.id) {
+ if (!val.datosUpdate.estructura_id) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ path: ["datosUpdate", "estructura_id"],
+ message: "estructura_id es requerido para crear",
+ });
+ }
+ if (!val.datosUpdate.nombre) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ path: ["datosUpdate", "nombre"],
+ message: "nombre es requerido para crear",
+ });
+ }
+ if (val.datosUpdate.creditos === undefined) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ path: ["datosUpdate", "creditos"],
+ message: "creditos es requerido para crear",
+ });
+ }
+ }
+ });
+
+type EdgeAIGenerateSubjectUnifiedInput = z.infer;
+
+type AsignaturaBaseSeleccionada = {
+ id: string;
+ plan_estudio_id: string;
+ estructura_id: string | null;
+ nombre: string;
+ codigo: string | null;
+ tipo: Database["public"]["Tables"]["asignaturas"]["Row"]["tipo"];
+ creditos: number;
+ horas_academicas: number | null;
+ horas_independientes: number | null;
+ numero_ciclo: number | null;
+ linea_plan_id: string | null;
+ orden_celda: number | null;
+ estado: Database["public"]["Enums"]["estado_asignatura"];
+};
function withColumnDefsAndRefs(
schemaDef: Record,
@@ -96,28 +124,6 @@ function withColumnDefsAndRefs(
return nextSchema;
}
-function splitAiOutputStringsAndColumns(
- aiOutput: unknown,
-): { aiOutputJson: Json; columnasGeneradas: Record } {
- if (!aiOutput || typeof aiOutput !== "object" || Array.isArray(aiOutput)) {
- return { aiOutputJson: aiOutput as unknown as Json, columnasGeneradas: {} };
- }
-
- const record = aiOutput as Record;
- const stringsOnly: Record = {};
- const columnasGeneradas: Record = {};
-
- 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) => {
@@ -171,9 +177,8 @@ Deno.serve(async (req: Request): Promise => {
}
const contentType = (req.headers.get("content-type") || "").toLowerCase();
- const isMultipart = contentType.startsWith("multipart/form-data");
const isJson = contentType.includes("application/json");
- if (!isMultipart && !isJson) {
+ if (!isJson) {
console.error(
`[${
new Date().toISOString()
@@ -183,7 +188,7 @@ Deno.serve(async (req: Request): Promise => {
415,
"Content-Type no soportado.",
"UNSUPPORTED_MEDIA_TYPE",
- { contentType },
+ { contentType, expected: "application/json" },
);
}
@@ -237,340 +242,224 @@ Deno.serve(async (req: Request): Promise => {
) ?? "gpt-5-nano";
// -----------------------------
- // JSON update flow
+ // Unified JSON create/update flow (background)
// -----------------------------
- if (isJson) {
- let rawBody: unknown;
- try {
- rawBody = await req.json();
- } catch (e) {
- throw new HttpError(
- 400,
- "Body JSON inválido.",
- "INVALID_JSON",
- { cause: e },
- );
- }
+ let rawBody: unknown;
+ try {
+ rawBody = await req.json();
+ } catch (e) {
+ throw new HttpError(
+ 400,
+ "Body JSON inválido.",
+ "INVALID_JSON",
+ { cause: e },
+ );
+ }
- const parsedBody = JsonUpdateSchema.safeParse(rawBody);
- if (!parsedBody.success) {
- throw new HttpError(
- 422,
- formatZodIssues(parsedBody.error.issues),
- "VALIDATION_ERROR",
- parsedBody.error,
- );
- }
+ const parsedBody = UnifiedJsonSchema.safeParse(rawBody);
+ if (!parsedBody.success) {
+ throw new HttpError(
+ 422,
+ formatZodIssues(parsedBody.error.issues),
+ "VALIDATION_ERROR",
+ parsedBody.error,
+ );
+ }
- const payload: EdgeAIGenerateSubjectJsonUpdateInput = parsedBody.data;
+ const payload: EdgeAIGenerateSubjectUnifiedInput = parsedBody.data;
+ const { datosUpdate, iaConfig } = payload;
+ const isUpdate = Boolean(datosUpdate.id);
- const { data: existingAsignatura, error: existingAsignaturaError } =
- await supabaseService
- .from("asignaturas")
- .select(
- "id,plan_estudio_id,estructura_id,nombre,codigo,tipo,creditos,horas_academicas,horas_independientes,numero_ciclo,linea_plan_id,orden_celda",
- )
- .eq("id", payload.id)
- .single();
+ const svc = OpenAIService.fromEnv();
+ if (!(svc instanceof OpenAIService)) {
+ throw new HttpError(
+ 500,
+ "Configuración del servidor incompleta.",
+ "OPENAI_MISCONFIGURED",
+ svc,
+ );
+ }
- if (existingAsignaturaError) {
- const maybeCode = (existingAsignaturaError as { code?: string }).code;
+ // Resolve fields (merge for update)
+ const selectCols =
+ "id,plan_estudio_id,estructura_id,nombre,codigo,tipo,creditos,horas_academicas,horas_independientes,numero_ciclo,linea_plan_id,orden_celda,estado";
+
+ let asignaturaBase: AsignaturaBaseSeleccionada | null = null;
+
+ if (isUpdate) {
+ const { data, error } = await supabaseService
+ .from("asignaturas")
+ .select(selectCols)
+ .eq("id", datosUpdate.id as string)
+ .single();
+ if (error) {
+ const maybeCode = (error as { code?: string }).code;
if (maybeCode === "PGRST116") {
throw new HttpError(
404,
"No se encontró la asignatura.",
"NOT_FOUND",
- { table: "asignaturas", id: payload.id },
+ { table: "asignaturas", id: datosUpdate.id },
);
}
throw new HttpError(
500,
"No se pudo obtener la asignatura.",
"SUPABASE_QUERY_FAILED",
- existingAsignaturaError,
+ error,
);
}
-
- const resolved = {
- plan_estudio_id: payload.plan_estudio_id ??
- existingAsignatura.plan_estudio_id,
- estructura_id: payload.estructura_id ??
- existingAsignatura.estructura_id,
- nombre: payload.nombre ?? existingAsignatura.nombre,
- codigo: payload.codigo ?? existingAsignatura.codigo,
- tipo: payload.tipo ?? existingAsignatura.tipo,
- creditos: payload.creditos ?? existingAsignatura.creditos,
- horas_academicas: payload.horas_academicas ??
- existingAsignatura.horas_academicas,
- horas_independientes: payload.horas_independientes ??
- existingAsignatura.horas_independientes,
- numero_ciclo: payload.numero_ciclo ?? existingAsignatura.numero_ciclo,
- linea_plan_id: payload.linea_plan_id ??
- existingAsignatura.linea_plan_id,
- orden_celda: payload.orden_celda ?? existingAsignatura.orden_celda,
- };
-
- if (!resolved.estructura_id) {
- throw new HttpError(
- 422,
- "estructura_id es requerido (no está presente ni en el JSON ni en la asignatura existente).",
- "VALIDATION_ERROR",
- {
- id: payload.id,
- existing_estructura_id: existingAsignatura.estructura_id,
- },
- );
- }
-
- const { data: estructura, error: estructuraError } = await supabaseService
- .from("estructuras_asignatura")
- .select("id,nombre,definicion,version")
- .eq("id", resolved.estructura_id)
- .single();
- if (estructuraError) {
- const maybeCode = (estructuraError as { code?: string }).code;
- if (maybeCode === "PGRST116") {
- throw new HttpError(
- 404,
- "No se encontró la estructura de la asignatura.",
- "NOT_FOUND",
- { table: "estructuras_asignatura", id: resolved.estructura_id },
- );
- }
- throw new HttpError(
- 500,
- "No se pudo obtener la estructura de la asignatura.",
- "SUPABASE_QUERY_FAILED",
- estructuraError,
- );
- }
-
- const systemPrompt =
- "Eres un asistente experto en diseño curricular. Responde únicamente con JSON válido que cumpla estrictamente el JSON Schema proporcionado.";
-
- const userPrompt =
- `Genera un borrador completo completo de una ASIGNATURA con base en lo siguiente:\n` +
- `- Nombre de la asignatura: ${resolved.nombre}\n` +
- `- Código (clave de la asignatura): ${
- resolved.codigo ?? "(no especificado)"
- }\n` +
- `- Tipo: ${resolved.tipo ?? "(no especificado)"}\n` +
- `- Número de créditos: ${resolved.creditos}\n` +
- `- Horas académicas: ${
- resolved.horas_academicas ?? "(no especificado)"
- }\n` +
- `- Horas independientes: ${
- resolved.horas_independientes ?? "(no especificado)"
- }\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 =
- typeof estructura?.definicion === "object" &&
- estructura?.definicion !== null
- ? (estructura.definicion as Record)
- : {};
-
- const schemaForAI = withColumnDefsAndRefs(schemaDef);
-
- const aiStructuredPayload: StructuredResponseOptions = {
- model: AI_GENERATE_SUBJECT_UPDATE_MODELO,
- input: [
- { role: "system", content: systemPrompt },
- { role: "user", content: userPrompt },
- ],
- text: {
- format: {
- type: "json_schema",
- name: "asignatura_contenido_tematico",
- schema: schemaForAI,
- strict: true,
- },
- },
- };
-
- const svc = OpenAIService.fromEnv();
- if (!(svc instanceof OpenAIService)) {
- throw new HttpError(
- 500,
- "Configuración del servidor incompleta.",
- "OPENAI_MISCONFIGURED",
- svc,
- );
- }
-
- const aiResult = await svc.createStructuredResponse(aiStructuredPayload);
- if (!aiResult.ok) {
- const status = aiResult.code === "MissingEnv" ? 500 : 502;
- throw new HttpError(
- status,
- "No se pudo generar la asignatura con IA.",
- "OPENAI_REQUEST_FAILED",
- aiResult,
- );
- }
-
- let aiOutput = aiResult.output ?? null;
- if (aiOutput == null && aiResult.outputText) {
- try {
- aiOutput = 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 (!aiOutput) {
- throw new HttpError(
- 502,
- "La respuesta de la IA no contiene salida estructurada.",
- "OPENAI_MISSING_STRUCTURED_OUTPUT",
- { outputText: aiResult.outputText ?? null },
- );
- }
-
- const { aiOutputJson, columnasGeneradas } =
- splitAiOutputStringsAndColumns(aiOutput);
-
- const updatePatch: Database["public"]["Tables"]["asignaturas"]["Update"] =
- {
- datos: aiOutputJson,
- tipo_origen: "IA",
- estado: "borrador",
- };
-
- for (const value of Object.values(columnasGeneradas)) {
- if (!value || typeof value !== "object" || Array.isArray(value)) {
- continue;
- }
- const xColumn = (value as Record)["x-column"];
- const xDef = (value as Record)["x-definicion"];
- if (typeof xColumn !== "string" || !xColumn.length) continue;
- (updatePatch as unknown as Record)[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;
- }
- if (payload.estructura_id !== undefined) {
- updatePatch.estructura_id = payload.estructura_id;
- }
- if (payload.nombre !== undefined) {
- updatePatch.nombre = payload.nombre;
- }
- if (payload.codigo !== undefined) {
- updatePatch.codigo = payload.codigo;
- }
- if (payload.tipo !== undefined) {
- updatePatch.tipo = payload.tipo as Database["public"]["Tables"][
- "asignaturas"
- ]["Update"]["tipo"];
- }
- if (payload.creditos !== undefined) {
- updatePatch.creditos = payload.creditos;
- }
- if (payload.horas_academicas !== undefined) {
- updatePatch.horas_academicas = payload.horas_academicas;
- }
- if (payload.horas_independientes !== undefined) {
- updatePatch.horas_independientes = payload.horas_independientes;
- }
- if (payload.numero_ciclo !== undefined) {
- updatePatch.numero_ciclo = payload.numero_ciclo;
- }
- if (payload.linea_plan_id !== undefined) {
- updatePatch.linea_plan_id = payload.linea_plan_id;
- }
- if (payload.orden_celda !== undefined) {
- updatePatch.orden_celda = payload.orden_celda;
- }
-
- updatePatch.meta_origen = {
- generado_por: "ai_generate_subject_update_json",
- ai: {
- responseId: aiResult.responseId ?? null,
- conversationId: aiResult.conversationId ?? null,
- model: aiResult.model,
- usage: aiResult.usage ?? null,
- },
- iaConfig: {
- descripcionEnfoqueAcademico: payload.descripcionEnfoqueAcademico ??
- null,
- },
- } as unknown as Json;
-
- const { data: asignatura, error: asignaturaError } = await supabaseService
- .from("asignaturas")
- .update(updatePatch)
- .eq("id", payload.id)
- .select(
- "id,nombre,codigo,tipo,creditos,horas_academicas,horas_independientes,estructura_id,plan_estudio_id,contenido_tematico,meta_origen,datos,creado_en,actualizado_en,numero_ciclo,linea_plan_id,orden_celda",
- )
- .single();
-
- if (asignaturaError) {
- const maybeCode = (asignaturaError as { code?: string }).code;
- if (maybeCode === "PGRST116") {
- throw new HttpError(
- 404,
- "No se encontró la asignatura.",
- "NOT_FOUND",
- { table: "asignaturas", id: payload.id },
- );
- }
- throw new HttpError(
- 500,
- "No se pudo actualizar la asignatura.",
- "SUPABASE_UPDATE_FAILED",
- asignaturaError,
- );
- }
-
- console.log(
- `[${
- new Date().toISOString()
- }][${functionName}]: JSON update processed successfully`,
- );
- return sendSuccess(asignatura);
+ asignaturaBase = data as unknown as AsignaturaBaseSeleccionada;
}
- // -----------------------------
- // Multipart create flow (existing)
- // -----------------------------
- const formData = await req.formData();
- const validation = parseAndValidate(formData);
- if (!validation.success) {
- console.error(
- `[${new Date().toISOString()}][${functionName}]: Validation errors:`,
- validation.errors,
- );
- const message = validation.errors
- .map((e, i) => `${i + 1}. ${e}`)
- .join("\n");
+ const resolved = {
+ id: (datosUpdate.id ?? asignaturaBase?.id ?? null) as string | null,
+ plan_estudio_id: datosUpdate.plan_estudio_id ??
+ asignaturaBase?.plan_estudio_id,
+ estructura_id: datosUpdate.estructura_id ??
+ asignaturaBase?.estructura_id ?? null,
+ nombre: datosUpdate.nombre ?? asignaturaBase?.nombre ?? null,
+ codigo: datosUpdate.codigo ?? asignaturaBase?.codigo ?? null,
+ tipo: datosUpdate.tipo ?? asignaturaBase?.tipo ?? null,
+ creditos: datosUpdate.creditos ?? asignaturaBase?.creditos ?? null,
+ horas_academicas: datosUpdate.horas_academicas ??
+ asignaturaBase?.horas_academicas ?? null,
+ horas_independientes: datosUpdate.horas_independientes ??
+ asignaturaBase?.horas_independientes ?? null,
+ numero_ciclo: datosUpdate.numero_ciclo ?? asignaturaBase?.numero_ciclo ??
+ null,
+ linea_plan_id: datosUpdate.linea_plan_id ??
+ asignaturaBase?.linea_plan_id ?? null,
+ orden_celda: datosUpdate.orden_celda ?? asignaturaBase?.orden_celda ??
+ null,
+ };
+
+ if (!resolved.estructura_id) {
throw new HttpError(
422,
- message,
+ "estructura_id es requerido.",
"VALIDATION_ERROR",
- { errors: validation.errors },
+ { estructura_id: resolved.estructura_id },
+ );
+ }
+ if (!resolved.nombre) {
+ throw new HttpError(
+ 422,
+ "nombre es requerido.",
+ "VALIDATION_ERROR",
+ { nombre: resolved.nombre },
+ );
+ }
+ if (resolved.creditos == null) {
+ throw new HttpError(
+ 422,
+ "creditos es requerido.",
+ "VALIDATION_ERROR",
+ { creditos: resolved.creditos },
);
}
- const payload = validation.data;
+ // Crear/actualizar stub en estado 'generando'
+ let asignaturaId: string;
+ if (isUpdate) {
+ const updatePatch: Database["public"]["Tables"]["asignaturas"]["Update"] =
+ {
+ estado: "generando",
+ tipo_origen: "IA",
+ plan_estudio_id: resolved.plan_estudio_id,
+ estructura_id: resolved.estructura_id,
+ nombre: resolved.nombre,
+ codigo: resolved.codigo,
+ creditos: resolved.creditos,
+ horas_academicas: resolved.horas_academicas,
+ horas_independientes: resolved.horas_independientes,
+ numero_ciclo: resolved.numero_ciclo,
+ linea_plan_id: resolved.linea_plan_id,
+ orden_celda: resolved.orden_celda,
+ tipo: resolved
+ .tipo as Database["public"]["Tables"]["asignaturas"]["Update"][
+ "tipo"
+ ],
+ meta_origen: {
+ generado_por: "ai_generate_subject_unified",
+ iaConfig: {
+ descripcionEnfoqueAcademico:
+ iaConfig.descripcionEnfoqueAcademico ?? null,
+ instruccionesAdicionalesIA: iaConfig.instruccionesAdicionalesIA ??
+ null,
+ archivosAdjuntos: iaConfig.archivosAdjuntos ?? [],
+ },
+ } as unknown as Json,
+ };
+
+ const { data, error } = await supabaseService
+ .from("asignaturas")
+ .update(updatePatch)
+ .eq("id", resolved.id as string)
+ .select("id")
+ .single();
+ if (error) {
+ throw new HttpError(
+ 500,
+ "No se pudo actualizar la asignatura (stub).",
+ "SUPABASE_UPDATE_FAILED",
+ error,
+ );
+ }
+ asignaturaId = data.id;
+ } else {
+ const insertPatch: Database["public"]["Tables"]["asignaturas"]["Insert"] =
+ {
+ plan_estudio_id: resolved.plan_estudio_id as string,
+ 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"],
+ creditos: resolved.creditos as number,
+ horas_academicas: resolved.horas_academicas ?? null,
+ horas_independientes: resolved.horas_independientes ?? null,
+ numero_ciclo: resolved.numero_ciclo ?? null,
+ linea_plan_id: resolved.linea_plan_id ?? null,
+ orden_celda: resolved.orden_celda ?? null,
+ estado: "generando",
+ tipo_origen: "IA",
+ meta_origen: {
+ generado_por: "ai_generate_subject_unified",
+ iaConfig: {
+ descripcionEnfoqueAcademico:
+ iaConfig.descripcionEnfoqueAcademico ?? null,
+ instruccionesAdicionalesIA: iaConfig.instruccionesAdicionalesIA ??
+ null,
+ archivosAdjuntos: iaConfig.archivosAdjuntos ?? [],
+ },
+ } as unknown as Json,
+ };
+
+ const { data, error } = await supabaseService
+ .from("asignaturas")
+ .insert(insertPatch)
+ .select("id")
+ .single();
+ if (error) {
+ const maybeCode = (error as { code?: string }).code;
+ const status = maybeCode ? 409 : 500;
+ throw new HttpError(
+ status,
+ "No se pudo crear la asignatura (stub).",
+ "SUPABASE_INSERT_FAILED",
+ { ...error, code: maybeCode },
+ );
+ }
+ asignaturaId = data.id;
+ }
const { data: estructura, error: estructuraError } = await supabaseService
.from("estructuras_asignatura")
.select("id,nombre,definicion,version")
- .eq("id", payload.datosBasicos.estructuraId)
+ .eq("id", resolved.estructura_id)
.single();
if (estructuraError) {
const maybeCode = (estructuraError as { code?: string }).code;
@@ -579,10 +468,7 @@ Deno.serve(async (req: Request): Promise => {
404,
"No se encontró la estructura de la asignatura.",
"NOT_FOUND",
- {
- table: "estructuras_asignatura",
- id: payload.datosBasicos.estructuraId,
- },
+ { table: "estructuras_asignatura", id: resolved.estructura_id },
);
}
throw new HttpError(
@@ -594,35 +480,39 @@ Deno.serve(async (req: Request): Promise => {
}
const systemPrompt =
- `Eres un asistente experto en diseño curricular. Responde únicamente con JSON válido que cumpla estrictamente el JSON Schema proporcionado.
- # REGLAS DE FORMATO PARA CAMPOS DE TEXTO (STRING)
-1. **Prioridad:** Estas reglas aplican por defecto. Si el prompt del usuario solicita explícitamente un formato distinto, esa instrucción tiene prioridad sobre estas reglas.
-2. **Estilo Visual:** Redacta el contenido exclusivamente para visualización en texto plano (estilo 'white-space: pre-wrap').
-3. **Estructura Vertical:** Utiliza saltos de línea explícitos para romper líneas y doble salto de línea para separar párrafos.
-4. **Indentación Estricta:** Usa exactamente 2 espacios para la indentación jerárquica. No uses tabuladores.
-5. **Listas:** Utiliza un guion seguido de un espacio ("- ") para los elementos de lista.
-6. **Prohibiciones:** No incluyas etiquetas HTML, sintaxis Markdown ni caracteres de escape literales visibles en el texto final. Asegúrate de que el JSON final contenga saltos de línea válidos ('\n') y no texto escapado.`;
+ `Eres un asistente experto en diseño curricular. Responde únicamente con JSON válido que cumpla estrictamente el JSON Schema proporcionado.`;
+
+ const archivosAdjuntosTexto = (iaConfig.archivosAdjuntos ?? []).length
+ ? `\n- Rutas de archivos adjuntos (referencia): ${
+ (iaConfig.archivosAdjuntos ?? []).join(", ")
+ }`
+ : "";
const userPrompt =
`Genera un borrador completo completo de una ASIGNATURA con base en lo siguiente:\n` +
- `- Nombre de la asignatura: ${payload.datosBasicos.nombre}\n` +
+ `- Nombre de la asignatura: ${resolved.nombre}\n` +
`- Código (clave de la asignatura): ${
- payload.datosBasicos.codigo ?? "(no especificado)"
+ resolved.codigo ?? "(no especificado)"
}\n` +
- `- Tipo: ${payload.datosBasicos.tipo ?? "(no especificado)"}\n` +
- `- Número de créditos: ${payload.datosBasicos.creditos}\n` +
+ `- Tipo: ${resolved.tipo ?? "(no especificado)"}\n` +
+ `- Número de créditos: ${resolved.creditos}\n` +
`- Horas académicas: ${
- payload.datosBasicos.horasAcademicas ?? "(no especificado)"
+ resolved.horas_academicas ?? "(no especificado)"
}\n` +
`- Horas independientes: ${
- payload.datosBasicos.horasIndependientes ?? "(no especificado)"
+ resolved.horas_independientes ?? "(no especificado)"
}\n` +
`- Descripción del enfoque académico (sobre el contenido de la respuesta generada): ${
- payload.iaConfig?.descripcionEnfoqueAcademico ?? "(ninguna)"
+ iaConfig.descripcionEnfoqueAcademico ?? "(ninguna)"
}\n` +
`- Instrucciones adicionales (sobre el formato de la respuesta generada): ${
- payload.iaConfig?.instruccionesAdicionalesIA ?? "(ninguna)"
- }`;
+ 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 (${
+ resolved.horas_academicas ?? 0
+ }). No te pases ni te falten horas.`;
const schemaDef: Record =
typeof estructura?.definicion === "object" &&
@@ -633,7 +523,15 @@ Deno.serve(async (req: Request): Promise => {
const schemaForAI = withColumnDefsAndRefs(schemaDef);
const aiStructuredPayload: StructuredResponseOptions = {
- model: AI_GENERATE_SUBJECT_INSERT_MODELO,
+ model: isUpdate
+ ? AI_GENERATE_SUBJECT_UPDATE_MODELO
+ : AI_GENERATE_SUBJECT_INSERT_MODELO,
+ background: true,
+ metadata: {
+ tabla: "asignaturas",
+ accion: isUpdate ? "actualizar" : "crear",
+ id: asignaturaId,
+ },
input: [
{ role: "system", content: systemPrompt },
{ role: "user", content: userPrompt },
@@ -648,126 +546,27 @@ Deno.serve(async (req: Request): Promise => {
},
};
- const svc = OpenAIService.fromEnv();
- if (!(svc instanceof OpenAIService)) {
- throw new HttpError(
- 500,
- "Configuración del servidor incompleta.",
- "OPENAI_MISCONFIGURED",
- svc,
- );
- }
-
- const aiResult = await svc.createStructuredResponse(
- aiStructuredPayload,
- payload.archivosAdjuntos,
- );
+ const aiResult = await svc.createStructuredResponse(aiStructuredPayload);
if (!aiResult.ok) {
const status = aiResult.code === "MissingEnv" ? 500 : 502;
throw new HttpError(
status,
- "No se pudo generar la asignatura con IA.",
+ "No se pudo iniciar la generación de la asignatura con IA.",
"OPENAI_REQUEST_FAILED",
aiResult,
);
}
- let aiOutput = aiResult.output ?? null;
- if (aiOutput == null && aiResult.outputText) {
- try {
- aiOutput = 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 (!aiOutput) {
- throw new HttpError(
- 502,
- "La respuesta de la IA no contiene salida estructurada.",
- "OPENAI_MISSING_STRUCTURED_OUTPUT",
- { outputText: aiResult.outputText ?? null },
- );
- }
-
- const { aiOutputJson, columnasGeneradas } = splitAiOutputStringsAndColumns(
- aiOutput,
- );
-
- const asignaturaInsert:
- Database["public"]["Tables"]["asignaturas"]["Insert"] = {
- plan_estudio_id: payload.plan_estudio_id,
- estructura_id: payload.datosBasicos.estructuraId,
- nombre: payload.datosBasicos.nombre,
- codigo: payload.datosBasicos.codigo ?? null,
- creditos: payload.datosBasicos.creditos,
- horas_academicas: payload.datosBasicos.horasAcademicas ?? null,
- horas_independientes: payload.datosBasicos.horasIndependientes ?? null,
- tipo: (payload.datosBasicos.tipo ?? undefined) as Database["public"][
- "Tables"
- ]["asignaturas"]["Insert"]["tipo"],
- tipo_origen: "IA",
- datos: aiOutputJson,
- meta_origen: {
- generado_por: "ai_generate_subject",
- ai: {
- responseId: aiResult.responseId ?? null,
- conversationId: aiResult.conversationId ?? null,
- model: aiResult.model,
- usage: aiResult.usage ?? null,
- },
- referencias: {
- archivosReferenciaIds: payload.iaConfig?.archivosReferencia ?? null,
- repositoriosReferencia: payload.iaConfig?.repositoriosReferencia ??
- null,
- },
- iaConfig: {
- descripcionEnfoqueAcademico:
- payload.iaConfig?.descripcionEnfoqueAcademico ?? null,
- instruccionesAdicionalesIA:
- payload.iaConfig?.instruccionesAdicionalesIA ?? null,
- },
- } 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)["x-column"];
- const xDef = (value as Record)["x-definicion"];
- if (typeof xColumn !== "string" || !xColumn.length) continue;
- (asignaturaInsert as unknown as Record)[xColumn] =
- xDef as unknown as Json;
- }
-
- const { data: asignatura, error: asignaturaError } = await supabaseService
- .from("asignaturas")
- .insert(asignaturaInsert)
- .select(
- "id,nombre,codigo,tipo,creditos,horas_academicas,horas_independientes,estructura_id,plan_estudio_id,contenido_tematico,meta_origen,creado_en,actualizado_en",
- )
- .single();
-
- if (asignaturaError) {
- const maybeCode = (asignaturaError as { code?: string }).code;
- const status = maybeCode ? 409 : 500;
- throw new HttpError(
- status,
- "No se pudo guardar la asignatura.",
- "SUPABASE_INSERT_FAILED",
- { ...asignaturaError, code: maybeCode },
- );
- }
-
console.log(
`[${
new Date().toISOString()
- }][${functionName}]: Request processed successfully`,
+ }][${functionName}]: Subject generation started in background`,
);
- return sendSuccess(asignatura);
+ return sendSuccess({
+ id: asignaturaId,
+ estado: "generando",
+ openai: { responseId: aiResult.responseId ?? null },
+ });
} catch (error) {
if (error instanceof HttpError) {
console.error(
@@ -801,86 +600,6 @@ Deno.serve(async (req: Request): Promise => {
}
});
-const jsonFromString = (schema: T) =>
- z.string().transform((str, ctx) => {
- try {
- return JSON.parse(str);
- } catch {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: "El formato no es un JSON válido",
- });
- return z.NEVER;
- }
- }).pipe(schema);
-
-const jsonFromStringOptional = (schema: T) =>
- z.string().optional().default("{}").transform((str, ctx) => {
- try {
- return JSON.parse(str);
- } catch {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: "El formato no es un JSON válido",
- });
- return z.NEVER;
- }
- }).pipe(schema);
-
-const DatosBasicosSchema = z.object({
- nombre: z.string().min(1, "El nombre es requerido"),
- codigo: z.string().optional().nullable(),
- tipo: z.union([z.string().min(1), z.null()]).optional().transform((v) =>
- v ?? null
- ),
- creditos: z.number({ invalid_type_error: "Créditos debe ser numérico" })
- .positive("Créditos debe ser >= 0"),
- horasAcademicas: z.number().int().nonnegative().optional().nullable(),
- horasIndependientes: z.number().int().nonnegative().optional().nullable(),
- estructuraId: z.string().uuid("estructuraId debe ser un UUID"),
-});
-
-const IAConfigSchema = z.object({
- descripcionEnfoqueAcademico: z.string().optional(),
- instruccionesAdicionalesIA: z.string().optional(),
- archivosReferencia: z.array(z.string().uuid()).optional(),
- repositoriosReferencia: z.array(z.string().uuid()).optional(),
-}).partial();
-
-const SolicitudSchema = z.object({
- plan_estudio_id: z.string().uuid("plan_estudio_id debe ser un UUID"),
- datosBasicos: jsonFromString(DatosBasicosSchema),
- iaConfig: jsonFromStringOptional(IAConfigSchema),
- archivosAdjuntos: z.array(z.instanceof(File)).optional().default([]),
-});
-
-type EdgeAIGenerateSubjectInput = z.infer;
-
-function parseAndValidate(
- formData: FormData,
-): { success: true; data: EdgeAIGenerateSubjectInput } | {
- success: false;
- errors: string[];
-} {
- const rawInput = {
- plan_estudio_id: formData.get("plan_estudio_id"),
- datosBasicos: formData.get("datosBasicos"),
- iaConfig: formData.get("iaConfig"),
- archivosAdjuntos: formData.getAll("archivosAdjuntos"),
- };
-
- const result = SolicitudSchema.safeParse(rawInput);
- if (!result.success) {
- const errors = result.error.errors.map((issue) => {
- const path = issue.path.length ? `${issue.path.join(".")}: ` : "";
- return `${path}${issue.message}`;
- });
- return { success: false, errors };
- }
-
- return { success: true, data: result.data };
-}
-
const definicionesDeEstructurasDeColumnas = {
contenido_tematico: {
"type": "object",
diff --git a/supabase/functions/ai-generate-subject/types.ts b/supabase/functions/ai-generate-subject/types.ts
index ec9d6e4..a3ee32e 100644
--- a/supabase/functions/ai-generate-subject/types.ts
+++ b/supabase/functions/ai-generate-subject/types.ts
@@ -1,37 +1,24 @@
import { Tables } from "../_shared/database.types.ts";
-export interface UploadedFile {
- id: string; // Necesario para React (key)
- file: File; // La fuente de verdad (contiene name, size, type)
- preview?: string; // Opcional: si fueran imágenes
-}
-
export type AIGenerateSubjectInput = {
- plan_estudio_id: Tables<"asignaturas">["plan_estudio_id"];
- datosBasicos: {
- nombre: Tables<"asignaturas">["nombre"];
+ datosUpdate: {
+ id?: Tables<"asignaturas">["id"]; // si viene, es update; si no, es insert
+ plan_estudio_id: Tables<"asignaturas">["plan_estudio_id"];
+ estructura_id?: Tables<"asignaturas">["estructura_id"] | null;
+
+ nombre?: Tables<"asignaturas">["nombre"];
codigo?: Tables<"asignaturas">["codigo"];
- tipo: Tables<"asignaturas">["tipo"] | null;
- creditos: Tables<"asignaturas">["creditos"] | null;
- horasAcademicas?: Tables<"asignaturas">["horas_academicas"] | null;
- horasIndependientes?: Tables<"asignaturas">["horas_independientes"] | null;
- estructuraId: Tables<"asignaturas">["estructura_id"] | null;
+ tipo?: Tables<"asignaturas">["tipo"] | null;
+ creditos?: Tables<"asignaturas">["creditos"] | null;
+ horas_academicas?: Tables<"asignaturas">["horas_academicas"] | null;
+ horas_independientes?: Tables<"asignaturas">["horas_independientes"] | null;
+ numero_ciclo?: Tables<"asignaturas">["numero_ciclo"] | null;
+ linea_plan_id?: Tables<"asignaturas">["linea_plan_id"] | null;
+ orden_celda?: Tables<"asignaturas">["orden_celda"] | null;
};
- // clonInterno?: {
- // facultadId?: string
- // carreraId?: string
- // planOrigenId?: string
- // asignaturaOrigenId?: string | null
- // }
- // clonTradicional?: {
- // archivoWordAsignaturaId: string | null
- // archivosAdicionalesIds: Array
- // }
iaConfig?: {
- descripcionEnfoqueAcademico: string;
- instruccionesAdicionalesIA: string;
- archivosReferencia: Array;
- repositoriosReferencia?: Array;
+ descripcionEnfoqueAcademico?: string;
+ instruccionesAdicionalesIA?: string;
+ archivosAdjuntos?: string[]; // rutas (no File)
};
- archivosAdjuntos?: Array;
};
diff --git a/supabase/functions/openai-webhook-responses/asignaturas/crear.ts b/supabase/functions/openai-webhook-responses/asignaturas/crear.ts
new file mode 100644
index 0000000..faa348d
--- /dev/null
+++ b/supabase/functions/openai-webhook-responses/asignaturas/crear.ts
@@ -0,0 +1,209 @@
+import type { OpenAI } from "openai";
+import { supabase } from "../supabase.ts";
+import type { Json } from "../../_shared/database.types.ts";
+import { ResponseMetadata } from "../../_shared/utils.ts";
+
+function extractOutputText(response: OpenAI.Responses.Response): string {
+ const direct = (response as unknown as { output_text?: unknown }).output_text;
+ if (typeof direct === "string") return direct;
+
+ const output = (response as unknown as { output?: unknown }).output;
+ if (!Array.isArray(output)) return "";
+
+ try {
+ return output
+ .filter((item) => (item as { type?: unknown })?.type === "message")
+ .flatMap((item) => (item as { content?: unknown })?.content ?? [])
+ .filter((c) => (c as { type?: unknown })?.type === "output_text")
+ .map((c) => String((c as { text?: unknown })?.text ?? ""))
+ .join("");
+ } catch {
+ return "";
+ }
+}
+
+function splitAiOutputStringsAndColumns(
+ aiOutput: unknown,
+): { aiOutputJson: Json; columnasGeneradas: Record } {
+ if (!aiOutput || typeof aiOutput !== "object" || Array.isArray(aiOutput)) {
+ return { aiOutputJson: aiOutput as unknown as Json, columnasGeneradas: {} };
+ }
+
+ const record = aiOutput as Record;
+ const stringsOnly: Record = {};
+ const columnasGeneradas: Record = {};
+
+ 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 };
+}
+
+async function marcarFalloAsignatura(
+ asignaturaId: string,
+ reason: string,
+ extra?: unknown,
+): Promise {
+ try {
+ const { data: existing, error: existingError } = await supabase
+ .from("asignaturas")
+ .select("meta_origen")
+ .eq("id", asignaturaId)
+ .maybeSingle();
+ if (existingError) {
+ console.warn("No se pudo leer meta_origen para marcar fallo", {
+ asignaturaId,
+ existingError,
+ });
+ }
+
+ const baseMeta = (existing?.meta_origen &&
+ typeof existing.meta_origen === "object" &&
+ !Array.isArray(existing.meta_origen))
+ ? (existing.meta_origen as Record)
+ : {};
+
+ const nextMeta: Record = {
+ ...baseMeta,
+ ai_error: {
+ reason,
+ extra: extra ?? null,
+ at: new Date().toISOString(),
+ },
+ };
+
+ const { error } = await supabase
+ .from("asignaturas")
+ .update({ estado: "borrador", meta_origen: nextMeta as unknown as Json })
+ .eq("id", asignaturaId);
+ if (error) {
+ console.warn("No se pudo marcar fallo en asignatura", {
+ asignaturaId,
+ error,
+ });
+ }
+ } catch (e) {
+ console.warn("Fallo inesperado marcando fallo en asignatura", {
+ asignaturaId,
+ e,
+ });
+ }
+}
+
+export async function handleCrearAsignaturaResponse(
+ response: OpenAI.Responses.Response,
+): Promise {
+ const metadata = response.metadata as ResponseMetadata | null;
+ const asignaturaId = metadata?.id;
+ if (!asignaturaId) {
+ console.warn("No se recibió metadata.id para actualizar la asignatura");
+ return;
+ }
+
+ try {
+ const outputText = extractOutputText(response);
+ if (!outputText) {
+ console.warn("La respuesta no contiene output_text");
+ await marcarFalloAsignatura(asignaturaId, "MISSING_OUTPUT_TEXT", {
+ responseId: response.id,
+ });
+ return;
+ }
+
+ let aiOutput: unknown;
+ try {
+ aiOutput = JSON.parse(outputText);
+ } catch (e) {
+ console.warn("No se pudo parsear JSON de la respuesta", e);
+ await marcarFalloAsignatura(asignaturaId, "INVALID_JSON", {
+ responseId: response.id,
+ outputText,
+ });
+ return;
+ }
+
+ const { aiOutputJson, columnasGeneradas } = splitAiOutputStringsAndColumns(
+ aiOutput,
+ );
+
+ const { data: existing, error: existingError } = await supabase
+ .from("asignaturas")
+ .select("meta_origen")
+ .eq("id", asignaturaId)
+ .maybeSingle();
+
+ if (existingError) {
+ console.warn("No se pudo leer meta_origen existente", {
+ asignaturaId,
+ existingError,
+ });
+ }
+
+ const baseMeta = (existing?.meta_origen &&
+ typeof existing.meta_origen === "object" &&
+ !Array.isArray(existing.meta_origen))
+ ? (existing.meta_origen as Record)
+ : {};
+
+ const nextMeta: Record = {
+ ...baseMeta,
+ ai: {
+ ...(typeof baseMeta.ai === "object" && baseMeta.ai &&
+ !Array.isArray(baseMeta.ai)
+ ? (baseMeta.ai as Record)
+ : {}),
+ responseId: response.id,
+ model: response.model,
+ },
+ };
+
+ const updatePatch: Record = {
+ datos: aiOutputJson,
+ estado: "borrador",
+ tipo_origen: "IA",
+ meta_origen: nextMeta 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)["x-column"];
+ const xDef = (value as Record)["x-definicion"];
+ if (typeof xColumn !== "string" || !xColumn.length) continue;
+ updatePatch[xColumn] = xDef as unknown as Json;
+ }
+
+ const { error: updateError } = await supabase
+ .from("asignaturas")
+ .update(
+ updatePatch as unknown as {
+ [k: string]: unknown;
+ },
+ )
+ .eq("id", asignaturaId);
+
+ if (updateError) {
+ console.warn("No se pudo actualizar asignatura con datos", {
+ asignaturaId,
+ updateError,
+ });
+ await marcarFalloAsignatura(asignaturaId, "SUPABASE_UPDATE_FAILED", {
+ updateError,
+ });
+ return;
+ }
+ } catch (e) {
+ console.warn("Fallo inesperado procesando asignatura", {
+ asignaturaId,
+ e,
+ });
+ await marcarFalloAsignatura(asignaturaId, "UNEXPECTED", { e });
+ return;
+ }
+}
diff --git a/supabase/functions/openai-webhook-responses/asignaturas/index.ts b/supabase/functions/openai-webhook-responses/asignaturas/index.ts
new file mode 100644
index 0000000..62e5337
--- /dev/null
+++ b/supabase/functions/openai-webhook-responses/asignaturas/index.ts
@@ -0,0 +1,25 @@
+import type { OpenAI } from "openai";
+import { ResponseMetadata } from "../../_shared/utils.ts";
+import { handleCrearAsignaturaResponse } from "./crear.ts";
+
+export async function handleAsignaturasResponse(
+ response: OpenAI.Responses.Response,
+): Promise {
+ const metadata = response.metadata as ResponseMetadata | null;
+ const accion = metadata?.accion;
+
+ if (!accion) {
+ console.warn("No se recibió metadata.accion para asignaturas");
+ return;
+ }
+
+ switch (accion) {
+ case "crear":
+ case "actualizar":
+ await handleCrearAsignaturaResponse(response);
+ return;
+ default:
+ console.warn("Acción no reconocida para asignaturas:", accion);
+ return;
+ }
+}
diff --git a/supabase/functions/openai-webhook-responses/index.ts b/supabase/functions/openai-webhook-responses/index.ts
index d004d0f..246fe78 100644
--- a/supabase/functions/openai-webhook-responses/index.ts
+++ b/supabase/functions/openai-webhook-responses/index.ts
@@ -7,6 +7,7 @@ import "@supabase/functions-js/edge-runtime.d.ts";
import OpenAI from "openai";
import { ResponseMetadata } from "../_shared/utils.ts";
import { handlePlanesEstudioResponse } from "./planes_estudio/index.ts";
+import { handleAsignaturasResponse } from "./asignaturas/index.ts";
console.log("Starting OpenAI webhook responses function");
const client = new OpenAI({
@@ -29,6 +30,9 @@ async function handleCompletedResponse(
case "planes_estudio":
await handlePlanesEstudioResponse(response);
break;
+ case "asignaturas":
+ await handleAsignaturasResponse(response);
+ break;
default:
console.warn("Tabla no reconocida:", metadata.tabla);
}
diff --git a/supabase/functions/tests/ai-generate-test-prototipo.ts b/supabase/functions/tests/ai-generate-test-prototipo.ts
index 1ab5425..4d68194 100644
--- a/supabase/functions/tests/ai-generate-test-prototipo.ts
+++ b/supabase/functions/tests/ai-generate-test-prototipo.ts
@@ -141,7 +141,7 @@ Deno.test("ai-structured (multipart + file)", async () => {
assert(typeof data.output?.resumen === "string");
assert(data.output.resumen.length > 0);
- // opcional: valida que registró upload(s) a storage
- assert(Array.isArray(data.references?.uploadedToStorage));
+ // opcional: valida que registró upload(s) a OpenAI files
+ assert(Array.isArray(data.references?.openaiFileIds));
});
*/
diff --git a/supabase/functions/tests/ai-structured-test.ts b/supabase/functions/tests/ai-structured-test.ts
index 0145f7d..d4d09ca 100644
--- a/supabase/functions/tests/ai-structured-test.ts
+++ b/supabase/functions/tests/ai-structured-test.ts
@@ -160,6 +160,6 @@ Deno.test({
assert(typeof data.output?.resumen === "string");
assert(data.output.resumen.length > 0);
- // opcional: valida que registró upload(s) a storage
- assert(Array.isArray(data.references?.uploadedToStorage));
+ // opcional: valida que registró upload(s) a OpenAI files
+ assert(Array.isArray(data.references?.openaiFileIds));
});