REFACTOR OpenAI service to manage env and convo creation #12
@@ -50,11 +50,7 @@ export class OpenAIService {
|
||||
private readonly openai: OpenAI;
|
||||
private readonly bucket: string;
|
||||
|
||||
private constructor(
|
||||
client: SupabaseClient,
|
||||
openai: OpenAI,
|
||||
bucket: string,
|
||||
) {
|
||||
private constructor(client: SupabaseClient, openai: OpenAI, bucket: string) {
|
||||
this.supabase = client;
|
||||
this.openai = openai;
|
||||
this.bucket = bucket;
|
||||
@@ -80,41 +76,21 @@ export class OpenAIService {
|
||||
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>(
|
||||
options: StructuredResponseOptions,
|
||||
files?: File[],
|
||||
): Promise<StructuredResponseResult<TOutput>> {
|
||||
try {
|
||||
const uploadedToStorage = await this.uploadFilesToStorage(
|
||||
files ?? [],
|
||||
);
|
||||
const uploadedToStorage = await this.uploadFilesToStorage(files ?? []);
|
||||
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:
|
||||
OpenAITypes.OpenAI.Responses.ResponseInputFile[] =
|
||||
openaiFileIds.map((id) => ({
|
||||
type: "input_file",
|
||||
file_id: id,
|
||||
}));
|
||||
|
||||
const arr = Array.isArray(options.input) ? options.input : [];
|
||||
arr.push({
|
||||
role: "user",
|
||||
content: [
|
||||
...fileParts,
|
||||
{
|
||||
type: "input_text",
|
||||
text: "Usa estos archivos como referencia",
|
||||
},
|
||||
],
|
||||
});
|
||||
newOptions.input = arr;
|
||||
}
|
||||
|
||||
// Narrow to non-streaming response
|
||||
const openaiRaw = (await this.openai.responses.create(
|
||||
newOptions as OpenAITypes.OpenAI.Responses.ResponseCreateParamsNonStreaming,
|
||||
@@ -122,7 +98,8 @@ export class OpenAIService {
|
||||
|
||||
const { model, id: responseId } = openaiRaw;
|
||||
const usage = openaiRaw?.usage ?? null;
|
||||
const conversationId = (
|
||||
const conversationId =
|
||||
(
|
||||
openaiRaw as OpenAITypes.OpenAI.Responses.Response & {
|
||||
conversation_id?: string | null;
|
||||
}
|
||||
@@ -134,14 +111,13 @@ export class OpenAIService {
|
||||
|
||||
// Prefer `output_text` if present (SDK convenience)
|
||||
const maybeOutputText = openaiRaw.output_text;
|
||||
if (
|
||||
typeof maybeOutputText === "string" &&
|
||||
maybeOutputText.length > 0
|
||||
) {
|
||||
if (typeof maybeOutputText === "string" && maybeOutputText.length > 0) {
|
||||
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,7 +125,9 @@ export class OpenAIService {
|
||||
try {
|
||||
outputText = JSON.stringify(maybeOutput);
|
||||
output = maybeOutput as TOutput;
|
||||
} catch { /* ignore */ }
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,9 +186,7 @@ export class OpenAIService {
|
||||
ids.push(created.id);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`OpenAI file upload failed: ${
|
||||
(e as Error)?.message ?? String(e)
|
||||
}`,
|
||||
`OpenAI file upload failed: ${(e as Error)?.message ?? String(e)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// Setup type definitions for built-in Supabase Runtime APIs
|
||||
import "@supabase/functions-js/edge-runtime.d.ts";
|
||||
import { corsHeaders } from "../_shared/cors.ts";
|
||||
import { OpenAIService } from "../_shared/openai-service.ts";
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
|
||||
|
||||
type WebhookPayload = {
|
||||
@@ -13,9 +15,6 @@ type WebhookPayload = {
|
||||
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"]);
|
||||
@@ -27,27 +26,16 @@ const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, {
|
||||
function jsonResponse(status: number, body: Record<string, unknown>) {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: { "Content-Type": "application/json", ...corsHeaders },
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
async function createConversationId(
|
||||
openaiService: OpenAIService,
|
||||
metadata: Record<string, string>,
|
||||
) {
|
||||
const conversation = await openaiService.createConversation(metadata);
|
||||
const conversationId = conversation?.id as string | undefined;
|
||||
|
||||
if (!conversationId) {
|
||||
throw new Error("OpenAI response missing conversation id");
|
||||
@@ -65,8 +53,9 @@ Deno.serve(async (req) => {
|
||||
return jsonResponse(500, { error: "Supabase env vars missing" });
|
||||
}
|
||||
|
||||
if (!OPENAI_API_KEY) {
|
||||
return jsonResponse(500, { error: "OPENAI_API_KEY missing" });
|
||||
const openaiService = OpenAIService.fromEnv();
|
||||
if (!(openaiService instanceof OpenAIService)) {
|
||||
return jsonResponse(500, { error: openaiService.message });
|
||||
}
|
||||
|
||||
let payload: WebhookPayload;
|
||||
@@ -108,7 +97,7 @@ Deno.serve(async (req) => {
|
||||
|
||||
let conversationId: string;
|
||||
try {
|
||||
conversationId = await createConversationId({
|
||||
conversationId = await createConversationId(openaiService, {
|
||||
table,
|
||||
record_id: String(recordId),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user