diff --git a/supabase/config.toml b/supabase/config.toml index d81e000..5f119d7 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -26,3 +26,14 @@ 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" ] + +[functions.ai-generate-subject] +enabled = true +verify_jwt = true +import_map = "./functions/ai-generate-subject/deno.json" +# Uncomment to specify a custom file path to the entrypoint. +# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx +entrypoint = "./functions/ai-generate-subject/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/ai-generate-subject/*.html" ] diff --git a/supabase/functions/_shared/openai-service.ts b/supabase/functions/_shared/openai-service.ts index afa0cd4..a0a0258 100644 --- a/supabase/functions/_shared/openai-service.ts +++ b/supabase/functions/_shared/openai-service.ts @@ -3,11 +3,9 @@ import OpenAI from "npm:openai@6.16.0"; import type * as OpenAITypes from "npm:openai@6.16.0"; import { createClient, type SupabaseClient } from "npm:@supabase/supabase-js@2"; - // Use non-streaming params to ensure `responses.create` returns a typed Response export type StructuredResponseOptions = OpenAITypes.OpenAI.Responses.ResponseCreateParamsNonStreaming; - export type StructuredResponseSuccess = { ok: true; output?: TOutput; // parsed JSON when available @@ -22,7 +20,6 @@ export type StructuredResponseSuccess = { }; openaiRaw: OpenAITypes.OpenAI.Responses.Response; // keep for advanced consumers }; - export type StructuredResponseFailure = { ok: false; code: @@ -33,23 +30,19 @@ export type StructuredResponseFailure = { message: string; cause?: unknown; }; - export type StructuredResponseResult = | StructuredResponseSuccess | StructuredResponseFailure; - export interface OpenAIServiceConfig { supabaseUrl: string; serviceRoleKey: string; openAIApiKey: string; bucket?: string; // default: ai-storage } - export class OpenAIService { private readonly supabase: SupabaseClient; private readonly openai: OpenAI; private readonly bucket: string; - private constructor( client: SupabaseClient, openai: OpenAI, @@ -59,13 +52,11 @@ export class OpenAIService { this.openai = openai; this.bucket = bucket; } - static fromEnv(): StructuredResponseFailure | OpenAIService { const supabaseUrl = Deno.env.get("SUPABASE_URL") ?? ""; const serviceRoleKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? ""; const openAIApiKey = Deno.env.get("OPENAI_API_KEY") ?? ""; const bucket = Deno.env.get("SUPABASE_BUCKET") ?? "ai-storage"; - if (!supabaseUrl || !serviceRoleKey || !openAIApiKey) { return { ok: false, @@ -74,12 +65,16 @@ export class OpenAIService { "Required env vars missing: SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, OPENAI_API_KEY", }; } - const client = createClient(supabaseUrl, serviceRoleKey); const openai = new OpenAI({ apiKey: openAIApiKey }); return new OpenAIService(client, openai, bucket); } - + async createConversation(metadata?: Record) { + const conversation = await this.openai.conversations.create({ + metadata, + }); + return conversation; + } async createStructuredResponse( options: StructuredResponseOptions, files?: File[], @@ -91,7 +86,6 @@ export class OpenAIService { const openaiFileIds = await this.uploadFilesToOpenAI(files ?? []); const newOptions = { ...options }; - // Attach file references to the request as an extra user message if (openaiFileIds.length > 0) { const fileParts: @@ -100,7 +94,6 @@ export class OpenAIService { type: "input_file", file_id: id, })); - const arr = Array.isArray(options.input) ? options.input : []; arr.push({ role: "user", @@ -119,7 +112,6 @@ export class OpenAIService { const openaiRaw = (await this.openai.responses.create( newOptions as OpenAITypes.OpenAI.Responses.ResponseCreateParamsNonStreaming, )) as OpenAITypes.OpenAI.Responses.Response; - const { model, id: responseId } = openaiRaw; const usage = openaiRaw?.usage ?? null; const conversationId = ( @@ -127,11 +119,9 @@ export class OpenAIService { conversation_id?: string | null; } ).conversation_id ?? null; - // Try to read structured JSON output let output: TOutput | undefined = undefined; let outputText: string | undefined = undefined; - // Prefer `output_text` if present (SDK convenience) const maybeOutputText = openaiRaw.output_text; if ( @@ -141,7 +131,9 @@ export class OpenAIService { outputText = maybeOutputText; try { output = JSON.parse(maybeOutputText) as TOutput; - } catch { /* non-JSON text, keep as text only */ } + } catch { + /* non-JSON text, keep as text only */ + } } else { // Fallback: attempt to serialize `openaiRaw.output` into text const maybeOutput = openaiRaw.output as unknown; @@ -149,10 +141,11 @@ export class OpenAIService { try { outputText = JSON.stringify(maybeOutput); output = maybeOutput as TOutput; - } catch { /* ignore */ } + } catch { + /* ignore */ + } } } - return { ok: true, output, @@ -172,11 +165,9 @@ export class OpenAIService { : message.includes("OpenAI file upload failed") ? "OpenAIFileUploadFailed" : "OpenAIRequestFailed"; - return { ok: false, code, message, cause: err }; } } - private async uploadFilesToStorage(files: File[]): Promise { const paths: string[] = []; for (const file of files) { @@ -188,7 +179,6 @@ export class OpenAIService { contentType: file.type || "application/octet-stream", upsert: false, }); - if (error) { throw new Error(`Supabase upload failed: ${error.message}`); } @@ -196,7 +186,6 @@ export class OpenAIService { } return paths; } - private async uploadFilesToOpenAI(files: File[]): Promise { const ids: string[] = []; for (const file of files) { @@ -216,7 +205,6 @@ export class OpenAIService { } return ids; } - private sanitizeFilename(name: string): string { return name .normalize("NFD") diff --git a/supabase/functions/ai-generate-subject/.npmrc b/supabase/functions/ai-generate-subject/.npmrc new file mode 100644 index 0000000..48c6388 --- /dev/null +++ b/supabase/functions/ai-generate-subject/.npmrc @@ -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 diff --git a/supabase/functions/ai-generate-subject/deno.json b/supabase/functions/ai-generate-subject/deno.json new file mode 100644 index 0000000..f6ca845 --- /dev/null +++ b/supabase/functions/ai-generate-subject/deno.json @@ -0,0 +1,3 @@ +{ + "imports": {} +} diff --git a/supabase/functions/ai-generate-subject/index.ts b/supabase/functions/ai-generate-subject/index.ts new file mode 100644 index 0000000..8cb545f --- /dev/null +++ b/supabase/functions/ai-generate-subject/index.ts @@ -0,0 +1,25 @@ +import "jsr:@supabase/functions-js/edge-runtime.d.ts"; +import { corsHeaders } from "../_shared/cors.ts"; + +console.log("Hello from Functions!"); + +Deno.serve(async (req) => { + const url = new URL(req.url); + const functionName = url.pathname.split("/").pop(); + console.log( + `[${new Date().toISOString()}][${functionName}]: Request received`, + ); + + if (req.method === "OPTIONS") { + return new Response(null, { status: 204, headers: corsHeaders }); + } + const { name } = await req.json(); + const data = { + message: `Hello ${name}!`, + }; + + return new Response( + JSON.stringify(data), + { headers: { "Content-Type": "application/json" } }, + ); +});