issue/6-generacin-de-materias #16
@@ -26,3 +26,14 @@ entrypoint = "./functions/create-chat-conversation/index.ts"
|
|||||||
# Specifies static files to be bundled with the function. Supports glob patterns.
|
# 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:
|
# For example, if you want to serve static HTML pages in your function:
|
||||||
# static_files = [ "./functions/create-chat-conversation/*.html" ]
|
# 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" ]
|
||||||
|
|||||||
@@ -3,11 +3,9 @@
|
|||||||
import OpenAI from "npm:openai@6.16.0";
|
import OpenAI from "npm:openai@6.16.0";
|
||||||
import type * as OpenAITypes 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";
|
import { createClient, type SupabaseClient } from "npm:@supabase/supabase-js@2";
|
||||||
|
|
||||||
// Use non-streaming params to ensure `responses.create` returns a typed Response
|
// Use non-streaming params to ensure `responses.create` returns a typed Response
|
||||||
export type StructuredResponseOptions =
|
export type StructuredResponseOptions =
|
||||||
OpenAITypes.OpenAI.Responses.ResponseCreateParamsNonStreaming;
|
OpenAITypes.OpenAI.Responses.ResponseCreateParamsNonStreaming;
|
||||||
|
|
||||||
export type StructuredResponseSuccess<TOutput = unknown> = {
|
export type StructuredResponseSuccess<TOutput = unknown> = {
|
||||||
ok: true;
|
ok: true;
|
||||||
output?: TOutput; // parsed JSON when available
|
output?: TOutput; // parsed JSON when available
|
||||||
@@ -22,7 +20,6 @@ export type StructuredResponseSuccess<TOutput = unknown> = {
|
|||||||
};
|
};
|
||||||
openaiRaw: OpenAITypes.OpenAI.Responses.Response; // keep for advanced consumers
|
openaiRaw: OpenAITypes.OpenAI.Responses.Response; // keep for advanced consumers
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StructuredResponseFailure = {
|
export type StructuredResponseFailure = {
|
||||||
ok: false;
|
ok: false;
|
||||||
code:
|
code:
|
||||||
@@ -33,23 +30,19 @@ export type StructuredResponseFailure = {
|
|||||||
message: string;
|
message: string;
|
||||||
cause?: unknown;
|
cause?: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StructuredResponseResult<TOutput = unknown> =
|
export type StructuredResponseResult<TOutput = unknown> =
|
||||||
| StructuredResponseSuccess<TOutput>
|
| StructuredResponseSuccess<TOutput>
|
||||||
| StructuredResponseFailure;
|
| StructuredResponseFailure;
|
||||||
|
|
||||||
export interface OpenAIServiceConfig {
|
export interface OpenAIServiceConfig {
|
||||||
supabaseUrl: string;
|
supabaseUrl: string;
|
||||||
serviceRoleKey: string;
|
serviceRoleKey: string;
|
||||||
openAIApiKey: string;
|
openAIApiKey: string;
|
||||||
bucket?: string; // default: ai-storage
|
bucket?: string; // default: ai-storage
|
||||||
}
|
}
|
||||||
|
|
||||||
export class OpenAIService {
|
export class OpenAIService {
|
||||||
private readonly supabase: SupabaseClient;
|
private readonly supabase: SupabaseClient;
|
||||||
private readonly openai: OpenAI;
|
private readonly openai: OpenAI;
|
||||||
private readonly bucket: string;
|
private readonly bucket: string;
|
||||||
|
|
||||||
private constructor(
|
private constructor(
|
||||||
client: SupabaseClient,
|
client: SupabaseClient,
|
||||||
openai: OpenAI,
|
openai: OpenAI,
|
||||||
@@ -59,13 +52,11 @@ export class OpenAIService {
|
|||||||
this.openai = openai;
|
this.openai = openai;
|
||||||
this.bucket = bucket;
|
this.bucket = bucket;
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromEnv(): StructuredResponseFailure | OpenAIService {
|
static fromEnv(): StructuredResponseFailure | OpenAIService {
|
||||||
const supabaseUrl = Deno.env.get("SUPABASE_URL") ?? "";
|
const supabaseUrl = Deno.env.get("SUPABASE_URL") ?? "";
|
||||||
const serviceRoleKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "";
|
const serviceRoleKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "";
|
||||||
const openAIApiKey = Deno.env.get("OPENAI_API_KEY") ?? "";
|
const openAIApiKey = Deno.env.get("OPENAI_API_KEY") ?? "";
|
||||||
const bucket = Deno.env.get("SUPABASE_BUCKET") ?? "ai-storage";
|
const bucket = Deno.env.get("SUPABASE_BUCKET") ?? "ai-storage";
|
||||||
|
|
||||||
if (!supabaseUrl || !serviceRoleKey || !openAIApiKey) {
|
if (!supabaseUrl || !serviceRoleKey || !openAIApiKey) {
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
@@ -74,12 +65,16 @@ export class OpenAIService {
|
|||||||
"Required env vars missing: SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, OPENAI_API_KEY",
|
"Required env vars missing: SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, OPENAI_API_KEY",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = createClient(supabaseUrl, serviceRoleKey);
|
const client = createClient(supabaseUrl, serviceRoleKey);
|
||||||
const openai = new OpenAI({ apiKey: openAIApiKey });
|
const openai = new OpenAI({ apiKey: openAIApiKey });
|
||||||
return new OpenAIService(client, openai, bucket);
|
return new OpenAIService(client, openai, bucket);
|
||||||
}
|
}
|
||||||
|
async createConversation(metadata?: Record<string, string>) {
|
||||||
|
const conversation = await this.openai.conversations.create({
|
||||||
|
metadata,
|
||||||
|
});
|
||||||
|
return conversation;
|
||||||
|
}
|
||||||
async createStructuredResponse<TOutput = unknown>(
|
async createStructuredResponse<TOutput = unknown>(
|
||||||
options: StructuredResponseOptions,
|
options: StructuredResponseOptions,
|
||||||
files?: File[],
|
files?: File[],
|
||||||
@@ -91,7 +86,6 @@ export class OpenAIService {
|
|||||||
const openaiFileIds = await this.uploadFilesToOpenAI(files ?? []);
|
const openaiFileIds = await this.uploadFilesToOpenAI(files ?? []);
|
||||||
|
|
||||||
const newOptions = { ...options };
|
const newOptions = { ...options };
|
||||||
|
|
||||||
// Attach file references to the request as an extra user message
|
// Attach file references to the request as an extra user message
|
||||||
if (openaiFileIds.length > 0) {
|
if (openaiFileIds.length > 0) {
|
||||||
const fileParts:
|
const fileParts:
|
||||||
@@ -100,7 +94,6 @@ export class OpenAIService {
|
|||||||
type: "input_file",
|
type: "input_file",
|
||||||
file_id: id,
|
file_id: id,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const arr = Array.isArray(options.input) ? options.input : [];
|
const arr = Array.isArray(options.input) ? options.input : [];
|
||||||
arr.push({
|
arr.push({
|
||||||
role: "user",
|
role: "user",
|
||||||
@@ -119,7 +112,6 @@ export class OpenAIService {
|
|||||||
const openaiRaw = (await this.openai.responses.create(
|
const openaiRaw = (await this.openai.responses.create(
|
||||||
newOptions as OpenAITypes.OpenAI.Responses.ResponseCreateParamsNonStreaming,
|
newOptions as OpenAITypes.OpenAI.Responses.ResponseCreateParamsNonStreaming,
|
||||||
)) as OpenAITypes.OpenAI.Responses.Response;
|
)) as OpenAITypes.OpenAI.Responses.Response;
|
||||||
|
|
||||||
const { model, id: responseId } = openaiRaw;
|
const { model, id: responseId } = openaiRaw;
|
||||||
const usage = openaiRaw?.usage ?? null;
|
const usage = openaiRaw?.usage ?? null;
|
||||||
const conversationId = (
|
const conversationId = (
|
||||||
@@ -127,11 +119,9 @@ export class OpenAIService {
|
|||||||
conversation_id?: string | null;
|
conversation_id?: string | null;
|
||||||
}
|
}
|
||||||
).conversation_id ?? null;
|
).conversation_id ?? null;
|
||||||
|
|
||||||
// Try to read structured JSON output
|
// Try to read structured JSON output
|
||||||
let output: TOutput | undefined = undefined;
|
let output: TOutput | undefined = undefined;
|
||||||
let outputText: string | undefined = undefined;
|
let outputText: string | undefined = undefined;
|
||||||
|
|
||||||
// Prefer `output_text` if present (SDK convenience)
|
// Prefer `output_text` if present (SDK convenience)
|
||||||
const maybeOutputText = openaiRaw.output_text;
|
const maybeOutputText = openaiRaw.output_text;
|
||||||
if (
|
if (
|
||||||
@@ -141,7 +131,9 @@ export class OpenAIService {
|
|||||||
outputText = maybeOutputText;
|
outputText = maybeOutputText;
|
||||||
try {
|
try {
|
||||||
output = JSON.parse(maybeOutputText) as TOutput;
|
output = JSON.parse(maybeOutputText) as TOutput;
|
||||||
} catch { /* non-JSON text, keep as text only */ }
|
} catch {
|
||||||
|
/* non-JSON text, keep as text only */
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback: attempt to serialize `openaiRaw.output` into text
|
// Fallback: attempt to serialize `openaiRaw.output` into text
|
||||||
const maybeOutput = openaiRaw.output as unknown;
|
const maybeOutput = openaiRaw.output as unknown;
|
||||||
@@ -149,10 +141,11 @@ export class OpenAIService {
|
|||||||
try {
|
try {
|
||||||
outputText = JSON.stringify(maybeOutput);
|
outputText = JSON.stringify(maybeOutput);
|
||||||
output = maybeOutput as TOutput;
|
output = maybeOutput as TOutput;
|
||||||
} catch { /* ignore */ }
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
output,
|
output,
|
||||||
@@ -172,11 +165,9 @@ export class OpenAIService {
|
|||||||
: message.includes("OpenAI file upload failed")
|
: message.includes("OpenAI file upload failed")
|
||||||
? "OpenAIFileUploadFailed"
|
? "OpenAIFileUploadFailed"
|
||||||
: "OpenAIRequestFailed";
|
: "OpenAIRequestFailed";
|
||||||
|
|
||||||
return { ok: false, code, message, cause: err };
|
return { ok: false, code, message, cause: err };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async uploadFilesToStorage(files: File[]): Promise<string[]> {
|
private async uploadFilesToStorage(files: File[]): Promise<string[]> {
|
||||||
const paths: string[] = [];
|
const paths: string[] = [];
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
@@ -188,7 +179,6 @@ export class OpenAIService {
|
|||||||
contentType: file.type || "application/octet-stream",
|
contentType: file.type || "application/octet-stream",
|
||||||
upsert: false,
|
upsert: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
throw new Error(`Supabase upload failed: ${error.message}`);
|
throw new Error(`Supabase upload failed: ${error.message}`);
|
||||||
}
|
}
|
||||||
@@ -196,7 +186,6 @@ export class OpenAIService {
|
|||||||
}
|
}
|
||||||
return paths;
|
return paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async uploadFilesToOpenAI(files: File[]): Promise<string[]> {
|
private async uploadFilesToOpenAI(files: File[]): Promise<string[]> {
|
||||||
const ids: string[] = [];
|
const ids: string[] = [];
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
@@ -216,7 +205,6 @@ export class OpenAIService {
|
|||||||
}
|
}
|
||||||
return ids;
|
return ids;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sanitizeFilename(name: string): string {
|
private sanitizeFilename(name: string): string {
|
||||||
return name
|
return name
|
||||||
.normalize("NFD")
|
.normalize("NFD")
|
||||||
|
|||||||
@@ -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,3 @@
|
|||||||
|
{
|
||||||
|
"imports": {}
|
||||||
|
}
|
||||||
@@ -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" } },
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user