REFACTOR OpenAI service to manage env and convo creation #12

Merged
alexrg merged 1 commits from issue/9-hacer-una-funcin-que-inserte-el-id-de-conversacin into main 2026-02-03 15:39:50 +00:00
2 changed files with 181 additions and 216 deletions
+19 -43
View File
@@ -50,11 +50,7 @@ export class OpenAIService {
private readonly openai: OpenAI; private readonly openai: OpenAI;
private readonly bucket: string; private readonly bucket: string;
private constructor( private constructor(client: SupabaseClient, openai: OpenAI, bucket: string) {
client: SupabaseClient,
openai: OpenAI,
bucket: string,
) {
this.supabase = client; this.supabase = client;
this.openai = openai; this.openai = openai;
this.bucket = bucket; this.bucket = bucket;
@@ -80,41 +76,21 @@ export class OpenAIService {
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[],
): Promise<StructuredResponseResult<TOutput>> { ): Promise<StructuredResponseResult<TOutput>> {
try { try {
const uploadedToStorage = await this.uploadFilesToStorage( const uploadedToStorage = await this.uploadFilesToStorage(files ?? []);
files ?? [],
);
const openaiFileIds = await this.uploadFilesToOpenAI(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 // Narrow to non-streaming response
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,
@@ -122,7 +98,8 @@ export class OpenAIService {
const { model, id: responseId } = openaiRaw; const { model, id: responseId } = openaiRaw;
const usage = openaiRaw?.usage ?? null; const usage = openaiRaw?.usage ?? null;
const conversationId = ( const conversationId =
(
openaiRaw as OpenAITypes.OpenAI.Responses.Response & { openaiRaw as OpenAITypes.OpenAI.Responses.Response & {
conversation_id?: string | null; conversation_id?: string | null;
} }
@@ -134,14 +111,13 @@ export class OpenAIService {
// 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 (typeof maybeOutputText === "string" && maybeOutputText.length > 0) {
typeof maybeOutputText === "string" &&
maybeOutputText.length > 0
) {
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,7 +125,9 @@ 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 */
}
} }
} }
@@ -208,9 +186,7 @@ export class OpenAIService {
ids.push(created.id); ids.push(created.id);
} catch (e) { } catch (e) {
throw new Error( throw new Error(
`OpenAI file upload failed: ${ `OpenAI file upload failed: ${(e as Error)?.message ?? String(e)}`,
(e as Error)?.message ?? String(e)
}`,
); );
} }
} }
@@ -1,5 +1,7 @@
// Setup type definitions for built-in Supabase Runtime APIs // Setup type definitions for built-in Supabase Runtime APIs
import "@supabase/functions-js/edge-runtime.d.ts"; 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"; import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
type WebhookPayload = { type WebhookPayload = {
@@ -13,9 +15,6 @@ type WebhookPayload = {
const SUPABASE_URL = Deno.env.get("SUPABASE_URL") ?? ""; const SUPABASE_URL = Deno.env.get("SUPABASE_URL") ?? "";
const SUPABASE_SERVICE_ROLE_KEY = const SUPABASE_SERVICE_ROLE_KEY =
Deno.env.get("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_SCHEMA = "public";
const ALLOWED_TABLES = new Set(["planes_estudio", "asignaturas"]); 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>) { function jsonResponse(status: number, body: Record<string, unknown>) {
return new Response(JSON.stringify(body), { return new Response(JSON.stringify(body), {
status, status,
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json", ...corsHeaders },
}); });
} }
async function createConversationId(metadata: Record<string, string>) { async function createConversationId(
const response = await fetch(`${OPENAI_BASE_URL}/conversations`, { openaiService: OpenAIService,
method: "POST", metadata: Record<string, string>,
headers: { ) {
"Content-Type": "application/json", const conversation = await openaiService.createConversation(metadata);
Authorization: `Bearer ${OPENAI_API_KEY}`, const conversationId = conversation?.id as string | undefined;
},
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) { if (!conversationId) {
throw new Error("OpenAI response missing conversation id"); throw new Error("OpenAI response missing conversation id");
@@ -65,8 +53,9 @@ Deno.serve(async (req) => {
return jsonResponse(500, { error: "Supabase env vars missing" }); return jsonResponse(500, { error: "Supabase env vars missing" });
} }
if (!OPENAI_API_KEY) { const openaiService = OpenAIService.fromEnv();
return jsonResponse(500, { error: "OPENAI_API_KEY missing" }); if (!(openaiService instanceof OpenAIService)) {
return jsonResponse(500, { error: openaiService.message });
} }
let payload: WebhookPayload; let payload: WebhookPayload;
@@ -108,7 +97,7 @@ Deno.serve(async (req) => {
let conversationId: string; let conversationId: string;
try { try {
conversationId = await createConversationId({ conversationId = await createConversationId(openaiService, {
table, table,
record_id: String(recordId), record_id: String(recordId),
}); });