feat: implement create-chat-conversation function with OpenAI integration and Supabase support
CI / test (pull_request) Failing after 8s
CI / test (pull_request) Failing after 8s
- Added CORS handling for the create-chat-conversation function. - Implemented health check endpoint for the function. - Created endpoints for managing conversations and messages with OpenAI. - Added error handling and response formatting for better API usability. - Introduced utility functions for environment variable management and Supabase client creation. - Enhanced schema handling for structured responses from OpenAI. - Implemented conversation archiving and retrieval logic.
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
export const corsHeaders: Record<string, string> = {
|
||||
"access-control-allow-origin": "*",
|
||||
"access-control-allow-headers":
|
||||
"authorization, x-client-info, apikey, content-type",
|
||||
"access-control-allow-methods": "GET,POST,OPTIONS",
|
||||
};
|
||||
|
||||
export function withCors(res: Response) {
|
||||
const h = new Headers(res.headers);
|
||||
for (const [k, v] of Object.entries(corsHeaders)) h.set(k, v);
|
||||
return new Response(res.body, { status: res.status, headers: h });
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export function mustGetEnv(name: string): string {
|
||||
const v = Deno.env.get(name);
|
||||
if (!v) throw new Error(`Missing env var: ${name}`);
|
||||
return v;
|
||||
}
|
||||
|
||||
export function getEnv(name: string, fallback?: string): string | undefined {
|
||||
return Deno.env.get(name) ?? fallback;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
export class HttpError extends Error {
|
||||
status: number;
|
||||
code: string;
|
||||
details?: unknown;
|
||||
|
||||
constructor(
|
||||
status: number,
|
||||
code: string,
|
||||
message: string,
|
||||
details?: unknown,
|
||||
) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
this.code = code;
|
||||
this.details = details;
|
||||
}
|
||||
}
|
||||
|
||||
export function jsonResponse(
|
||||
body: unknown,
|
||||
status = 200,
|
||||
headers: HeadersInit = {},
|
||||
) {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: {
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
...headers,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import OpenAI from "npm:openai@6.16.0";
|
||||
import { mustGetEnv } from "./env.ts";
|
||||
|
||||
export function getOpenAI() {
|
||||
// OpenAI lib toma OPENAI_API_KEY de env automáticamente,
|
||||
// pero lo validamos para fallar rápido:
|
||||
mustGetEnv("OPENAI_API_KEY");
|
||||
return new OpenAI();
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { HttpError } from "./errors.ts";
|
||||
|
||||
export function pickSchemaFields(
|
||||
definicion: any,
|
||||
campos?: string[],
|
||||
) {
|
||||
if (!definicion || definicion.type !== "object" || !definicion.properties) {
|
||||
return definicion;
|
||||
}
|
||||
|
||||
const extra = {
|
||||
properties: {
|
||||
"ai-message": {
|
||||
type: "string",
|
||||
description:
|
||||
"Mensaje breve para el usuario final confirmando qué se mejoró y qué se hizo.",
|
||||
examples: [
|
||||
"Listo: mejoré la redacción del perfil de ingreso y propuse un tema de investigación alineado al plan.",
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let out = structuredClone(definicion);
|
||||
|
||||
// Si piden campos, filtramos propiedades/required a esos campos
|
||||
if (Array.isArray(campos) && campos.length > 0) {
|
||||
const entries = Object.entries(out.properties).filter(([k]) =>
|
||||
campos.includes(k)
|
||||
);
|
||||
out.properties = Object.fromEntries(entries);
|
||||
if (Array.isArray(out.required)) {
|
||||
out.required = out.required.filter((k: string) => campos.includes(k));
|
||||
}
|
||||
}
|
||||
|
||||
// Siempre agregamos ai-message
|
||||
out.properties = { ...out.properties, ...extra.properties };
|
||||
out.required = Array.isArray(out.required)
|
||||
? [...new Set([...out.required, ...Object.keys(extra.properties)])]
|
||||
: Object.keys(extra.properties);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
export function safePlanForPrompt(plan: any) {
|
||||
const copy = structuredClone(plan);
|
||||
if (copy?.estructuras_plan) delete copy.estructuras_plan;
|
||||
return copy;
|
||||
}
|
||||
|
||||
export function assertUuid(v: string, name: string) {
|
||||
// validación ligera
|
||||
if (!v || typeof v !== "string" || v.length < 10) {
|
||||
throw new HttpError(400, "bad_input", `Invalid ${name}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { createClient } from "jsr:@supabase/supabase-js";
|
||||
import { mustGetEnv } from "./env.ts";
|
||||
import { HttpError } from "./errors.ts";
|
||||
|
||||
export function getSupabaseServiceClient() {
|
||||
const url = mustGetEnv("SUPABASE_URL");
|
||||
const key = mustGetEnv("SUPABASE_SERVICE_ROLE_KEY");
|
||||
return createClient(url, key, { auth: { persistSession: false } });
|
||||
}
|
||||
|
||||
export function getSupabaseAnonClient(authHeader?: string) {
|
||||
const url = mustGetEnv("SUPABASE_URL");
|
||||
const key = mustGetEnv("SUPABASE_ANON_KEY");
|
||||
return createClient(url, key, {
|
||||
auth: { persistSession: false },
|
||||
global: authHeader ? { headers: { Authorization: authHeader } } : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
export async function requireUser(authHeader?: string) {
|
||||
if (!authHeader) {
|
||||
throw new HttpError(401, "missing_auth", "Missing Authorization header");
|
||||
}
|
||||
const anon = getSupabaseAnonClient(authHeader);
|
||||
const { data, error } = await anon.auth.getUser();
|
||||
if (error || !data?.user) {
|
||||
throw new HttpError(401, "invalid_auth", "Invalid or expired token", error);
|
||||
}
|
||||
return data.user;
|
||||
}
|
||||
Reference in New Issue
Block a user