// deno-lint-ignore no-import-prefix import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.39.7' import { corsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts' import { requireUser, authErrorResponse, type AuthError } from '../_shared/auth.ts' const EMBEDDING_DIMENSIONS = 1536 const PROVIDER_TIMEOUT_MS = 45_000 function json(status: number, body: Record): Response { return new Response(JSON.stringify(body), { status, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }) } function isEmbedding(value: unknown): value is number[] { return Array.isArray(value) && value.length === EMBEDDING_DIMENSIONS && value.every((entry) => typeof entry === 'number' && Number.isFinite(entry)) } Deno.serve(async (req: Request) => { const preflight = handleCorsPreflightRequest(req) if (preflight) return preflight if (req.method !== 'POST') return json(405, { error: 'method_not_allowed' }) try { await requireUser(req) const body = await req.json().catch(() => null) as { query?: unknown count?: unknown } | null const query = typeof body?.query === 'string' ? body.query.trim() : '' const count = body?.count === undefined ? 5 : body.count if (!query || query.length > 4_000) return json(400, { error: 'invalid_query' }) if (typeof count !== 'number' || !Number.isInteger(count) || count < 1 || count > 20) { return json(400, { error: 'invalid_count' }) } const openaiKey = Deno.env.get('OPENAI_API_KEY') ?? '' if (!openaiKey) return json(503, { error: 'embedding_provider_unavailable' }) let embeddingResponse: Response try { embeddingResponse = await fetch('https://api.openai.com/v1/embeddings', { method: 'POST', headers: { Authorization: `Bearer ${openaiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ model: 'text-embedding-3-small', input: query, dimensions: EMBEDDING_DIMENSIONS, }), signal: AbortSignal.timeout(PROVIDER_TIMEOUT_MS), }) } catch { return json(502, { error: 'embedding_upstream_failed' }) } if (!embeddingResponse.ok) return json(502, { error: 'embedding_upstream_failed' }) const embeddingPayload = await embeddingResponse.json().catch(() => null) as { data?: Array<{ embedding?: unknown }> } | null const queryEmbedding = embeddingPayload?.data?.[0]?.embedding if (!isEmbedding(queryEmbedding)) return json(502, { error: 'embedding_response_invalid' }) const authHeader = req.headers.get('Authorization') ?? '' const supabaseUrl = Deno.env.get('SUPABASE_URL') ?? '' const anonKey = Deno.env.get('SUPABASE_ANON_KEY') ?? '' if (!supabaseUrl || !anonKey) return json(503, { error: 'knowledge_storage_unavailable' }) const userClient = createClient(supabaseUrl, anonKey, { global: { headers: { Authorization: authHeader } }, auth: { persistSession: false, autoRefreshToken: false }, }) const result = await userClient.rpc('match_knowledge_chunks', { query_embedding: queryEmbedding, match_count: count, similarity_threshold: 0.5, }) if (result.error) return json(500, { error: 'knowledge_search_failed' }) return json(200, { results: result.data ?? [] }) } catch (error) { if ( error && typeof error === 'object' && 'status' in error && (error.status === 401 || error.status === 403) && 'message' in error && typeof error.message === 'string' ) { return authErrorResponse(error as AuthError, corsHeaders) } return json(500, { error: 'internal_error' }) } })