Files
genesis-2/supabase/functions/ai-structured/index.ts
T
alexrg c51281856a feat: update deno.json and deno.lock to include OpenAI and Zod imports; add sample PDF file
- Added imports for OpenAI and Zod in deno.json
- Updated deno.lock with new versions for OpenAI and Zod
- Introduced a sample PDF file demonstrating bookmark functionality
- Refactored ai-structured function to improve error handling and logging
- Updated tests to reflect changes in input structure and expected output
2026-01-19 16:12:17 -06:00

152 lines
4.3 KiB
TypeScript

// supabase/functions/ai-structured/index.ts
/// <reference lib="deno.window" />
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<string, unknown> & {
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<unknown>)
? 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,
);
}
});