Add initial project files including .gitignore, README, package.json, and TypeScript configuration
- Created .gitignore to exclude unnecessary files - Added README.md with installation and usage instructions - Initialized package.json with dependencies and configurations - Set up tsconfig.json for TypeScript compiler options - Implemented basic index.ts file for logging - Added Supabase function structure in ai-structured directory - Created test files for user authentication and function invocation
This commit is contained in:
+34
@@ -0,0 +1,34 @@
|
||||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
@@ -0,0 +1,15 @@
|
||||
# genesis-2
|
||||
|
||||
To install dependencies:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
To run:
|
||||
|
||||
```bash
|
||||
bun run index.ts
|
||||
```
|
||||
|
||||
This project was created using `bun init` in bun v1.3.0. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
||||
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^2.90.1",
|
||||
"supabase": "^2.72.6",
|
||||
"supabase-js": "^1.0.4",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
|
||||
|
||||
"@supabase/auth-js": ["@supabase/auth-js@2.90.1", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-vxb66dgo6h3yyPbR06735Ps+dK3hj0JwS8w9fdQPVZQmocSTlKUW5MfxSy99mN0XqCCuLMQ3jCEiIIUU23e9ng=="],
|
||||
|
||||
"@supabase/functions-js": ["@supabase/functions-js@2.90.1", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-x9mV9dF1Lam9qL3zlpP6mSM5C9iqMPtF5B/tU1Jj/F0ufX5mjDf9ghVBaErVxmrQJRL4+iMKWKY2GnODkpS8tw=="],
|
||||
|
||||
"@supabase/postgrest-js": ["@supabase/postgrest-js@2.90.1", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-jh6vqzaYzoFn3raaC0hcFt9h+Bt+uxNRBSdc7PfToQeRGk7PDPoweHsbdiPWREtDVTGKfu+PyPW9e2jbK+BCgQ=="],
|
||||
|
||||
"@supabase/realtime-js": ["@supabase/realtime-js@2.90.1", "", { "dependencies": { "@types/phoenix": "^1.6.6", "@types/ws": "^8.18.1", "tslib": "2.8.1", "ws": "^8.18.2" } }, "sha512-PWbnEMkcQRuor8jhObp4+Snufkq8C6fBp+MchVp2qBPY1NXk/c3Iv3YyiFYVzo0Dzuw4nAlT4+ahuPggy4r32w=="],
|
||||
|
||||
"@supabase/storage-js": ["@supabase/storage-js@2.90.1", "", { "dependencies": { "iceberg-js": "^0.8.1", "tslib": "2.8.1" } }, "sha512-GHY+Ps/K/RBfRj7kwx+iVf2HIdqOS43rM2iDOIDpapyUnGA9CCBFzFV/XvfzznGykd//z2dkGZhlZZprsVFqGg=="],
|
||||
|
||||
"@supabase/supabase-js": ["@supabase/supabase-js@2.90.1", "", { "dependencies": { "@supabase/auth-js": "2.90.1", "@supabase/functions-js": "2.90.1", "@supabase/postgrest-js": "2.90.1", "@supabase/realtime-js": "2.90.1", "@supabase/storage-js": "2.90.1" } }, "sha512-U8KaKGLUgTIFHtwEW1dgw1gK7XrdpvvYo7nzzqPx721GqPe8WZbAiLh/hmyKLGBYQ/mmQNr20vU9tWSDZpii3w=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
|
||||
|
||||
"@types/node": ["@types/node@25.0.6", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-NNu0sjyNxpoiW3YuVFfNz7mxSQ+S4X2G28uqg2s+CzoqoQjLPsWSbsFFyztIAqt2vb8kfEAsJNepMGPTxFDx3Q=="],
|
||||
|
||||
"@types/phoenix": ["@types/phoenix@1.6.7", "", {}, "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q=="],
|
||||
|
||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||
|
||||
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||
|
||||
"bin-links": ["bin-links@6.0.0", "", { "dependencies": { "cmd-shim": "^8.0.0", "npm-normalize-package-bin": "^5.0.0", "proc-log": "^6.0.0", "read-cmd-shim": "^6.0.0", "write-file-atomic": "^7.0.0" } }, "sha512-X4CiKlcV2GjnCMwnKAfbVWpHa++65th9TuzAEYtZoATiOE2DQKhSp4CJlyLoTqdhBKlXjpXjCTYPNNFS33Fi6w=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
|
||||
|
||||
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
|
||||
|
||||
"cmd-shim": ["cmd-shim@8.0.0", "", {}, "sha512-Jk/BK6NCapZ58BKUxlSI+ouKRbjH1NLZCgJkYoab+vEHUY3f6OzpNBN9u7HFSv9J6TRDGs4PLOHezoKGaFRSCA=="],
|
||||
|
||||
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
|
||||
|
||||
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
|
||||
|
||||
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||
|
||||
"iceberg-js": ["iceberg-js@0.8.1", "", {}, "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA=="],
|
||||
|
||||
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||
|
||||
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||
|
||||
"minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
|
||||
|
||||
"node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
||||
|
||||
"npm-normalize-package-bin": ["npm-normalize-package-bin@5.0.0", "", {}, "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag=="],
|
||||
|
||||
"proc-log": ["proc-log@6.1.0", "", {}, "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ=="],
|
||||
|
||||
"read-cmd-shim": ["read-cmd-shim@6.0.0", "", {}, "sha512-1zM5HuOfagXCBWMN83fuFI/x+T/UhZ7k+KIzhrHXcQoeX5+7gmaDYjELQHmmzIodumBHeByBJT4QYS7ufAgs7A=="],
|
||||
|
||||
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
||||
"supabase": ["supabase@2.72.6", "", { "dependencies": { "bin-links": "^6.0.0", "https-proxy-agent": "^7.0.2", "node-fetch": "^3.3.2", "tar": "7.5.2" }, "bin": { "supabase": "bin/supabase" } }, "sha512-axO1MkZgwMIxj78vKiGosnuXwzOaQq0Kz0I8jheNMwJH9dKGf3q/arivYT44z8u3t5FjGUxQWOuQoJetzcmm/A=="],
|
||||
|
||||
"supabase-js": ["supabase-js@1.0.4", "", {}, "sha512-b9Pxxl3ACEUhY/qTyF7WG9yUQTQQ/5Qm/0KnqbBOQdMaKi13glxs0SzqB1LSJnk2mZvufm6FF8Pyv5ZMc5YVqw=="],
|
||||
|
||||
"tar": ["tar@7.5.2", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
|
||||
|
||||
"write-file-atomic": ["write-file-atomic@7.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" } }, "sha512-YnlPC6JqnZl6aO4uRc+dx5PHguiR9S6WeoLtpxNT9wIG+BDya7ZNE1q7KOjVgaA73hKhKLpVPgJ5QA9THQ5BRg=="],
|
||||
|
||||
"ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
|
||||
|
||||
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^2.90.1",
|
||||
"supabase": "^2.72.6",
|
||||
"supabase-js": "^1.0.4"
|
||||
},
|
||||
"name": "genesis-2",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
main
|
||||
@@ -0,0 +1 @@
|
||||
v2.67.1
|
||||
@@ -0,0 +1 @@
|
||||
verify_jwt = false
|
||||
@@ -0,0 +1,312 @@
|
||||
// supabase/functions/ai-structured/index.ts
|
||||
import OpenAI from "jsr:@openai/openai";
|
||||
import { createClient } from "jsr:@supabase/supabase-js";
|
||||
|
||||
const corsHeaders = {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Headers":
|
||||
"authorization, x-client-info, apikey, content-type",
|
||||
};
|
||||
|
||||
function json(body: unknown, status = 200) {
|
||||
return new Response(JSON.stringify(body, null, 2), {
|
||||
status,
|
||||
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
function extractOutputText(resp: any): string {
|
||||
if (typeof resp?.output_text === "string") return resp.output_text;
|
||||
|
||||
const texts: string[] = [];
|
||||
for (const item of resp?.output ?? []) {
|
||||
if (item?.type === "message") {
|
||||
for (const part of item?.content ?? []) {
|
||||
if (part?.type === "output_text" && typeof part?.text === "string") {
|
||||
texts.push(part.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return texts.join("\n");
|
||||
}
|
||||
|
||||
function sanitizeTools(tools: any[]): any[] {
|
||||
return (tools ?? []).map((t) => {
|
||||
if (t?.type === "mcp" && t?.authorization) {
|
||||
return { ...t, authorization: "[REDACTED]" };
|
||||
}
|
||||
return t;
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeInputWithOpenAIFileIds(input: any, fileIds: string[]) {
|
||||
if (!fileIds?.length) return input;
|
||||
|
||||
// Responses API acepta input como string o array.
|
||||
const items = Array.isArray(input)
|
||||
? [...input]
|
||||
: [{ role: "user", content: input ?? "" }];
|
||||
|
||||
// Busca último mensaje "user"
|
||||
let idx = -1;
|
||||
for (let i = items.length - 1; i >= 0; i--) {
|
||||
if (items[i]?.role === "user") {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (idx === -1) {
|
||||
items.push({ role: "user", content: "" });
|
||||
idx = items.length - 1;
|
||||
}
|
||||
|
||||
const msg = items[idx];
|
||||
const fileParts = fileIds.map((id) => ({ type: "input_file", file_id: id }));
|
||||
|
||||
if (typeof msg.content === "string") {
|
||||
msg.content = [{ type: "input_text", text: msg.content }, ...fileParts];
|
||||
} else if (Array.isArray(msg.content)) {
|
||||
msg.content = [...msg.content, ...fileParts];
|
||||
} else {
|
||||
// fallback raro
|
||||
msg.content = [{ type: "input_text", text: String(msg.content) }, ...fileParts];
|
||||
}
|
||||
|
||||
items[idx] = msg;
|
||||
return items;
|
||||
}
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
if (req.method === "OPTIONS") return new Response(null, { status: 204, headers: corsHeaders });
|
||||
|
||||
try {
|
||||
// 1) Auth requerido ✅
|
||||
const authHeader =
|
||||
req.headers.get("Authorization") ?? req.headers.get("authorization");
|
||||
if (!authHeader) return json({ ok: false, error: "Missing Authorization header" }, 401);
|
||||
|
||||
const SUPABASE_URL = Deno.env.get("SUPABASE_URL");
|
||||
const SUPABASE_ANON_KEY = Deno.env.get("SUPABASE_ANON_KEY");
|
||||
if (!SUPABASE_URL || !SUPABASE_ANON_KEY) {
|
||||
return json({ ok: false, error: "Missing SUPABASE_URL / SUPABASE_ANON_KEY" }, 500);
|
||||
}
|
||||
|
||||
// Cliente con el JWT del usuario (para validar quién llama)
|
||||
const supabaseUser = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
||||
global: { headers: { Authorization: authHeader } },
|
||||
});
|
||||
|
||||
const { data: userData, error: userErr } = await supabaseUser.auth.getUser();
|
||||
if (userErr || !userData?.user) {
|
||||
return json({ ok: false, error: "Invalid token" }, 401);
|
||||
}
|
||||
const user = userData.user;
|
||||
|
||||
// (Opcional) service role para uploads server-side (más confiable)
|
||||
const SERVICE_ROLE = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY");
|
||||
const supabaseAdmin = SERVICE_ROLE
|
||||
? createClient(SUPABASE_URL, SERVICE_ROLE)
|
||||
: supabaseUser;
|
||||
|
||||
// 2) Parse body (JSON o multipart) 📦
|
||||
const contentType = req.headers.get("content-type") ?? "";
|
||||
let payload: any = {};
|
||||
let tempFiles: File[] = [];
|
||||
|
||||
if (contentType.includes("multipart/form-data")) {
|
||||
const form = await req.formData();
|
||||
const raw = form.get("payload")?.toString() ?? "{}";
|
||||
payload = JSON.parse(raw);
|
||||
|
||||
// Soporta nombres comunes
|
||||
for (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
|
||||
if (tempFiles.length === 0) {
|
||||
for (const [, v] of form.entries()) if (v instanceof File) tempFiles.push(v);
|
||||
}
|
||||
} else {
|
||||
payload = await req.json();
|
||||
}
|
||||
|
||||
// 3) Construye config final para OpenAI (passthrough + overrides) 🧠
|
||||
// Puedes mandar "response" o "openai" (ambos se mezclan)
|
||||
const responseCfg: any = {
|
||||
...(payload.openai ?? {}),
|
||||
...(payload.response ?? {}),
|
||||
};
|
||||
|
||||
// Conveniencia top-level
|
||||
if (payload.input !== undefined && responseCfg.input === undefined) responseCfg.input = payload.input;
|
||||
if (payload.model && !responseCfg.model) responseCfg.model = payload.model;
|
||||
if (payload.temperature !== undefined && responseCfg.temperature === undefined) responseCfg.temperature = payload.temperature;
|
||||
if (payload.conversationId && responseCfg.conversation === undefined) responseCfg.conversation = payload.conversationId;
|
||||
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
|
||||
responseCfg.model = responseCfg.model ?? "gpt-5";
|
||||
responseCfg.temperature = responseCfg.temperature ?? 0.2;
|
||||
|
||||
// Regla API: no mezclar conversation + previous_response_id
|
||||
if (responseCfg.conversation && responseCfg.previous_response_id) {
|
||||
return json(
|
||||
{ ok: false, error: "Do not send both `conversation` and `previous_response_id`." },
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
// 4) Structured Outputs (JSON Schema) ✅
|
||||
// Espera payload.structured (o payload.outputSchema) como objeto o string JSON
|
||||
let structured = payload.structured ?? payload.outputSchema;
|
||||
if (!structured) return json({ ok: false, error: "Missing `structured` (JSON schema format)." }, 400);
|
||||
|
||||
if (typeof structured === "string") {
|
||||
try {
|
||||
structured = JSON.parse(structured);
|
||||
} catch {
|
||||
return json({ ok: false, error: "`structured` is a string but not valid JSON." }, 400);
|
||||
}
|
||||
}
|
||||
|
||||
// Permite que te manden { format: {...} }
|
||||
const format = structured.format ? structured.format : structured;
|
||||
|
||||
if (!format.type) format.type = "json_schema";
|
||||
if (format.type === "json_schema") {
|
||||
format.name = format.name ?? "structured_output";
|
||||
format.strict = format.strict ?? true;
|
||||
if (!format.schema) return json({ ok: false, error: "structured.schema is required." }, 400);
|
||||
}
|
||||
|
||||
responseCfg.text = { ...(responseCfg.text ?? {}), format };
|
||||
|
||||
// 5) Referencias (3 tipos) 📎
|
||||
const references = payload.references ?? {};
|
||||
const openaiFileIds: string[] =
|
||||
references.openaiFileIds ?? payload.openaiFileIds ?? [];
|
||||
const vectorStoreIds: string[] =
|
||||
references.vectorStoreIds ?? payload.vectorStoreIds ?? [];
|
||||
|
||||
// 5a) Vector stores => file_search tool
|
||||
const tools: any[] = Array.isArray(responseCfg.tools) ? [...responseCfg.tools] : [];
|
||||
if (vectorStoreIds.length) {
|
||||
tools.push({ type: "file_search", vector_store_ids: vectorStoreIds });
|
||||
|
||||
// Si quieres ver resultados de búsqueda en la respuesta
|
||||
const include = Array.isArray(responseCfg.include) ? responseCfg.include : [];
|
||||
if (!include.includes("file_search_call.results")) include.push("file_search_call.results");
|
||||
responseCfg.include = include;
|
||||
}
|
||||
|
||||
// 5b) MCP opcional
|
||||
const usarMCP: boolean = Boolean(payload.usarMCP ?? references.usarMCP ?? payload.mcp?.enabled);
|
||||
if (usarMCP) {
|
||||
const mcp = payload.mcp ?? {};
|
||||
const server_url = mcp.server_url ?? Deno.env.get("MCP_SERVER_URL");
|
||||
if (!server_url) {
|
||||
return json(
|
||||
{ ok: false, error: "usarMCP=true pero falta MCP_SERVER_URL o mcp.server_url" },
|
||||
400
|
||||
);
|
||||
}
|
||||
tools.push({
|
||||
type: "mcp",
|
||||
server_label: mcp.server_label ?? Deno.env.get("MCP_SERVER_LABEL") ?? "supabase",
|
||||
server_description: mcp.server_description ?? "Supabase MCP server",
|
||||
server_url,
|
||||
require_approval: mcp.require_approval ?? "never",
|
||||
authorization: mcp.authorization ?? Deno.env.get("MCP_AUTHORIZATION"),
|
||||
allowed_tools: mcp.allowed_tools, // opcional
|
||||
});
|
||||
}
|
||||
|
||||
responseCfg.tools = tools;
|
||||
|
||||
// 5c) OpenAI file IDs => se insertan al input como input_file
|
||||
if (openaiFileIds.length) {
|
||||
responseCfg.input = normalizeInputWithOpenAIFileIds(responseCfg.input, openaiFileIds);
|
||||
}
|
||||
|
||||
// 6) Archivos temporales => subir a Supabase Storage (NO a OpenAI) ☁️
|
||||
const uploadedToStorage: Array<{
|
||||
bucket: string;
|
||||
path: string;
|
||||
name: string;
|
||||
type: string;
|
||||
size: number;
|
||||
}> = [];
|
||||
|
||||
if (tempFiles.length) {
|
||||
const bucket = payload.storage?.bucket ?? 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) {
|
||||
const safeName = (f.name ?? "file").replaceAll(/[^a-zA-Z0-9._-]/g, "_");
|
||||
const path = `${prefix}/${user.id}/${crypto.randomUUID()}-${safeName}`;
|
||||
|
||||
const { data, error } = await supabaseAdmin.storage
|
||||
.from(bucket)
|
||||
.upload(path, f, { contentType: f.type, upsert: false });
|
||||
|
||||
if (error) {
|
||||
return json({ ok: false, error: `Storage upload failed: ${error.message}`, file: f.name }, 500);
|
||||
}
|
||||
|
||||
uploadedToStorage.push({
|
||||
bucket,
|
||||
path: data.path,
|
||||
name: f.name,
|
||||
type: f.type,
|
||||
size: f.size,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 7) Llamada a OpenAI Responses API 🚀
|
||||
const OPENAI_API_KEY = Deno.env.get("OPENAI_API_KEY");
|
||||
if (!OPENAI_API_KEY) return json({ ok: false, error: "Missing OPENAI_API_KEY" }, 500);
|
||||
|
||||
const openai = new OpenAI({ apiKey: OPENAI_API_KEY });
|
||||
const resp = await openai.responses.create(responseCfg);
|
||||
|
||||
const outputText = extractOutputText(resp);
|
||||
|
||||
let output: unknown = null;
|
||||
let outputParseError: string | null = null;
|
||||
try {
|
||||
output = JSON.parse(outputText);
|
||||
} catch (e) {
|
||||
outputParseError = (e as Error)?.message ?? String(e);
|
||||
}
|
||||
|
||||
return json({
|
||||
ok: outputParseError ? false : true,
|
||||
responseId: resp.id,
|
||||
conversationId:
|
||||
(resp as any)?.conversation?.id ??
|
||||
(typeof (resp as any)?.conversation === "string" ? (resp as any).conversation : responseCfg.conversation ?? null),
|
||||
model: resp.model ?? responseCfg.model,
|
||||
temperature: resp.temperature ?? responseCfg.temperature,
|
||||
usage: resp.usage ?? null,
|
||||
|
||||
outputText,
|
||||
output,
|
||||
outputParseError,
|
||||
|
||||
references: {
|
||||
uploadedToStorage,
|
||||
openaiFileIds,
|
||||
vectorStoreIds,
|
||||
// Si quieres mandar tus UUID internos de archivos, aquí van (solo eco)
|
||||
archivosReferenciaIds: references.archivosReferenciaIds ?? payload.archivosReferenciaIds ?? payload.archivosReferencia ?? [],
|
||||
},
|
||||
|
||||
tools: sanitizeTools(responseCfg.tools),
|
||||
});
|
||||
} catch (e) {
|
||||
return json({ ok: false, error: (e as Error)?.message ?? String(e) }, 500);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import { supabase } from "./new-user";
|
||||
|
||||
const /* { data, error } */_ = await supabase.auth.signInWithPassword({
|
||||
email: "guillermo.arrieta@lasalle.mx",
|
||||
password: "admin",
|
||||
});
|
||||
|
||||
/* if (error) {
|
||||
console.error("Error signing in:", error);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log("Successfully signed in:", data);
|
||||
} */
|
||||
|
||||
try {
|
||||
|
||||
const response = await supabase.functions.invoke("ai-structured", {
|
||||
body: {
|
||||
response: {
|
||||
model: "gpt-5",
|
||||
input: [
|
||||
{ role: "system", content: "Responde SIEMPRE en JSON válido." },
|
||||
{ role: "user", content: "Dame 3 ideas de proyecto de IA para educación." },
|
||||
],
|
||||
/* conversation: "conv_...", */ // opcional
|
||||
},
|
||||
structured: {
|
||||
type: "json_schema",
|
||||
name: "ideas",
|
||||
strict: true,
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
ideas: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
titulo: { type: "string" },
|
||||
descripcion: { type: "string" }
|
||||
},
|
||||
required: ["titulo", "descripcion"],
|
||||
additionalProperties: false
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ["ideas"],
|
||||
additionalProperties: false
|
||||
}
|
||||
},
|
||||
references: {
|
||||
/* vectorStoreIds: ["vs_..."], */ // opcional (file_search) ✅
|
||||
/* openaiFileIds: ["file_..."], */ // opcional (input_file) ✅
|
||||
},
|
||||
usarMCP: false, // opcional ✅
|
||||
},
|
||||
});
|
||||
console.log({ d: response.data, e: response.error });
|
||||
} catch (e: any) {
|
||||
const res = e?.context ?? e?.response;
|
||||
console.log("status", res?.status);
|
||||
if (res?.text) console.log("text", await res.text());
|
||||
else console.log("raw error keys", Object.keys(e ?? {}));
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
export const supabase = createClient(Bun.env.SUPABASE_URL!, Bun.env.SUPABASE_ANON_KEY!)
|
||||
/* try {
|
||||
const { data, error } = await supabase.auth.signUp({
|
||||
email: 'guillermo.arrieta@lasalle.mx',
|
||||
password: 'sadfasdfsadfdsasadfsad',
|
||||
})
|
||||
console.log({ data, error })
|
||||
} catch (error) {
|
||||
console.log({ error })
|
||||
} */
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user