Add configuration files and update dependencies for Supabase functions
This commit is contained in:
Vendored
+36
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Attach V8 Inspector",
|
||||||
|
"type": "node",
|
||||||
|
"address": "127.0.0.1",
|
||||||
|
"request": "attach",
|
||||||
|
|
||||||
|
// This is important: You must enter the path where your edge functions
|
||||||
|
// folders are located.
|
||||||
|
"localRoot": "${workspaceFolder}/supabase/functions/",
|
||||||
|
|
||||||
|
// CLI mounts all functions under /home/deno/functions
|
||||||
|
"remoteRoot": "/home/deno/functions/",
|
||||||
|
|
||||||
|
// As you can see from the two options above, VsCode tries to locate the script
|
||||||
|
// paths being used by the inspector session locally by matching local and remote
|
||||||
|
// paths.
|
||||||
|
//
|
||||||
|
// If a valid path cannot be found on either side, or the script contents of
|
||||||
|
// those paths do not match each other, the breakpoint setting may not proceed
|
||||||
|
// correctly.
|
||||||
|
//
|
||||||
|
// We set the two options below for this.
|
||||||
|
"sourceMaps": true,
|
||||||
|
"enableContentValidation": false,
|
||||||
|
|
||||||
|
// Properties below are optional.
|
||||||
|
"restart": true,
|
||||||
|
"timeout": 1000000,
|
||||||
|
|
||||||
|
// Inspector is accepting sessions on 8083 by default.
|
||||||
|
"port": 8083
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
{
|
||||||
|
}
|
||||||
@@ -1,12 +1,53 @@
|
|||||||
{
|
{
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"specifiers": {
|
"specifiers": {
|
||||||
|
"jsr:@openai/openai@*": "6.16.0",
|
||||||
|
"jsr:@supabase/functions-js@*": "2.90.1",
|
||||||
"npm:@supabase/supabase-js@^2.90.1": "2.90.1",
|
"npm:@supabase/supabase-js@^2.90.1": "2.90.1",
|
||||||
"npm:@types/bun@^1.3.5": "1.3.5",
|
"npm:@types/bun@^1.3.5": "1.3.5",
|
||||||
|
"npm:deno@^2.6.4": "2.6.4",
|
||||||
"npm:supabase-js@^1.0.4": "1.0.4",
|
"npm:supabase-js@^1.0.4": "1.0.4",
|
||||||
"npm:supabase@^2.72.6": "2.72.6"
|
"npm:supabase@^2.72.6": "2.72.6"
|
||||||
},
|
},
|
||||||
|
"jsr": {
|
||||||
|
"@openai/openai@6.16.0": {
|
||||||
|
"integrity": "ccee548f61c382d715091fff0c2c3390a4487823644430b8235df543d6d6a78b"
|
||||||
|
},
|
||||||
|
"@supabase/functions-js@2.90.1": {
|
||||||
|
"integrity": "9a077ecf42aa84594ef3aac621d643e15ffaa9506a4c051e35c93f127ffdb035"
|
||||||
|
}
|
||||||
|
},
|
||||||
"npm": {
|
"npm": {
|
||||||
|
"@deno/darwin-arm64@2.6.4": {
|
||||||
|
"integrity": "sha512-5pocuV2IVWYUzomXze+bDSO86uX1WPxUn82BS7BrLI3ix8Eq7l8DhvYgz064w7rwCM4CiLLSc6MAQpAI+0gUag==",
|
||||||
|
"os": ["darwin"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"@deno/darwin-x64@2.6.4": {
|
||||||
|
"integrity": "sha512-tX7Fz47HcoXTYR4wN2WzXjqJtEPTftzjEknCYkZa8D4BzvpN7Zo1ra3Jd6bWPN9+T6mgTL6YMPMMSigXddIUcA==",
|
||||||
|
"os": ["darwin"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"@deno/linux-arm64-glibc@2.6.4": {
|
||||||
|
"integrity": "sha512-wY4hjjVLq5idClWj6e1de9IUZ0QZ1rxo16o0dwCJpMi+w1lolxtYxGF4y7pkcxAdZOpMGsXZuYMPq1Z2n51esA==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"@deno/linux-x64-glibc@2.6.4": {
|
||||||
|
"integrity": "sha512-H9b0WJw+eWyC6GcMGCXxC/3lz2kVveyWyUrzGyQ8hJmtDJpWiDnCU2iPqEpEcwXHgAUFFyjnRaFwedwwv5ApdA==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"@deno/win32-arm64@2.6.4": {
|
||||||
|
"integrity": "sha512-fMH1dx54ORbCTpMorGjDshwonIu6KfgfPC+Pq+14jeq/8604EIFXiA484p3Ov+ViZycndNHEPe8ZKPNd8xLjJA==",
|
||||||
|
"os": ["win32"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"@deno/win32-x64@2.6.4": {
|
||||||
|
"integrity": "sha512-hAxqNrBLHCINtqDgKcRiu8QtMbtdnHOVW3OFQjzA3gZEcpnGUK3JiF5ql++83SjuQH8ViZKGcdqkioLR7Bf14A==",
|
||||||
|
"os": ["win32"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
"@isaacs/fs-minipass@4.0.1": {
|
"@isaacs/fs-minipass@4.0.1": {
|
||||||
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
|
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@@ -112,6 +153,19 @@
|
|||||||
"ms"
|
"ms"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"deno@2.6.4": {
|
||||||
|
"integrity": "sha512-/nHUtrwoqUsjVTBWGqz3SjAbtBSfqCLb38YdkwstrO5qpQd3RC20dxsGYPPANy9RqMFJMfDyARL7RtOcUfatpw==",
|
||||||
|
"optionalDependencies": [
|
||||||
|
"@deno/darwin-arm64",
|
||||||
|
"@deno/darwin-x64",
|
||||||
|
"@deno/linux-arm64-glibc",
|
||||||
|
"@deno/linux-x64-glibc",
|
||||||
|
"@deno/win32-arm64",
|
||||||
|
"@deno/win32-x64"
|
||||||
|
],
|
||||||
|
"scripts": true,
|
||||||
|
"bin": true
|
||||||
|
},
|
||||||
"fetch-blob@3.2.0": {
|
"fetch-blob@3.2.0": {
|
||||||
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@@ -227,6 +281,7 @@
|
|||||||
"dependencies": [
|
"dependencies": [
|
||||||
"npm:@supabase/supabase-js@^2.90.1",
|
"npm:@supabase/supabase-js@^2.90.1",
|
||||||
"npm:@types/bun@^1.3.5",
|
"npm:@types/bun@^1.3.5",
|
||||||
|
"npm:deno@^2.6.4",
|
||||||
"npm:supabase-js@^1.0.4",
|
"npm:supabase-js@^1.0.4",
|
||||||
"npm:supabase@^2.72.6"
|
"npm:supabase@^2.72.6"
|
||||||
]
|
]
|
||||||
|
|||||||
Binary file not shown.
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@supabase/supabase-js": "^2.90.1",
|
"@supabase/supabase-js": "^2.90.1",
|
||||||
|
"deno": "^2.6.4",
|
||||||
"supabase": "^2.72.6",
|
"supabase": "^2.72.6",
|
||||||
"supabase-js": "^1.0.4"
|
"supabase-js": "^1.0.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// supabase/functions/ai-structured/index.ts
|
// supabase/functions/ai-structured/index.ts
|
||||||
import OpenAI from'jsr:@openai/openai';
|
import OpenAI from "npm:openai";
|
||||||
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'
|
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
||||||
import { createClient } from 'npm:@supabase/supabase-js@2'
|
import { createClient } from "npm:@supabase/supabase-js@2";
|
||||||
|
|
||||||
const corsHeaders = {
|
const corsHeaders = {
|
||||||
"Access-Control-Allow-Origin": "*",
|
"Access-Control-Allow-Origin": "*",
|
||||||
@@ -9,6 +9,8 @@ const corsHeaders = {
|
|||||||
"authorization, x-client-info, apikey, content-type",
|
"authorization, x-client-info, apikey, content-type",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MB = 1024 * 1024;
|
||||||
|
|
||||||
function json(body: unknown, status = 200) {
|
function json(body: unknown, status = 200) {
|
||||||
return new Response(JSON.stringify(body, null, 2), {
|
return new Response(JSON.stringify(body, null, 2), {
|
||||||
status,
|
status,
|
||||||
@@ -45,9 +47,12 @@ function normalizeInputWithOpenAIFileIds(input: any, fileIds: string[]) {
|
|||||||
if (!fileIds?.length) return input;
|
if (!fileIds?.length) return input;
|
||||||
|
|
||||||
// Responses API acepta input como string o array.
|
// Responses API acepta input como string o array.
|
||||||
const items = Array.isArray(input)
|
let items = Array.isArray(input) ? [...input] : [{
|
||||||
? [...input]
|
role: "user",
|
||||||
: [{ role: "user", content: input ?? "" }];
|
content: [
|
||||||
|
input ?? "",
|
||||||
|
],
|
||||||
|
}];
|
||||||
|
|
||||||
// Busca último mensaje "user"
|
// Busca último mensaje "user"
|
||||||
let idx = -1;
|
let idx = -1;
|
||||||
@@ -58,39 +63,72 @@ function normalizeInputWithOpenAIFileIds(input: any, fileIds: string[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (idx === -1) {
|
if (idx === -1) {
|
||||||
items.push({ role: "user", content: "" });
|
items.push({ role: "user", content: [] });
|
||||||
idx = items.length - 1;
|
idx = items.length - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const msg = items[idx];
|
const msg = items[idx];
|
||||||
const fileParts = fileIds.map((id) => ({ type: "input_file", file_id: id }));
|
const fileParts = fileIds.map((id) => ({ type: "input_file", file_id: id }));
|
||||||
|
|
||||||
|
// Normalize content to array format
|
||||||
if (typeof msg.content === "string") {
|
if (typeof msg.content === "string") {
|
||||||
msg.content = [{ type: "input_text", text: msg.content }, ...fileParts];
|
msg.content = [{ type: "input_text", text: msg.content }, ...fileParts];
|
||||||
} else if (Array.isArray(msg.content)) {
|
} else if (Array.isArray(msg.content)) {
|
||||||
msg.content = [...msg.content, ...fileParts];
|
msg.content = [...msg.content, ...fileParts];
|
||||||
} else {
|
} else {
|
||||||
// fallback raro
|
// fallback raro
|
||||||
msg.content = [{ type: "input_text", text: String(msg.content) }, ...fileParts];
|
msg.content = [
|
||||||
|
{ type: "input_text", text: String(msg.content) },
|
||||||
|
...fileParts,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
items[idx] = msg;
|
items[idx] = msg;
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeInputUser(input: any) {
|
||||||
|
if (typeof input === "string") {
|
||||||
|
return [{
|
||||||
|
role: "user",
|
||||||
|
content: [{
|
||||||
|
type: "input_text",
|
||||||
|
text: input,
|
||||||
|
}],
|
||||||
|
}];
|
||||||
|
} else if (Array.isArray(input)) {
|
||||||
|
return input;
|
||||||
|
} else {
|
||||||
|
return [{
|
||||||
|
role: "user",
|
||||||
|
content: [{
|
||||||
|
type: "input_text",
|
||||||
|
text: String(input),
|
||||||
|
}],
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Deno.serve(async (req) => {
|
Deno.serve(async (req) => {
|
||||||
if (req.method === "OPTIONS") return new Response(null, { status: 204, headers: corsHeaders });
|
if (req.method === "OPTIONS") {
|
||||||
|
return new Response(null, { status: 204, headers: corsHeaders });
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1) Auth requerido ✅
|
// 1) Auth requerido ✅
|
||||||
const authHeader =
|
const authHeader = req.headers.get("Authorization") ??
|
||||||
req.headers.get("Authorization") ?? req.headers.get("authorization");
|
req.headers.get("authorization");
|
||||||
if (!authHeader) return json({ ok: false, error: "Missing Authorization header" }, 401);
|
if (!authHeader) {
|
||||||
|
return json({ ok: false, error: "Missing Authorization header" }, 401);
|
||||||
|
}
|
||||||
|
|
||||||
const SUPABASE_URL = Deno.env.get("SUPABASE_URL");
|
const SUPABASE_URL = Deno.env.get("SUPABASE_URL");
|
||||||
const SUPABASE_ANON_KEY = Deno.env.get("SUPABASE_ANON_KEY");
|
const SUPABASE_ANON_KEY = Deno.env.get("SUPABASE_ANON_KEY");
|
||||||
if (!SUPABASE_URL || !SUPABASE_ANON_KEY) {
|
if (!SUPABASE_URL || !SUPABASE_ANON_KEY) {
|
||||||
return json({ ok: false, error: "Missing SUPABASE_URL / SUPABASE_ANON_KEY" }, 500);
|
return json({
|
||||||
|
ok: false,
|
||||||
|
error: "Missing SUPABASE_URL / SUPABASE_ANON_KEY",
|
||||||
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cliente con el JWT del usuario (para validar quién llama)
|
// Cliente con el JWT del usuario (para validar quién llama)
|
||||||
@@ -98,7 +136,8 @@ Deno.serve(async (req) => {
|
|||||||
global: { headers: { Authorization: authHeader } },
|
global: { headers: { Authorization: authHeader } },
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: userData, error: userErr } = await supabaseUser.auth.getUser();
|
const { data: userData, error: userErr } = await supabaseUser.auth
|
||||||
|
.getUser();
|
||||||
if (userErr || !userData?.user) {
|
if (userErr || !userData?.user) {
|
||||||
return json({ ok: false, error: "Invalid token" }, 401);
|
return json({ ok: false, error: "Invalid token" }, 401);
|
||||||
}
|
}
|
||||||
@@ -116,17 +155,32 @@ Deno.serve(async (req) => {
|
|||||||
let tempFiles: File[] = [];
|
let tempFiles: File[] = [];
|
||||||
|
|
||||||
if (contentType.includes("multipart/form-data")) {
|
if (contentType.includes("multipart/form-data")) {
|
||||||
const form = await req.formData();
|
let form;
|
||||||
const raw = form.get("payload")?.toString() ?? "{}";
|
try {
|
||||||
|
form = await req.formData();
|
||||||
|
} catch (error) {
|
||||||
|
return json({
|
||||||
|
ok: false,
|
||||||
|
error: "Invalid multipart/form-data body",
|
||||||
|
details: error,
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
|
const raw = form.get("payload") ?? "{}";
|
||||||
payload = JSON.parse(raw);
|
payload = JSON.parse(raw);
|
||||||
|
|
||||||
// Soporta nombres comunes
|
// Soporta nombres comunes
|
||||||
for (const key of ["files", "files[]", "archivos", "archivosTemporales"]) {
|
for (
|
||||||
for (const v of form.getAll(key)) if (v instanceof File) tempFiles.push(v);
|
const key of ["files", "files[]", "archivos", "archivosTemporales"]
|
||||||
|
) {
|
||||||
|
for (const v of form.getAll(key)) {
|
||||||
|
if (v instanceof File) tempFiles.push(v);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Fallback: toma cualquier File en el form
|
// Fallback: toma cualquier File en el form
|
||||||
if (tempFiles.length === 0) {
|
if (tempFiles.length === 0) {
|
||||||
for (const [, v] of form.entries()) if (v instanceof File) tempFiles.push(v);
|
for (const [, v] of form.entries()) {
|
||||||
|
if (v instanceof File) tempFiles.push(v);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
payload = await req.json();
|
payload = await req.json();
|
||||||
@@ -140,11 +194,24 @@ Deno.serve(async (req) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Conveniencia top-level
|
// Conveniencia top-level
|
||||||
if (payload.input !== undefined && responseCfg.input === undefined) responseCfg.input = payload.input;
|
if (payload.input !== undefined && responseCfg.input === undefined) {
|
||||||
|
responseCfg.input = payload.input;
|
||||||
|
}
|
||||||
|
// Normaliza input a array de mensajes
|
||||||
|
if (responseCfg.input !== undefined) {
|
||||||
|
responseCfg.input = normalizeInputUser(responseCfg.input);
|
||||||
|
}
|
||||||
if (payload.model && !responseCfg.model) responseCfg.model = payload.model;
|
if (payload.model && !responseCfg.model) responseCfg.model = payload.model;
|
||||||
if (payload.conversationId && responseCfg.conversation === undefined) responseCfg.conversation = payload.conversationId;
|
if (payload.conversationId && responseCfg.conversation === undefined) {
|
||||||
if (payload.previousResponseId && responseCfg.previous_response_id === undefined) responseCfg.previous_response_id = payload.previousResponseId;
|
responseCfg.conversation = payload.conversationId;
|
||||||
if (payload.reasoning && responseCfg.reasoning === undefined) responseCfg.reasoning = payload.reasoning;
|
}
|
||||||
|
if (
|
||||||
|
payload.previousResponseId &&
|
||||||
|
responseCfg.previous_response_id === undefined
|
||||||
|
) responseCfg.previous_response_id = payload.previousResponseId;
|
||||||
|
if (payload.reasoning && responseCfg.reasoning === undefined) {
|
||||||
|
responseCfg.reasoning = payload.reasoning;
|
||||||
|
}
|
||||||
|
|
||||||
// Defaults
|
// Defaults
|
||||||
responseCfg.model = responseCfg.model ?? "gpt-5";
|
responseCfg.model = responseCfg.model ?? "gpt-5";
|
||||||
@@ -152,21 +219,32 @@ Deno.serve(async (req) => {
|
|||||||
// Regla API: no mezclar conversation + previous_response_id
|
// Regla API: no mezclar conversation + previous_response_id
|
||||||
if (responseCfg.conversation && responseCfg.previous_response_id) {
|
if (responseCfg.conversation && responseCfg.previous_response_id) {
|
||||||
return json(
|
return json(
|
||||||
{ ok: false, error: "Do not send both `conversation` and `previous_response_id`." },
|
{
|
||||||
400
|
ok: false,
|
||||||
|
error: "Do not send both `conversation` and `previous_response_id`.",
|
||||||
|
},
|
||||||
|
400,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4) Structured Outputs (JSON Schema) ✅
|
// 4) Structured Outputs (JSON Schema) ✅
|
||||||
// Espera payload.structured (o payload.outputSchema) como objeto o string JSON
|
// Espera payload.structured (o payload.outputSchema) como objeto o string JSON
|
||||||
let structured = payload.structured ?? payload.outputSchema;
|
let structured = payload.structured ?? payload.outputSchema;
|
||||||
if (!structured) return json({ ok: false, error: "Missing `structured` (JSON schema format)." }, 400);
|
if (!structured) {
|
||||||
|
return json({
|
||||||
|
ok: false,
|
||||||
|
error: "Missing `structured` (JSON schema format).",
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof structured === "string") {
|
if (typeof structured === "string") {
|
||||||
try {
|
try {
|
||||||
structured = JSON.parse(structured);
|
structured = JSON.parse(structured);
|
||||||
} catch {
|
} catch {
|
||||||
return json({ ok: false, error: "`structured` is a string but not valid JSON." }, 400);
|
return json({
|
||||||
|
ok: false,
|
||||||
|
error: "`structured` is a string but not valid JSON.",
|
||||||
|
}, 400);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,43 +255,60 @@ Deno.serve(async (req) => {
|
|||||||
if (format.type === "json_schema") {
|
if (format.type === "json_schema") {
|
||||||
format.name = format.name ?? "structured_output";
|
format.name = format.name ?? "structured_output";
|
||||||
format.strict = format.strict ?? true;
|
format.strict = format.strict ?? true;
|
||||||
if (!format.schema) return json({ ok: false, error: "structured.schema is required." }, 400);
|
if (!format.schema) {
|
||||||
|
return json(
|
||||||
|
{ ok: false, error: "structured.schema is required." },
|
||||||
|
400,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
responseCfg.text = { ...(responseCfg.text ?? {}), format };
|
responseCfg.text = { ...(responseCfg.text ?? {}), format };
|
||||||
|
|
||||||
// 5) Referencias (3 tipos) 📎
|
// 5) Referencias (3 tipos) 📎
|
||||||
const references = payload.references ?? {};
|
const references = payload.references ?? {};
|
||||||
const openaiFileIds: string[] =
|
const openaiFileIds: string[] = references.openaiFileIds ??
|
||||||
references.openaiFileIds ?? payload.openaiFileIds ?? [];
|
payload.openaiFileIds ?? [];
|
||||||
const vectorStoreIds: string[] =
|
const vectorStoreIds: string[] = references.vectorStoreIds ??
|
||||||
references.vectorStoreIds ?? payload.vectorStoreIds ?? [];
|
payload.vectorStoreIds ?? [];
|
||||||
|
|
||||||
// 5a) Vector stores => file_search tool
|
// 5a) Vector stores => file_search tool
|
||||||
const tools: any[] = Array.isArray(responseCfg.tools) ? [...responseCfg.tools] : [];
|
const tools: any[] = Array.isArray(responseCfg.tools)
|
||||||
|
? [...responseCfg.tools]
|
||||||
|
: [];
|
||||||
if (vectorStoreIds.length) {
|
if (vectorStoreIds.length) {
|
||||||
tools.push({ type: "file_search", vector_store_ids: vectorStoreIds });
|
tools.push({ type: "file_search", vector_store_ids: vectorStoreIds });
|
||||||
|
|
||||||
// Si quieres ver resultados de búsqueda en la respuesta
|
// Si quieres ver resultados de búsqueda en la respuesta
|
||||||
const include = Array.isArray(responseCfg.include) ? responseCfg.include : [];
|
const include = Array.isArray(responseCfg.include)
|
||||||
if (!include.includes("file_search_call.results")) include.push("file_search_call.results");
|
? responseCfg.include
|
||||||
|
: [];
|
||||||
|
if (!include.includes("file_search_call.results")) {
|
||||||
|
include.push("file_search_call.results");
|
||||||
|
}
|
||||||
responseCfg.include = include;
|
responseCfg.include = include;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5b) MCP opcional
|
// 5b) MCP opcional
|
||||||
const usarMCP: boolean = Boolean(payload.usarMCP ?? references.usarMCP ?? payload.mcp?.enabled);
|
const usarMCP: boolean = Boolean(
|
||||||
|
payload.usarMCP ?? references.usarMCP ?? payload.mcp?.enabled,
|
||||||
|
);
|
||||||
if (usarMCP) {
|
if (usarMCP) {
|
||||||
const mcp = payload.mcp ?? {};
|
const mcp = payload.mcp ?? {};
|
||||||
const server_url = mcp.server_url ?? Deno.env.get("MCP_SERVER_URL");
|
const server_url = mcp.server_url ?? Deno.env.get("MCP_SERVER_URL");
|
||||||
if (!server_url) {
|
if (!server_url) {
|
||||||
return json(
|
return json(
|
||||||
{ ok: false, error: "usarMCP=true pero falta MCP_SERVER_URL o mcp.server_url" },
|
{
|
||||||
400
|
ok: false,
|
||||||
|
error: "usarMCP=true pero falta MCP_SERVER_URL o mcp.server_url",
|
||||||
|
},
|
||||||
|
400,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
tools.push({
|
tools.push({
|
||||||
type: "mcp",
|
type: "mcp",
|
||||||
server_label: mcp.server_label ?? Deno.env.get("MCP_SERVER_LABEL") ?? "supabase",
|
server_label: mcp.server_label ?? Deno.env.get("MCP_SERVER_LABEL") ??
|
||||||
|
"supabase",
|
||||||
server_description: mcp.server_description ?? "Supabase MCP server",
|
server_description: mcp.server_description ?? "Supabase MCP server",
|
||||||
server_url,
|
server_url,
|
||||||
require_approval: mcp.require_approval ?? "never",
|
require_approval: mcp.require_approval ?? "never",
|
||||||
@@ -226,7 +321,10 @@ 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) {
|
||||||
responseCfg.input = normalizeInputWithOpenAIFileIds(responseCfg.input, openaiFileIds);
|
responseCfg.input = normalizeInputWithOpenAIFileIds(
|
||||||
|
responseCfg.input,
|
||||||
|
openaiFileIds,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6) Archivos temporales => subir a Supabase Storage (NO a OpenAI) ☁️
|
// 6) Archivos temporales => subir a Supabase Storage (NO a OpenAI) ☁️
|
||||||
@@ -239,19 +337,38 @@ Deno.serve(async (req) => {
|
|||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
if (tempFiles.length) {
|
if (tempFiles.length) {
|
||||||
const bucket = payload.storage?.bucket ?? Deno.env.get("AI_FILES_BUCKET") ?? "ai-temp";
|
const bucket = payload.storage?.bucket ??
|
||||||
const prefix = payload.storage?.prefix ?? Deno.env.get("AI_FILES_PREFIX") ?? "tmp";
|
Deno.env.get("AI_FILES_BUCKET") ?? "ai-temp";
|
||||||
|
const prefix = payload.storage?.prefix ??
|
||||||
|
Deno.env.get("AI_FILES_PREFIX") ?? "tmp";
|
||||||
|
|
||||||
for (const f of tempFiles) {
|
for (const f of tempFiles) {
|
||||||
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}`;
|
||||||
|
|
||||||
|
const bytes = new Uint8Array(await f.arrayBuffer());
|
||||||
const { data, error } = await supabaseAdmin.storage
|
const { data, error } = await supabaseAdmin.storage
|
||||||
.from(bucket)
|
.from(bucket)
|
||||||
.upload(path, f, { contentType: f.type, upsert: false });
|
.upload(path, bytes, { contentType: f.type, upsert: false });
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return json({ ok: false, error: `Storage upload failed: ${error.message}`, file: f.name }, 500);
|
return json({
|
||||||
|
ok: false,
|
||||||
|
error: `Storage upload failed: ${error.message}`,
|
||||||
|
file: f.name,
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
// Add as an input_file base64 reference to the last user message content
|
||||||
|
const fileBase64 = btoa(String.fromCharCode(...bytes));
|
||||||
|
if (Array.isArray(responseCfg.input) && responseCfg.input.length > 0) {
|
||||||
|
const lastMsg = responseCfg.input[responseCfg.input.length - 1];
|
||||||
|
if (lastMsg?.role === "user" && Array.isArray(lastMsg.content)) {
|
||||||
|
lastMsg.content.push({
|
||||||
|
type: "input_file",
|
||||||
|
filename: f.name,
|
||||||
|
file_data: `data:${f.type};base64,${fileBase64}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadedToStorage.push({
|
uploadedToStorage.push({
|
||||||
@@ -266,19 +383,22 @@ Deno.serve(async (req) => {
|
|||||||
|
|
||||||
// 7) Llamada a OpenAI Responses API 🚀
|
// 7) Llamada a OpenAI Responses API 🚀
|
||||||
const OPENAI_API_KEY = Deno.env.get("OPENAI_API_KEY");
|
const OPENAI_API_KEY = Deno.env.get("OPENAI_API_KEY");
|
||||||
if (!OPENAI_API_KEY) return json({ ok: false, error: "Missing OPENAI_API_KEY" }, 500);
|
if (!OPENAI_API_KEY) {
|
||||||
|
return json({ ok: false, error: "Missing OPENAI_API_KEY" }, 500);
|
||||||
|
}
|
||||||
|
|
||||||
const openai = new OpenAI({ apiKey: OPENAI_API_KEY });
|
const openai = new OpenAI({ apiKey: OPENAI_API_KEY });
|
||||||
let resp = null;
|
let resp = null;
|
||||||
try {
|
try {
|
||||||
|
|
||||||
resp = await openai.responses.create(responseCfg);
|
resp = await openai.responses.create(responseCfg);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return json({ ok: false, error: "OpenAI API error", details: (error as Error)?.message ?? String(error) }, 500);
|
return json({
|
||||||
|
ok: false,
|
||||||
|
error: "OpenAI API error",
|
||||||
|
details: (error as Error)?.message ?? String(error),
|
||||||
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const outputText = extractOutputText(resp);
|
const outputText = extractOutputText(resp);
|
||||||
let output: unknown = null;
|
let output: unknown = null;
|
||||||
let outputParseError: string | null = null;
|
let outputParseError: string | null = null;
|
||||||
@@ -288,12 +408,10 @@ Deno.serve(async (req) => {
|
|||||||
outputParseError = (e as Error)?.message ?? String(e);
|
outputParseError = (e as Error)?.message ?? String(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
const conv =
|
const conv = resp?.conversation?.id ??
|
||||||
resp?.conversation?.id ??
|
|
||||||
(typeof resp?.conversation === "string"
|
(typeof resp?.conversation === "string"
|
||||||
? resp.conversation
|
? resp.conversation
|
||||||
: responseCfg.conversation ?? null)
|
: responseCfg.conversation ?? null);
|
||||||
|
|
||||||
|
|
||||||
return json({
|
return json({
|
||||||
ok: outputParseError ? false : true,
|
ok: outputParseError ? false : true,
|
||||||
@@ -311,7 +429,8 @@ Deno.serve(async (req) => {
|
|||||||
openaiFileIds,
|
openaiFileIds,
|
||||||
vectorStoreIds,
|
vectorStoreIds,
|
||||||
// Si quieres mandar tus UUID internos de archivos, aquí van (solo eco)
|
// Si quieres mandar tus UUID internos de archivos, aquí van (solo eco)
|
||||||
archivosReferenciaIds: references.archivosReferenciaIds ?? payload.archivosReferenciaIds ?? payload.archivosReferencia ?? [],
|
archivosReferenciaIds: references.archivosReferenciaIds ??
|
||||||
|
payload.archivosReferenciaIds ?? payload.archivosReferencia ?? [],
|
||||||
},
|
},
|
||||||
|
|
||||||
tools: sanitizeTools(responseCfg.tools),
|
tools: sanitizeTools(responseCfg.tools),
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { supabase } from "./new-user";
|
||||||
|
|
||||||
|
const file = Bun.file("files/carta.pdf");
|
||||||
|
|
||||||
|
await supabase.auth.signInWithPassword({
|
||||||
|
email: "guillermo.arrieta@lasalle.mx",
|
||||||
|
password: "admin",
|
||||||
|
});
|
||||||
|
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append("payload", JSON.stringify({
|
||||||
|
response: { input: "Resume estos documentos en JSON.", model: "gpt-5" },
|
||||||
|
structured: { type: "json_schema", name: "resumen", strict: true, schema: {
|
||||||
|
type: "object", properties: { resumen: { type: "string" } }, required: ["resumen"], additionalProperties: false
|
||||||
|
} },
|
||||||
|
storage: { prefix: "tmp" },
|
||||||
|
}));
|
||||||
|
fd.append("files", file);
|
||||||
|
|
||||||
|
const response = await supabase.functions.invoke("ai-structured", {
|
||||||
|
method: "POST",
|
||||||
|
body: fd,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log({ d: response.data, e: response.error });
|
||||||
Reference in New Issue
Block a user