feat: update AI subject generation and handling responses
- Refactor AIGenerateSubjectInput type to include optional fields for updates. - Add handling for "asignaturas" responses in OpenAI webhook. - Implement crear.ts for creating/updating subjects based on AI responses. - Update tests to validate OpenAI file uploads instead of storage uploads.
This commit is contained in:
@@ -1209,6 +1209,14 @@ export type Database = {
|
||||
}
|
||||
}
|
||||
Functions: {
|
||||
append_conversacion_asignatura: {
|
||||
Args: { p_append: Json; p_id: string }
|
||||
Returns: undefined
|
||||
}
|
||||
append_conversacion_plan: {
|
||||
Args: { p_append: Json; p_id: string }
|
||||
Returns: undefined
|
||||
}
|
||||
unaccent: { Args: { "": string }; Returns: string }
|
||||
unaccent_immutable: { Args: { "": string }; Returns: string }
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
/// <reference lib="deno.window" />
|
||||
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;
|
||||
@@ -15,7 +14,6 @@ export type StructuredResponseSuccess<TOutput = unknown> = {
|
||||
responseId: string;
|
||||
conversationId?: string | null;
|
||||
references: {
|
||||
uploadedToStorage: string[]; // supabase storage paths
|
||||
openaiFileIds: string[]; // file ids in OpenAI
|
||||
};
|
||||
openaiRaw: OpenAITypes.OpenAI.Responses.Response; // keep for advanced consumers
|
||||
@@ -24,7 +22,6 @@ export type StructuredResponseFailure = {
|
||||
ok: false;
|
||||
code:
|
||||
| "MissingEnv"
|
||||
| "StorageUploadFailed"
|
||||
| "OpenAIFileUploadFailed"
|
||||
| "OpenAIRequestFailed";
|
||||
message: string;
|
||||
@@ -34,40 +31,24 @@ export type StructuredResponseResult<TOutput = unknown> =
|
||||
| StructuredResponseSuccess<TOutput>
|
||||
| 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,
|
||||
bucket: string,
|
||||
) {
|
||||
this.supabase = client;
|
||||
private constructor(openai: OpenAI) {
|
||||
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) {
|
||||
if (!openAIApiKey) {
|
||||
return {
|
||||
ok: false,
|
||||
code: "MissingEnv",
|
||||
message:
|
||||
"Required env vars missing: SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, OPENAI_API_KEY",
|
||||
message: "Required env vars missing: OPENAI_API_KEY",
|
||||
};
|
||||
}
|
||||
const client = createClient(supabaseUrl, serviceRoleKey);
|
||||
const openai = new OpenAI({ apiKey: openAIApiKey });
|
||||
return new OpenAIService(client, openai, bucket);
|
||||
return new OpenAIService(openai);
|
||||
}
|
||||
async createConversation(metadata?: Record<string, string>) {
|
||||
const conversation = await this.openai.conversations.create({
|
||||
@@ -80,9 +61,6 @@ export class OpenAIService {
|
||||
files?: File[],
|
||||
): Promise<StructuredResponseResult<TOutput>> {
|
||||
try {
|
||||
const uploadedToStorage = await this.uploadFilesToStorage(
|
||||
files ?? [],
|
||||
);
|
||||
const openaiFileIds = await this.uploadFilesToOpenAI(files ?? []);
|
||||
|
||||
const newOptions = { ...options };
|
||||
@@ -136,7 +114,7 @@ export class OpenAIService {
|
||||
conversationId: conversationId
|
||||
? String(conversationId)
|
||||
: null,
|
||||
references: { uploadedToStorage, openaiFileIds },
|
||||
references: { openaiFileIds },
|
||||
openaiRaw,
|
||||
};
|
||||
}
|
||||
@@ -175,38 +153,18 @@ export class OpenAIService {
|
||||
usage,
|
||||
responseId: String(responseId),
|
||||
conversationId: conversationId ? String(conversationId) : null,
|
||||
references: { uploadedToStorage, openaiFileIds },
|
||||
references: { openaiFileIds },
|
||||
openaiRaw,
|
||||
};
|
||||
} catch (err) {
|
||||
const e = err as Error;
|
||||
const message = e?.message ?? "Unknown error";
|
||||
const code = message.includes("Supabase upload failed")
|
||||
? "StorageUploadFailed"
|
||||
: message.includes("OpenAI file upload failed")
|
||||
const code = message.includes("OpenAI file upload failed")
|
||||
? "OpenAIFileUploadFailed"
|
||||
: "OpenAIRequestFailed";
|
||||
return { ok: false, code, message, cause: err };
|
||||
}
|
||||
}
|
||||
private async uploadFilesToStorage(files: File[]): Promise<string[]> {
|
||||
const paths: string[] = [];
|
||||
for (const file of files) {
|
||||
const safeName = this.sanitizeFilename(file.name);
|
||||
const path = `${crypto.randomUUID()}-${safeName}`;
|
||||
const { data, error } = await this.supabase.storage
|
||||
.from(this.bucket)
|
||||
.upload(path, file, {
|
||||
contentType: file.type || "application/octet-stream",
|
||||
upsert: false,
|
||||
});
|
||||
if (error) {
|
||||
throw new Error(`Supabase upload failed: ${error.message}`);
|
||||
}
|
||||
paths.push(data.path);
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
private async uploadFilesToOpenAI(files: File[]): Promise<string[]> {
|
||||
const ids: string[] = [];
|
||||
for (const file of files) {
|
||||
|
||||
Reference in New Issue
Block a user