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:
@@ -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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
*/
|
|
||||||
|
|||||||
Reference in New Issue
Block a user