// supabase/functions/ai-structured/index.ts /// import OpenAI from "npm:openai"; import type * as OpenAITypes from "npm:openai/types/types.js"; 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: Record & { model: string; input: Array<{ role: string; content: unknown }>; }; files: File[]; }; const fd = await req.formData(); // Asume siempre vienen: options, files[] const optionsRaw = fd.get("options"); const input: 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: ${input.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 input.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 input.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 = 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(input.options?.input as Array) ? input.options.input : []; inputArr.push({ role: "user", content: [ ...fileParts, { type: "input_text", text: "usa estos archivos como referencia" }, ], }); input.options.input = inputArr; console.log("Prepared OpenAI options with file references"); // 4) Call Responses API const openai_raw = await openai.responses.create(input.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: input.options, // opciones usadas }; return json(response); } catch (e) { return json( { ok: false, error: e?.message ?? String(e), // si fue Zod, normalmente trae detalles en `issues` issues: (e as any)?.issues, }, 400, ); } });