inicio de funcion de generación de asignatura

This commit is contained in:
2026-02-04 16:35:28 -06:00
parent e5657d6b0f
commit 7660d2c3a0
5 changed files with 54 additions and 24 deletions
+11
View File
@@ -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" ]
+12 -24
View File
@@ -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<TOutput = unknown> = {
ok: true;
output?: TOutput; // parsed JSON when available
@@ -22,7 +20,6 @@ export type StructuredResponseSuccess<TOutput = unknown> = {
};
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<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,
@@ -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<string, string>) {
const conversation = await this.openai.conversations.create({
metadata,
});
return conversation;
}
async createStructuredResponse<TOutput = unknown>(
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<string[]> {
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<string[]> {
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")
@@ -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" } },
);
});