Primera version de ai-generate-plan

This commit is contained in:
2026-01-16 16:58:43 -06:00
parent 37b0777e42
commit 241684d0d1
2 changed files with 156 additions and 21 deletions
+119 -21
View File
@@ -8,6 +8,7 @@ import { corsHeaders } from "../_shared/cors.ts";
import { createClient } from "npm:@supabase/supabase-js@2";
import type { AIGeneratePlanInput } from "./types.ts";
import { z } from "zod";
import { systemPrompt } from "./prompts.ts";
Deno.serve(async (req) => {
if (req.method === "OPTIONS") {
@@ -74,7 +75,7 @@ Deno.serve(async (req) => {
},
});
// TODO: iniciar sesion con el usuario
// TODO: iniciar sesion con el usuario e implementar RLS
const SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY");
if (!SERVICE_ROLE_KEY) {
@@ -104,22 +105,6 @@ Deno.serve(async (req) => {
const payload: AIGeneratePlanInput = validation.data;
const respuestaSubida: string[] = subirAStorage(
supabaseService,
payload.archivosAdjuntos,
"ai-storage",
"tmp",
);
const { data: carrera, error: carreraError } = await supabaseService
.from("carreras")
.select("id,nombre,facultad_id,facultades(id,nombre,nombre_corto)")
.eq("id", payload.datosBasicos.carreraId)
.maybeSingle();
if (carreraError) {
throw new Error("Error fetching carrera: " + carreraError.message);
}
const { data: estructuraPlan, error: estructuraPlanError } =
await supabaseService
.from("estructuras_plan")
@@ -132,8 +117,124 @@ Deno.serve(async (req) => {
);
}
const userPrompt =
`Genera un borrador completo del PLAN DE ESTUDIOS con base en lo siguiente:
- Descripción del enfoque: ${payload.iaConfig.descripcionEnfoque}
- Notas adicionales: ${payload.iaConfig.notasAdicionales ?? "Ninguna"}`;
const aiStructuredPayload = {
response: {
input: [
{ role: "system", content: systemPrompt },
{ role: "user", content: userPrompt },
],
},
estructuraPlan,
references: {
openaiFileIds: payload.iaConfig.archivosReferencia,
vectorStoreIds: payload.iaConfig.repositoriosIds,
},
usarMCP: Boolean(payload.iaConfig?.usarMCP),
};
const aiStructuredFormData = new FormData();
aiStructuredFormData.append("payload", JSON.stringify(aiStructuredPayload));
for (const file of payload.archivosAdjuntos ?? []) {
aiStructuredFormData.append("archivos", file);
}
const { data: aiJson, error } = await supabaseService.functions.invoke(
"ai-structured",
{
method: "POST",
body: aiStructuredFormData,
},
);
if (error) {
throw new Error(
"ai-structured invocation failed: " + error.message,
);
}
if (!aiJson || !aiJson.ok) {
throw new Error(
"ai-structured returned an error or no data",
);
}
// TODO: Insertar interacciones con IA y quizas forzar a que la informacion de datosBasicos sea la misma que la recibida
const { data: estado } = await supabaseService
.from("estados_plan")
.select("id,clave,orden")
.ilike("clave", "BORRADOR%")
.order("orden", { ascending: true })
.limit(1)
.maybeSingle();
const { data: carrera, error: carreraError } = await supabaseService
.from("carreras")
.select("id,nombre,facultad_id,facultades(id,nombre,nombre_corto)")
.eq("id", payload.datosBasicos.carreraId)
.maybeSingle();
if (carreraError) {
throw new Error("Error fetching carrera: " + carreraError.message);
}
const { data: plan, error: planError } = await supabaseService
.from("planes_estudio")
.insert({
carrera_id: carrera?.id,
estructura_id: estructuraPlan?.id,
nombre: payload.datosBasicos.nombrePlan,
nivel: payload.datosBasicos.nivel,
tipo_ciclo: payload.datosBasicos.tipoCiclo,
numero_ciclos: payload.datosBasicos.numCiclos,
datos: aiJson.output,
estado_actual_id: estado?.id,
activo: true,
tipo_origen: "IA",
meta_origen: {
generado_por: "ai_generate_plan",
ai_structured: {
cid: aiJson?.cid ?? null,
responseId: aiJson?.responseId ?? null,
conversationId: aiJson?.conversationId ?? null,
model: aiJson?.model,
usage: aiJson?.usage ?? null,
},
referencias: {
archivosReferenciaIds: payload.iaConfig?.archivosReferencia ?? null,
repositoriosIds: payload.iaConfig?.repositoriosIds ?? null,
// openaiFileIds,
// vectorStoreIds,
},
iaConfig: {
descripcionEnfoque: payload.iaConfig?.descripcionEnfoque ?? null,
notasAdicionales: payload.iaConfig?.notasAdicionales ?? null,
usarMCP: Boolean(payload.iaConfig?.usarMCP),
},
},
// creado_por: user.id,
// actualizado_por: user.id,
})
.select(
"id,nombre,nivel,tipo_ciclo,numero_ciclos,carrera_id,estructura_id,estado_actual_id,activo,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,datos",
)
.single();
if (planError) {
throw new Error("Error inserting plan: " + planError.message);
}
// TODO: update a interaccion_ia y e insert a cambios_plancon id de plan generado
const jsonResponse = {
ok: true,
plan,
};
return new Response(
JSON.stringify({ message: "Multipart/form-data received" }),
JSON.stringify(jsonResponse),
{
headers: { ...corsHeaders, "Content-Type": "application/json" },
status: 200,
@@ -284,6 +385,3 @@ function parseAndValidate(
return { success: true, data: finalInput };
}
async function subirAStorage(supabase: supa) {
}