// supabase/functions/ai-structured/index.ts /// 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 path = `${crypto.randomUUID()}-${(file.name)}`; 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: unknown; 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 }; 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, ); } } });