Files
genesis-2/supabase/functions/ai-structured/index.ts
T
2026-01-20 13:25:53 -06:00

167 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// supabase/functions/ai-structured/index.ts
/// <reference lib="deno.window" />
import OpenAI from "openai";
import type * as OpenAITypes from "openai";
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
import { createClient } from "npm:@supabase/supabase-js@2";
const json = (body: unknown, status = 200) =>
new Response(JSON.stringify(body), {
status,
headers: {
"content-type": "application/json; charset=utf-8",
"access-control-allow-origin": "*",
"access-control-allow-headers": "*",
},
});
Deno.serve(async (req) => {
if (req.method === "OPTIONS") return json({ ok: true });
if (req.method !== "POST") return json({ error: "Method not allowed" }, 405);
console.log("Received request");
try {
const SUPABASE_URL = Deno.env.get("SUPABASE_URL") ?? "";
const SUPABASE_SERVICE_ROLE_KEY =
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "";
const SUPABASE_BUCKET = Deno.env.get("SUPABASE_BUCKET") ?? "ai-storage";
const OPENAI_API_KEY = Deno.env.get("OPENAI_API_KEY") ?? "";
if (!SUPABASE_URL || !SUPABASE_SERVICE_ROLE_KEY || !OPENAI_API_KEY) {
return json({ error: "Missing env vars" }, 500);
}
console.log("Env vars loaded");
type InputForm = {
options: OpenAITypes.OpenAI.Responses.ResponseCreateParams;
files: File[];
};
const fd = await req.formData();
// Asume siempre vienen: options, files[]
const optionsRaw = fd.get("options");
const inputForm: InputForm = {
options: typeof optionsRaw === "string"
? JSON.parse(optionsRaw)
: optionsRaw,
files: fd.getAll("files").filter((x): x is File => x instanceof File),
};
console.log(`Parsed form data: ${inputForm.files.length} files`);
const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY);
const openai = new OpenAI({ apiKey: OPENAI_API_KEY });
console.log("Parsed options");
// 1) Upload a Supabase Storage
const supabase_paths: string[] = [];
for (const file of inputForm.files ?? []) {
const safeName = file.name
.normalize("NFD") // 1. Descompone letras de tildes (í -> i + ´)
.replace(/[\u0300-\u036f]/g, "") // 2. Borra las tildes
.replace(/[^a-zA-Z0-9.-]/g, "_"); // 3. Reemplaza espacios y símbolos raros por "_"
const path = `${crypto.randomUUID()}-${safeName}`;
const { data, error } = await supabase.storage
.from(SUPABASE_BUCKET)
.upload(path, file, {
contentType: file.type || "application/octet-stream",
upsert: false,
/* metadata */
});
if (error) throw new Error(`Supabase upload failed: ${error.message}`);
supabase_paths.push(data.path);
}
console.log("Uploaded files to Supabase Storage");
// 2) Upload a OpenAI Files
const openai_file_ids: string[] = [];
for (const file of inputForm.files ?? []) {
const created = await openai.files.create({
file,
purpose: "user_data",
});
openai_file_ids.push(created.id);
}
console.log("Uploaded files to OpenAI Files");
// 3) Inject file_ids into options.input by appending a new user message
const fileParts: OpenAITypes.OpenAI.Responses.ResponseInputFile[] =
openai_file_ids.map((id) => ({
type: "input_file",
file_id: id,
}));
// Asume que SIEMPRE hay input; de todas formas, cae bien si no.
const inputArr = Array.isArray(inputForm.options?.input)
? inputForm.options.input
: [];
inputArr.push({
role: "user",
content: [
...fileParts,
{ type: "input_text", text: "usa estos archivos como referencia" },
],
});
inputForm.options.input = inputArr;
console.log("Prepared OpenAI options with file references");
// 4) Call Responses API
const openai_raw = await openai.responses.create(inputForm.options);
console.log("Received response from OpenAI Responses API");
type SuccessResponse = {
ok: true;
openai_raw: OpenAITypes.OpenAI.Responses.Response;
supabase_paths: string[];
openai_file_ids: string[];
options: unknown;
};
const response: SuccessResponse = {
ok: true,
openai_raw, // cruda
supabase_paths, // rutas en el bucket
openai_file_ids, // IDs en OpenAI
options: inputForm.options, // opciones usadas
};
response.openai_raw.output.
return json(response);
} catch (e) {
if (e instanceof Error) {
console.error("Error occurred:", e.message);
return json(
{
ok: false,
error: e?.message ?? String(e),
// si fue Zod, normalmente trae detalles en `issues`
issues: (e as any)?.issues,
},
400,
);
} else {
console.error("Unknown error occurred:", e);
return json(
{
ok: false,
error: "An unknown error occurred",
},
500,
);
}
}
});