fix: update database queries to use 'template' instead of 'template_id' and adjust related references in ai_generate_plan function

This commit is contained in:
2026-01-16 14:51:42 -06:00
parent 6c2faa97f1
commit 1d5d4b6a4e
3 changed files with 212 additions and 90 deletions
+150 -42
View File
@@ -126,12 +126,16 @@ function safePreview(val: unknown, maxLen = 1200) {
if (typeof v === "string") { if (typeof v === "string") {
if (v.length > 500) return v.slice(0, 500) + "…[TRUNCATED]"; if (v.length > 500) return v.slice(0, 500) + "…[TRUNCATED]";
// evita imprimir base64/data URLs // evita imprimir base64/data URLs
if (v.startsWith("data:") && v.includes(";base64,")) return "[DATA_URL_REDACTED]"; if (v.startsWith("data:") && v.includes(";base64,")) {
return "[DATA_URL_REDACTED]";
}
} }
return v; return v;
}); });
if (!jsonStr) return String(val); if (!jsonStr) return String(val);
return jsonStr.length > maxLen ? jsonStr.slice(0, maxLen) + "…[TRUNCATED]" : jsonStr; return jsonStr.length > maxLen
? jsonStr.slice(0, maxLen) + "…[TRUNCATED]"
: jsonStr;
} catch { } catch {
return String(val); return String(val);
} }
@@ -187,13 +191,17 @@ function toolsSummary(tools: any[]) {
return arr.map((t) => { return arr.map((t) => {
if (!t) return t; if (!t) return t;
const base: any = { type: t.type }; const base: any = { type: t.type };
if (t.type === "file_search") base.vector_store_ids_count = (t.vector_store_ids ?? []).length; if (t.type === "file_search") {
base.vector_store_ids_count = (t.vector_store_ids ?? []).length;
}
if (t.type === "mcp") { if (t.type === "mcp") {
base.server_label = t.server_label ?? null; base.server_label = t.server_label ?? null;
base.server_url = t.server_url ? "[SET]" : null; base.server_url = t.server_url ? "[SET]" : null;
base.require_approval = t.require_approval ?? null; base.require_approval = t.require_approval ?? null;
base.authorization = t.authorization ? "[REDACTED]" : null; base.authorization = t.authorization ? "[REDACTED]" : null;
base.allowed_tools_count = Array.isArray(t.allowed_tools) ? t.allowed_tools.length : null; base.allowed_tools_count = Array.isArray(t.allowed_tools)
? t.allowed_tools.length
: null;
} }
return base; return base;
}); });
@@ -207,7 +215,10 @@ Deno.serve(async (req) => {
const log = (msg: string, extra?: unknown) => { const log = (msg: string, extra?: unknown) => {
if (extra !== undefined) { if (extra !== undefined) {
console.log(`[${nowIso()}][ai-structured][${cid}] ${msg}`, safePreview(extra)); console.log(
`[${nowIso()}][ai-structured][${cid}] ${msg}`,
safePreview(extra),
);
} else { } else {
console.log(`[${nowIso()}][ai-structured][${cid}] ${msg}`); console.log(`[${nowIso()}][ai-structured][${cid}] ${msg}`);
} }
@@ -215,7 +226,10 @@ Deno.serve(async (req) => {
const warn = (msg: string, extra?: unknown) => { const warn = (msg: string, extra?: unknown) => {
if (extra !== undefined) { if (extra !== undefined) {
console.warn(`[${nowIso()}][ai-structured][${cid}] ⚠️ ${msg}`, safePreview(extra)); console.warn(
`[${nowIso()}][ai-structured][${cid}] ⚠️ ${msg}`,
safePreview(extra),
);
} else { } else {
console.warn(`[${nowIso()}][ai-structured][${cid}] ⚠️ ${msg}`); console.warn(`[${nowIso()}][ai-structured][${cid}] ⚠️ ${msg}`);
} }
@@ -223,7 +237,10 @@ Deno.serve(async (req) => {
const errlog = (msg: string, extra?: unknown) => { const errlog = (msg: string, extra?: unknown) => {
if (extra !== undefined) { if (extra !== undefined) {
console.error(`[${nowIso()}][ai-structured][${cid}] ❌ ${msg}`, safePreview(extra)); console.error(
`[${nowIso()}][ai-structured][${cid}] ❌ ${msg}`,
safePreview(extra),
);
} else { } else {
console.error(`[${nowIso()}][ai-structured][${cid}] ❌ ${msg}`); console.error(`[${nowIso()}][ai-structured][${cid}] ❌ ${msg}`);
} }
@@ -247,11 +264,17 @@ Deno.serve(async (req) => {
const authHeaderRaw = req.headers.get("Authorization") ?? const authHeaderRaw = req.headers.get("Authorization") ??
req.headers.get("authorization"); req.headers.get("authorization");
log("Authorization header present?", { present: Boolean(authHeaderRaw), value: redactAuthHeader(authHeaderRaw) }); log("Authorization header present?", {
present: Boolean(authHeaderRaw),
value: redactAuthHeader(authHeaderRaw),
});
if (!authHeaderRaw) { if (!authHeaderRaw) {
warn("Missing Authorization header"); warn("Missing Authorization header");
return json({ ok: false, error: "Missing Authorization header", cid }, 401); return json(
{ ok: false, error: "Missing Authorization header", cid },
401,
);
} }
const SUPABASE_URL = Deno.env.get("SUPABASE_URL"); const SUPABASE_URL = Deno.env.get("SUPABASE_URL");
@@ -275,8 +298,11 @@ Deno.serve(async (req) => {
}); });
const tAuth = performance.now(); const tAuth = performance.now();
const { data: userData, error: userErr } = await supabaseUser.auth.getUser(); const { data: userData, error: userErr } = await supabaseUser.auth
log("Supabase auth.getUser() done ⏱️", { ms: Math.round(performance.now() - tAuth) }); .getUser();
log("Supabase auth.getUser() done ⏱️", {
ms: Math.round(performance.now() - tAuth),
});
if (userErr || !userData?.user) { if (userErr || !userData?.user) {
warn("Invalid token", { userErr: userErr?.message ?? userErr ?? null }); warn("Invalid token", { userErr: userErr?.message ?? userErr ?? null });
@@ -284,10 +310,15 @@ Deno.serve(async (req) => {
} }
const user = userData.user; const user = userData.user;
log("Authenticated user ✅", { userId: user.id, email: user.email ?? null }); log("Authenticated user ✅", {
userId: user.id,
email: user.email ?? null,
});
const SERVICE_ROLE = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY"); const SERVICE_ROLE = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY");
log("Env presence (service role) 🗝️", { hasSERVICE_ROLE: Boolean(SERVICE_ROLE) }); log("Env presence (service role) 🗝️", {
hasSERVICE_ROLE: Boolean(SERVICE_ROLE),
});
const supabaseAdmin = SERVICE_ROLE const supabaseAdmin = SERVICE_ROLE
? createClient(SUPABASE_URL, SERVICE_ROLE) ? createClient(SUPABASE_URL, SERVICE_ROLE)
@@ -343,9 +374,13 @@ Deno.serve(async (req) => {
}, 400); }, 400);
} }
for (const key of ["files", "files[]", "archivos", "archivosTemporales"]) { for (
const key of ["files", "files[]", "archivos", "archivosTemporales"]
) {
const vals = form.getAll(key); const vals = form.getAll(key);
if (vals?.length) log(`Found multipart field "${key}"`, { count: vals.length }); if (vals?.length) {
log(`Found multipart field "${key}"`, { count: vals.length });
}
for (const v of vals) { for (const v of vals) {
if (v instanceof File) tempFiles.push(v); if (v instanceof File) tempFiles.push(v);
} }
@@ -361,19 +396,29 @@ Deno.serve(async (req) => {
log("Multipart files detected 📎", { log("Multipart files detected 📎", {
count: tempFiles.length, count: tempFiles.length,
files: tempFiles.map((f) => ({ name: f.name, type: f.type, size: f.size })), files: tempFiles.map((f) => ({
name: f.name,
type: f.type,
size: f.size,
})),
}); });
} else { } else {
log("Parsing JSON body 🧾"); log("Parsing JSON body 🧾");
try { try {
payload = await req.json(); payload = await req.json();
} catch (e) { } catch (e) {
errlog("Invalid JSON body", { error: (e as Error)?.message ?? String(e) }); errlog("Invalid JSON body", {
error: (e as Error)?.message ?? String(e),
});
return json({ ok: false, error: "Invalid JSON body", cid }, 400); return json({ ok: false, error: "Invalid JSON body", cid }, 400);
} }
} }
log("Body parsed ⏱️", { ms: Math.round(performance.now() - tParse) }); log("Body parsed ⏱️", { ms: Math.round(performance.now() - tParse) });
log("Payload top-level keys", { keys: Object.keys(payload ?? {}), hasOpenAI: Boolean(payload?.openai), hasResponse: Boolean(payload?.response) }); log("Payload top-level keys", {
keys: Object.keys(payload ?? {}),
hasOpenAI: Boolean(payload?.openai),
hasResponse: Boolean(payload?.response),
});
// 3) Construye config final para OpenAI (passthrough + overrides) 🧠 // 3) Construye config final para OpenAI (passthrough + overrides) 🧠
log("Step 3: Build responseCfg 🧠"); log("Step 3: Build responseCfg 🧠");
@@ -389,7 +434,10 @@ Deno.serve(async (req) => {
if (responseCfg.input !== undefined) { if (responseCfg.input !== undefined) {
responseCfg.input = normalizeInputUser(responseCfg.input); responseCfg.input = normalizeInputUser(responseCfg.input);
log("responseCfg.input normalized ✅", summarizeInputShape(responseCfg.input)); log(
"responseCfg.input normalized ✅",
summarizeInputShape(responseCfg.input),
);
} else { } else {
warn("responseCfg.input is undefined (is that expected?)"); warn("responseCfg.input is undefined (is that expected?)");
} }
@@ -406,7 +454,7 @@ Deno.serve(async (req) => {
responseCfg.reasoning = payload.reasoning; responseCfg.reasoning = payload.reasoning;
} }
responseCfg.model = responseCfg.model ?? "gpt-5"; responseCfg.model = responseCfg.model ?? "gpt-5-nano";
log("OpenAI config summary 🤖", { log("OpenAI config summary 🤖", {
model: responseCfg.model, model: responseCfg.model,
hasConversation: Boolean(responseCfg.conversation), hasConversation: Boolean(responseCfg.conversation),
@@ -445,11 +493,15 @@ Deno.serve(async (req) => {
} }
if (typeof structured === "string") { if (typeof structured === "string") {
log("Parsing structured from string 🧩", { preview: structured.slice(0, 200) }); log("Parsing structured from string 🧩", {
preview: structured.slice(0, 200),
});
try { try {
structured = JSON.parse(structured); structured = JSON.parse(structured);
} catch (e) { } catch (e) {
errlog("`structured` string is not valid JSON", { error: (e as Error)?.message ?? String(e) }); errlog("`structured` string is not valid JSON", {
error: (e as Error)?.message ?? String(e),
});
return json({ return json({
ok: false, ok: false,
error: "`structured` is a string but not valid JSON.", error: "`structured` is a string but not valid JSON.",
@@ -486,7 +538,9 @@ Deno.serve(async (req) => {
log("References summary", { log("References summary", {
openaiFileIdsCount: openaiFileIds.length, openaiFileIdsCount: openaiFileIds.length,
vectorStoreIdsCount: vectorStoreIds.length, vectorStoreIdsCount: vectorStoreIds.length,
usarMCP: Boolean(payload.usarMCP ?? references.usarMCP ?? payload.mcp?.enabled), usarMCP: Boolean(
payload.usarMCP ?? references.usarMCP ?? payload.mcp?.enabled,
),
}); });
// 5a) Vector stores => file_search tool // 5a) Vector stores => file_search tool
@@ -506,7 +560,10 @@ Deno.serve(async (req) => {
} }
responseCfg.include = include; responseCfg.include = include;
log("Added file_search tool 🗂️", { vectorStoreIdsCount: vectorStoreIds.length, include: responseCfg.include }); log("Added file_search tool 🗂️", {
vectorStoreIdsCount: vectorStoreIds.length,
include: responseCfg.include,
});
} }
// 5b) MCP opcional // 5b) MCP opcional
@@ -518,8 +575,11 @@ Deno.serve(async (req) => {
const server_url = mcp.server_url ?? Deno.env.get("MCP_SERVER_URL"); const server_url = mcp.server_url ?? Deno.env.get("MCP_SERVER_URL");
log("MCP config presence", { log("MCP config presence", {
hasServerUrl: Boolean(server_url), hasServerUrl: Boolean(server_url),
hasAuthorization: Boolean(mcp.authorization ?? Deno.env.get("MCP_AUTHORIZATION")), hasAuthorization: Boolean(
server_label: mcp.server_label ?? Deno.env.get("MCP_SERVER_LABEL") ?? "supabase", mcp.authorization ?? Deno.env.get("MCP_AUTHORIZATION"),
),
server_label: mcp.server_label ?? Deno.env.get("MCP_SERVER_LABEL") ??
"supabase",
}); });
if (!server_url) { if (!server_url) {
@@ -553,12 +613,17 @@ Deno.serve(async (req) => {
// 5c) OpenAI file IDs => se insertan al input como input_file // 5c) OpenAI file IDs => se insertan al input como input_file
if (openaiFileIds.length) { if (openaiFileIds.length) {
log("Injecting OpenAI file_ids into input 🧷", { count: openaiFileIds.length }); log("Injecting OpenAI file_ids into input 🧷", {
count: openaiFileIds.length,
});
responseCfg.input = normalizeInputWithOpenAIFileIds( responseCfg.input = normalizeInputWithOpenAIFileIds(
responseCfg.input, responseCfg.input,
openaiFileIds, openaiFileIds,
); );
log("Input after OpenAI file_ids injection", summarizeInputShape(responseCfg.input)); log(
"Input after OpenAI file_ids injection",
summarizeInputShape(responseCfg.input),
);
} }
// 6) Archivos temporales => subir a Supabase Storage (NO a OpenAI) ☁️ // 6) Archivos temporales => subir a Supabase Storage (NO a OpenAI) ☁️
@@ -583,28 +648,43 @@ Deno.serve(async (req) => {
const safeName = (f.name ?? "file").replaceAll(/[^a-zA-Z0-9._-]/g, "_"); const safeName = (f.name ?? "file").replaceAll(/[^a-zA-Z0-9._-]/g, "_");
const path = `${prefix}/${user.id}/${crypto.randomUUID()}-${safeName}`; const path = `${prefix}/${user.id}/${crypto.randomUUID()}-${safeName}`;
log("Uploading file to storage ⬆️", { name: f.name, type: f.type, size: f.size, path }); log("Uploading file to storage ⬆️", {
name: f.name,
type: f.type,
size: f.size,
path,
});
const tUp = performance.now(); const tUp = performance.now();
const bytes = new Uint8Array(await f.arrayBuffer()); const bytes = new Uint8Array(await f.arrayBuffer());
// warning útil: btoa + spread puede tronarse con archivos grandes // warning útil: btoa + spread puede tronarse con archivos grandes
if (bytes.length > 2 * MB) { if (bytes.length > 2 * MB) {
warn("Large file detected: base64 building might be heavy (btoa + spread) 🧨", { warn(
"Large file detected: base64 building might be heavy (btoa + spread) 🧨",
{
name: f.name, name: f.name,
bytes: bytes.length, bytes: bytes.length,
approxBase64Chars: Math.ceil(bytes.length * 4 / 3), approxBase64Chars: Math.ceil(bytes.length * 4 / 3),
}); },
);
} }
const { data, error } = await supabaseAdmin.storage const { data, error } = await supabaseAdmin.storage
.from(bucket) .from(bucket)
.upload(path, bytes, { contentType: f.type, upsert: false }); .upload(path, bytes, { contentType: f.type, upsert: false });
log("Storage upload done ⏱️", { ms: Math.round(performance.now() - tUp) }); log("Storage upload done ⏱️", {
ms: Math.round(performance.now() - tUp),
});
if (error) { if (error) {
errlog("Storage upload failed", { message: error.message, file: f.name, bucket, path }); errlog("Storage upload failed", {
message: error.message,
file: f.name,
bucket,
path,
});
return json({ return json({
ok: false, ok: false,
error: `Storage upload failed: ${error.message}`, error: `Storage upload failed: ${error.message}`,
@@ -616,7 +696,10 @@ Deno.serve(async (req) => {
// Add as an input_file base64 reference to the last user message content // Add as an input_file base64 reference to the last user message content
const tB64 = performance.now(); const tB64 = performance.now();
const fileBase64 = btoa(String.fromCharCode(...bytes)); // ⚠️ potencialmente pesado const fileBase64 = btoa(String.fromCharCode(...bytes)); // ⚠️ potencialmente pesado
log("Base64 built ⏱️", { ms: Math.round(performance.now() - tB64), base64Chars: fileBase64.length }); log("Base64 built ⏱️", {
ms: Math.round(performance.now() - tB64),
base64Chars: fileBase64.length,
});
if (Array.isArray(responseCfg.input) && responseCfg.input.length > 0) { if (Array.isArray(responseCfg.input) && responseCfg.input.length > 0) {
const lastMsg = responseCfg.input[responseCfg.input.length - 1]; const lastMsg = responseCfg.input[responseCfg.input.length - 1];
@@ -633,13 +716,19 @@ Deno.serve(async (req) => {
// no imprimimos file_data // no imprimimos file_data
}); });
} else { } else {
warn("Could not append input_file: last message is not user/content array", { warn(
"Could not append input_file: last message is not user/content array",
{
lastRole: lastMsg?.role ?? null, lastRole: lastMsg?.role ?? null,
lastContentType: typeof lastMsg?.content, lastContentType: typeof lastMsg?.content,
}); },
);
} }
} else { } else {
warn("responseCfg.input not array or empty; cannot append file_data", summarizeInputShape(responseCfg.input)); warn(
"responseCfg.input not array or empty; cannot append file_data",
summarizeInputShape(responseCfg.input),
);
} }
uploadedToStorage.push({ uploadedToStorage.push({
@@ -657,7 +746,9 @@ Deno.serve(async (req) => {
// 7) Llamada a OpenAI Responses API 🚀 // 7) Llamada a OpenAI Responses API 🚀
log("Step 7: OpenAI call 🚀"); log("Step 7: OpenAI call 🚀");
const OPENAI_API_KEY = Deno.env.get("OPENAI_API_KEY"); const OPENAI_API_KEY = Deno.env.get("OPENAI_API_KEY");
log("Env presence (OpenAI) 🔑", { hasOPENAI_API_KEY: Boolean(OPENAI_API_KEY) }); log("Env presence (OpenAI) 🔑", {
hasOPENAI_API_KEY: Boolean(OPENAI_API_KEY),
});
if (!OPENAI_API_KEY) { if (!OPENAI_API_KEY) {
errlog("Missing OPENAI_API_KEY"); errlog("Missing OPENAI_API_KEY");
@@ -710,7 +801,9 @@ Deno.serve(async (req) => {
const tParseOut = performance.now(); const tParseOut = performance.now();
try { try {
output = JSON.parse(outputText); output = JSON.parse(outputText);
log("Output JSON parsed ✅", { ms: Math.round(performance.now() - tParseOut) }); log("Output JSON parsed ✅", {
ms: Math.round(performance.now() - tParseOut),
});
} catch (e) { } catch (e) {
outputParseError = (e as Error)?.message ?? String(e); outputParseError = (e as Error)?.message ?? String(e);
warn("Output JSON parse FAILED 🧯", { warn("Output JSON parse FAILED 🧯", {
@@ -731,7 +824,7 @@ Deno.serve(async (req) => {
totalMs: Math.round(performance.now() - t0), totalMs: Math.round(performance.now() - t0),
}); });
return json({ const response = json({
ok: outputParseError ? false : true, ok: outputParseError ? false : true,
cid, // 👈 útil para correlacionar logs cid, // 👈 útil para correlacionar logs
responseId: resp.id, responseId: resp.id,
@@ -750,15 +843,30 @@ Deno.serve(async (req) => {
archivosReferenciaIds: references.archivosReferenciaIds ?? archivosReferenciaIds: references.archivosReferenciaIds ??
payload.archivosReferenciaIds ?? payload.archivosReferencia ?? [], payload.archivosReferenciaIds ?? payload.archivosReferencia ?? [],
}, },
tools: sanitizeTools(responseCfg.tools), tools: sanitizeTools(responseCfg.tools),
}); });
// Log final response
log("Response payload preview:", {
references: {
uploadedToStorage,
openaiFileIds,
vectorStoreIds,
archivosReferenciaIds: references.archivosReferenciaIds ??
payload.archivosReferenciaIds ?? payload.archivosReferencia ?? [],
},
});
return response;
} catch (e) { } catch (e) {
errlog("Unhandled error 💥", { errlog("Unhandled error 💥", {
message: (e as Error)?.message ?? String(e), message: (e as Error)?.message ?? String(e),
stack: (e as Error)?.stack ?? null, stack: (e as Error)?.stack ?? null,
totalMs: Math.round(performance.now() - t0), totalMs: Math.round(performance.now() - t0),
}); });
return json({ ok: false, error: (e as Error)?.message ?? String(e), cid }, 500); return json(
{ ok: false, error: (e as Error)?.message ?? String(e), cid },
500,
);
} }
}); });
+16 -16
View File
@@ -324,7 +324,7 @@ Deno.serve(async (req) => {
log("Using estructuraId from payload", { estructuraIdFromPayload }); log("Using estructuraId from payload", { estructuraIdFromPayload });
const { data, error } = await supabaseAdmin const { data, error } = await supabaseAdmin
.from("estructuras_plan") .from("estructuras_plan")
.select("id,nombre,tipo,template_id,definicion") .select("id,nombre,tipo,template,definicion")
.eq("id", estructuraIdFromPayload) .eq("id", estructuraIdFromPayload)
.single(); .single();
if (error) { if (error) {
@@ -336,7 +336,7 @@ Deno.serve(async (req) => {
// default: la más reciente tipo CURRICULAR, si no existe, la más reciente en general // default: la más reciente tipo CURRICULAR, si no existe, la más reciente en general
const { data, error } = await supabaseAdmin const { data, error } = await supabaseAdmin
.from("estructuras_plan") .from("estructuras_plan")
.select("id,nombre,tipo,template_id,definicion") .select("id,nombre,tipo,template,definicion")
.eq("tipo", "CURRICULAR") .eq("tipo", "CURRICULAR")
.order("actualizado_en", { ascending: false }) .order("actualizado_en", { ascending: false })
.limit(1) .limit(1)
@@ -350,7 +350,7 @@ Deno.serve(async (req) => {
if (!estructura) { if (!estructura) {
const { data: anyE, error: anyErr } = await supabaseAdmin const { data: anyE, error: anyErr } = await supabaseAdmin
.from("estructuras_plan") .from("estructuras_plan")
.select("id,nombre,tipo,template_id,definicion") .select("id,nombre,tipo,template,definicion")
.order("actualizado_en", { ascending: false }) .order("actualizado_en", { ascending: false })
.limit(1) .limit(1)
.maybeSingle(); .maybeSingle();
@@ -367,7 +367,7 @@ Deno.serve(async (req) => {
estructuraId: estructura?.id ?? null, estructuraId: estructura?.id ?? null,
nombre: estructura?.nombre ?? null, nombre: estructura?.nombre ?? null,
tipo: estructura?.tipo ?? null, tipo: estructura?.tipo ?? null,
template_id: estructura?.template_id ?? null, template: estructura?.template ?? null,
definicionKeys: estructura?.definicion ? Object.keys(estructura.definicion) : null, definicionKeys: estructura?.definicion ? Object.keys(estructura.definicion) : null,
}); });
@@ -378,15 +378,15 @@ Deno.serve(async (req) => {
// 6) Resolver referencias (archivos / repositorios) 📎 // 6) Resolver referencias (archivos / repositorios) 📎
log("Step 6: Resolve references 📎"); log("Step 6: Resolve references 📎");
const archivosReferenciaIds: string[] = Array.isArray(iaConfig?.archivosReferencia) const uploadedToStorage: string[] = Array.isArray(iaConfig?.uploadedToStorage)
? iaConfig.archivosReferencia ? iaConfig.uploadedToStorage
: []; : [];
const repositoriosIds: string[] = Array.isArray(iaConfig?.repositoriosIds) const repositoriosIds: string[] = Array.isArray(iaConfig?.repositoriosIds)
? iaConfig.repositoriosIds ? iaConfig.repositoriosIds
: []; : [];
log("Reference IDs received", { log("Reference IDs received", {
archivosReferenciaIdsCount: archivosReferenciaIds.length, uploadedToStorageCount: uploadedToStorage.length,
repositoriosIdsCount: repositoriosIds.length, repositoriosIdsCount: repositoriosIds.length,
usarMCP: Boolean(iaConfig?.usarMCP), usarMCP: Boolean(iaConfig?.usarMCP),
}); });
@@ -394,11 +394,11 @@ Deno.serve(async (req) => {
const archivosInfo: any[] = []; const archivosInfo: any[] = [];
const vectorInfo: any[] = []; const vectorInfo: any[] = [];
if (archivosReferenciaIds.length) { if (uploadedToStorage.length) {
const { data, error } = await supabaseAdmin const { data, error } = await supabaseAdmin
.from("archivos") .from("archivos")
.select("id,nombre,mime_type,openai_file_id,ruta_storage") .select("id,nombre,mime_type,openai_file_id,ruta_storage")
.in("id", archivosReferenciaIds); .in("id", uploadedToStorage);
if (error) warn("archivos lookup failed (continuing)", { message: error.message }); if (error) warn("archivos lookup failed (continuing)", { message: error.message });
for (const a of data ?? []) archivosInfo.push(a); for (const a of data ?? []) archivosInfo.push(a);
} }
@@ -481,7 +481,7 @@ Deno.serve(async (req) => {
references: { references: {
openaiFileIds, openaiFileIds,
vectorStoreIds, vectorStoreIds,
archivosReferenciaIds, uploadedToStorage,
}, },
usarMCP: Boolean(iaConfig?.usarMCP), usarMCP: Boolean(iaConfig?.usarMCP),
}; };
@@ -544,14 +544,14 @@ Deno.serve(async (req) => {
prompt: { prompt: {
datosBasicos, datosBasicos,
iaConfig, iaConfig,
estructura: { id: estructura.id, nombre: estructura.nombre, template_id: estructura.template_id, tipo: estructura.tipo }, estructura: { id: estructura.id, nombre: estructura.nombre, template: estructura.template, tipo: estructura.tipo },
request: aiStructuredBody, request: aiStructuredBody,
computed, computed,
}, },
respuesta: aiJson, respuesta: aiJson,
aceptada: false, aceptada: false,
conversacion_id: aiJson?.conversationId ?? null, conversacion_id: aiJson?.conversationId ?? null,
ids_archivos: archivosReferenciaIds, ids_archivos: uploadedToStorage,
ids_vector_store: repositoriosIds, ids_vector_store: repositoriosIds,
rutas_storage: rutasStorage, rutas_storage: rutasStorage,
}) })
@@ -596,7 +596,7 @@ Deno.serve(async (req) => {
estructura: { estructura: {
id: estructura.id, id: estructura.id,
nombre: estructura.nombre, nombre: estructura.nombre,
template_id: estructura.template_id, template: estructura.template,
tipo: estructura.tipo, tipo: estructura.tipo,
}, },
request: aiStructuredBody, request: aiStructuredBody,
@@ -605,7 +605,7 @@ Deno.serve(async (req) => {
respuesta: aiJson, respuesta: aiJson,
aceptada: true, // se está usando para crear el plan aceptada: true, // se está usando para crear el plan
conversacion_id: aiJson?.conversationId ?? null, conversacion_id: aiJson?.conversationId ?? null,
ids_archivos: archivosReferenciaIds, ids_archivos: uploadedToStorage,
ids_vector_store: repositoriosIds, ids_vector_store: repositoriosIds,
rutas_storage: rutasStorage, rutas_storage: rutasStorage,
}) })
@@ -672,7 +672,7 @@ Deno.serve(async (req) => {
usage: aiJson?.usage ?? null, usage: aiJson?.usage ?? null,
}, },
referencias: { referencias: {
archivosReferenciaIds, uploadedToStorage,
repositoriosIds, repositoriosIds,
openaiFileIds, openaiFileIds,
vectorStoreIds, vectorStoreIds,
@@ -761,7 +761,7 @@ Deno.serve(async (req) => {
usage: aiJson?.usage ?? null, usage: aiJson?.usage ?? null,
}, },
references: { references: {
archivosReferenciaIds, uploadedToStorage,
repositoriosIds, repositoriosIds,
openaiFileIds, openaiFileIds,
vectorStoreIds, vectorStoreIds,
@@ -53,7 +53,9 @@ Deno.test("ai_generate_plan creates plan and returns structured data", async ()
.select("id") .select("id")
.limit(1) .limit(1)
.single(); .single();
if (carreraError) throw new Error("Failed to fetch carrera: " + carreraError.message); if (carreraError) {
throw new Error("Failed to fetch carrera: " + carreraError.message);
}
if (!carrera) throw new Error("No carrera found"); if (!carrera) throw new Error("No carrera found");
const datosBasicos = { const datosBasicos = {
@@ -94,38 +96,49 @@ Deno.test("ai_generate_plan creates plan and returns structured data", async ()
); );
assertEquals(data.plan_datos.duracion_del_ciclo_escolar, "16"); // Semestre -> 16 assertEquals(data.plan_datos.duracion_del_ciclo_escolar, "16"); // Semestre -> 16
}); });
/*
Deno.test("ai-structured (multipart + file)", async () => { Deno.test("ai_generate_plan (multipart with files)", async () => {
const { client, accessToken } = await getAuthedClient(); const { client, accessToken } = await getAuthedClient();
// Lee un PDF local (ajusta path) const { data: carrera, error: carreraError } = await client
const bytes = await Deno.readFile("files/carta.pdf"); .from("carreras")
const file = new File([bytes], "carta.pdf", { type: "application/pdf" }); .select("id")
.limit(1)
.single();
if (carreraError) {
throw new Error("Failed to fetch carrera: " + carreraError.message);
}
if (!carrera) throw new Error("No carrera found");
const datosBasicos = {
nombrePlan: "Plan de estudios generado con referencias",
carreraId: carrera.id,
nivel: "Maestría",
tipoCiclo: "Semestre",
numCiclos: 4,
};
const iaConfig = {
descripcionEnfoque: "Programa de postgrado enfocado en tecnología",
notasAdicionales: "Considerar tendencias actuales en IA",
};
// Create a simple test file
const fileContent = "Sample curriculum reference document";
const file = new File([fileContent], "reference.txt", { type: "text/plain" });
const payload = { const payload = {
response: { input: "Resume estos documentos en JSON.", model: "gpt-5" }, datosBasicos,
structured: { iaConfig,
type: "json_schema",
name: "resumen",
strict: true,
schema: {
type: "object",
properties: { resumen: { type: "string" } },
required: ["resumen"],
additionalProperties: false,
},
},
storage: { prefix: "tmp" },
}; };
const fd = new FormData(); const fd = new FormData();
fd.append("payload", JSON.stringify(payload)); fd.append("payload", JSON.stringify(payload));
fd.append("files", file); fd.append("files", file);
const { data, error } = await client.functions.invoke("ai-structured", { const { data, error } = await client.functions.invoke("ai_generate_plan", {
headers: { headers: {
Authorization: `Bearer ${accessToken}`, // 👈 necesario Authorization: `Bearer ${accessToken}`,
// NO pongas Content-Type: fetch lo setea con boundary para FormData ✅
}, },
method: "POST", method: "POST",
body: fd, body: fd,
@@ -135,10 +148,11 @@ Deno.test("ai-structured (multipart + file)", async () => {
assert(data, "Expected data from function"); assert(data, "Expected data from function");
assertEquals(data.ok, true); assertEquals(data.ok, true);
assert(typeof data.output?.resumen === "string"); assert(data.plan);
assert(data.output.resumen.length > 0); assertEquals(data.plan.nombre, datosBasicos.nombrePlan);
assertEquals(data.plan.nivel, "Maestría");
assertEquals(data.plan.numero_ciclos, datosBasicos.numCiclos);
// opcional: valida que registró upload(s) a storage assert(data.references);
assert(Array.isArray(data.references?.uploadedToStorage)); assertEquals(data.references.tempFilesCount, 1);
}); });
*/