Add create-chat-conversation function and related configurations
This commit is contained in:
Vendored
+4
-2
@@ -2,10 +2,12 @@
|
||||
"deno.enablePaths": ["supabase/functions"],
|
||||
"deno.lint": true,
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "denoland.vscode-deno"
|
||||
"editor.defaultFormatter": "denoland.vscode-deno",
|
||||
},
|
||||
"notebook.defaultFormatter": "denoland.vscode-deno",
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"deno.enable": true,
|
||||
"editor.formatOnSave": true
|
||||
"editor.formatOnSave": true,
|
||||
|
||||
"typescript.tsserver.experimental.enableProjectDiagnostics": false,
|
||||
}
|
||||
|
||||
@@ -5,25 +5,20 @@
|
||||
"jsr:@david/dax@~0.44.2": "0.44.2",
|
||||
"jsr:@david/path@0.2": "0.2.0",
|
||||
"jsr:@david/which@~0.4.1": "0.4.1",
|
||||
"jsr:@openai/openai@*": "6.9.0",
|
||||
"jsr:@openai/openai@^6.16.0": "6.16.0",
|
||||
"jsr:@std/assert@1": "1.0.16",
|
||||
"jsr:@std/bytes@^1.0.5": "1.0.6",
|
||||
"jsr:@std/dotenv@*": "0.225.6",
|
||||
"jsr:@std/bytes@^1.0.6": "1.0.6",
|
||||
"jsr:@std/fmt@1": "1.0.9",
|
||||
"jsr:@std/fs@1": "1.0.22",
|
||||
"jsr:@std/fs@^1.0.20": "1.0.22",
|
||||
"jsr:@std/internal@^1.0.12": "1.0.12",
|
||||
"jsr:@std/io@0.225": "0.225.2",
|
||||
"jsr:@std/io@0.225": "0.225.3",
|
||||
"jsr:@std/path@1": "1.1.4",
|
||||
"jsr:@std/path@^1.1.4": "1.1.4",
|
||||
"jsr:@supabase/functions-js@*": "2.90.1",
|
||||
"jsr:@zod/zod@^4.3.5": "4.3.5",
|
||||
"npm:@supabase/supabase-js@^2.90.1": "2.90.1",
|
||||
"npm:@types/bun@^1.3.5": "1.3.5",
|
||||
"npm:deno@^2.6.4": "2.6.4",
|
||||
"npm:openai@^6.16.0": "6.16.0",
|
||||
"npm:supabase-js@^1.0.4": "1.0.4",
|
||||
"npm:supabase@^2.72.6": "2.72.6",
|
||||
"npm:zod@3": "3.25.76"
|
||||
},
|
||||
@@ -53,27 +48,15 @@
|
||||
"@david/which@0.4.1": {
|
||||
"integrity": "896a682b111f92ab866cc70c5b4afab2f5899d2f9bde31ed00203b9c250f225e"
|
||||
},
|
||||
"@openai/openai@6.9.0": {
|
||||
"integrity": "d1828101f5a782a75c25805c4e06c1316d9c19f422d84755fb908046184c3ef6"
|
||||
},
|
||||
"@openai/openai@6.16.0": {
|
||||
"integrity": "ccee548f61c382d715091fff0c2c3390a4487823644430b8235df543d6d6a78b",
|
||||
"dependencies": [
|
||||
"npm:zod"
|
||||
]
|
||||
},
|
||||
"@std/assert@1.0.16": {
|
||||
"integrity": "6a7272ed1eaa77defe76e5ff63ca705d9c495077e2d5fd0126d2b53fc5bd6532",
|
||||
"dependencies": [
|
||||
"jsr:@std/internal"
|
||||
]
|
||||
},
|
||||
"@std/bytes@1.0.6": {
|
||||
"integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a"
|
||||
},
|
||||
"@std/dotenv@0.225.6": {
|
||||
"integrity": "1d6f9db72f565bd26790fa034c26e45ecb260b5245417be76c2279e5734c421b"
|
||||
},
|
||||
"@std/fmt@1.0.9": {
|
||||
"integrity": "2487343e8899fb2be5d0e3d35013e54477ada198854e52dd05ed0422eddcabe0"
|
||||
},
|
||||
@@ -87,8 +70,8 @@
|
||||
"@std/internal@1.0.12": {
|
||||
"integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027"
|
||||
},
|
||||
"@std/io@0.225.2": {
|
||||
"integrity": "3c740cd4ee4c082e6cfc86458f47e2ab7cb353dc6234d5e9b1f91a2de5f4d6c7",
|
||||
"@std/io@0.225.3": {
|
||||
"integrity": "27b07b591384d12d7b568f39e61dff966b8230559122df1e9fd11cc068f7ddd1",
|
||||
"dependencies": [
|
||||
"jsr:@std/bytes"
|
||||
]
|
||||
@@ -99,9 +82,6 @@
|
||||
"jsr:@std/internal"
|
||||
]
|
||||
},
|
||||
"@supabase/functions-js@2.90.1": {
|
||||
"integrity": "9a077ecf42aa84594ef3aac621d643e15ffaa9506a4c051e35c93f127ffdb035"
|
||||
},
|
||||
"@zod/zod@4.3.5": {
|
||||
"integrity": "3d14553f025d6e3d1a836b9c56e366bbce27d942301e8c7c1ac4c365df5f8085"
|
||||
}
|
||||
@@ -340,8 +320,7 @@
|
||||
"minipass",
|
||||
"minizlib",
|
||||
"yallist"
|
||||
],
|
||||
"deprecated": true
|
||||
]
|
||||
},
|
||||
"tslib@2.8.1": {
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||
@@ -371,9 +350,9 @@
|
||||
},
|
||||
"workspace": {
|
||||
"dependencies": [
|
||||
"jsr:@david/dax@~0.44.2",
|
||||
"jsr:@openai/openai@^6.16.0",
|
||||
"jsr:@zod/zod@^4.3.5"
|
||||
"jsr:@david/dax@~0.44.2"
|
||||
],
|
||||
"packageJson": {
|
||||
"dependencies": [
|
||||
|
||||
@@ -15,3 +15,14 @@ entrypoint = "./functions/ai-generate-plan/index.ts"
|
||||
# enabled = false
|
||||
# verify_jwt = false
|
||||
# import_map = "./functions/ai-structured/deno.json"
|
||||
|
||||
[functions.create-chat-conversation]
|
||||
enabled = true
|
||||
verify_jwt = true
|
||||
import_map = "./functions/create-chat-conversation/deno.json"
|
||||
# Uncomment to specify a custom file path to the entrypoint.
|
||||
# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx
|
||||
entrypoint = "./functions/create-chat-conversation/index.ts"
|
||||
# Specifies static files to be bundled with the function. Supports glob patterns.
|
||||
# For example, if you want to serve static HTML pages in your function:
|
||||
# static_files = [ "./functions/create-chat-conversation/*.html" ]
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Configuration for private npm package dependencies
|
||||
# For more information on using private registries with Edge Functions, see:
|
||||
# https://supabase.com/docs/guides/functions/import-maps#importing-from-private-registries
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"imports": {
|
||||
"@supabase/functions-js": "jsr:@supabase/functions-js@^2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
// Setup type definitions for built-in Supabase Runtime APIs
|
||||
import "@supabase/functions-js/edge-runtime.d.ts";
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
|
||||
|
||||
type WebhookPayload = {
|
||||
type: "INSERT" | "UPDATE" | "DELETE";
|
||||
table: string;
|
||||
schema: string;
|
||||
record: Record<string, unknown> | null;
|
||||
old_record: Record<string, unknown> | null;
|
||||
};
|
||||
|
||||
const SUPABASE_URL = Deno.env.get("SUPABASE_URL") ?? "";
|
||||
const SUPABASE_SERVICE_ROLE_KEY =
|
||||
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "";
|
||||
const OPENAI_API_KEY = Deno.env.get("OPENAI_API_KEY") ?? "";
|
||||
const OPENAI_BASE_URL =
|
||||
Deno.env.get("OPENAI_BASE_URL") ?? "https://api.openai.com/v1";
|
||||
|
||||
const ALLOWED_SCHEMA = "public";
|
||||
const ALLOWED_TABLES = new Set(["planes_estudio", "asignaturas"]);
|
||||
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, {
|
||||
auth: { persistSession: false },
|
||||
});
|
||||
|
||||
function jsonResponse(status: number, body: Record<string, unknown>) {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
async function createConversationId(metadata: Record<string, string>) {
|
||||
const response = await fetch(`${OPENAI_BASE_URL}/conversations`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${OPENAI_API_KEY}`,
|
||||
},
|
||||
body: JSON.stringify({ metadata }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`OpenAI error: ${response.status} ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const conversationId = data?.id as string | undefined;
|
||||
|
||||
if (!conversationId) {
|
||||
throw new Error("OpenAI response missing conversation id");
|
||||
}
|
||||
|
||||
return conversationId;
|
||||
}
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
if (req.method !== "POST") {
|
||||
return jsonResponse(405, { error: "Method not allowed" });
|
||||
}
|
||||
|
||||
if (!SUPABASE_URL || !SUPABASE_SERVICE_ROLE_KEY) {
|
||||
return jsonResponse(500, { error: "Supabase env vars missing" });
|
||||
}
|
||||
|
||||
if (!OPENAI_API_KEY) {
|
||||
return jsonResponse(500, { error: "OPENAI_API_KEY missing" });
|
||||
}
|
||||
|
||||
let payload: WebhookPayload;
|
||||
try {
|
||||
payload = await req.json();
|
||||
} catch {
|
||||
return jsonResponse(400, { error: "Invalid JSON body" });
|
||||
}
|
||||
|
||||
const { type, table, schema, record } = payload;
|
||||
|
||||
if (schema !== ALLOWED_SCHEMA || !ALLOWED_TABLES.has(table)) {
|
||||
return jsonResponse(400, { error: "Table not allowed" });
|
||||
}
|
||||
|
||||
if (type !== "INSERT") {
|
||||
return jsonResponse(200, { status: "ignored", reason: "event_type" });
|
||||
}
|
||||
|
||||
if (!record || typeof record !== "object") {
|
||||
return jsonResponse(400, { error: "Missing record" });
|
||||
}
|
||||
|
||||
const recordId = record["id"] as string | number | undefined;
|
||||
if (!recordId) {
|
||||
return jsonResponse(400, { error: "Record id missing" });
|
||||
}
|
||||
|
||||
const existingConversationId = record["conversation_id"] as
|
||||
| string
|
||||
| null
|
||||
| undefined;
|
||||
if (existingConversationId) {
|
||||
return jsonResponse(200, {
|
||||
status: "skipped",
|
||||
conversation_id: existingConversationId,
|
||||
});
|
||||
}
|
||||
|
||||
let conversationId: string;
|
||||
try {
|
||||
conversationId = await createConversationId({
|
||||
table,
|
||||
record_id: String(recordId),
|
||||
});
|
||||
} catch (error) {
|
||||
return jsonResponse(502, { error: (error as Error).message });
|
||||
}
|
||||
|
||||
const { error } = await supabase
|
||||
.from(table)
|
||||
.update({ conversation_id: conversationId })
|
||||
.eq("id", recordId);
|
||||
|
||||
if (error) {
|
||||
return jsonResponse(500, { error: error.message });
|
||||
}
|
||||
|
||||
return jsonResponse(200, {
|
||||
status: "updated",
|
||||
table,
|
||||
record_id: recordId,
|
||||
conversation_id: conversationId,
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user