// server/supabase/functions/search-knowledge/index.ts // Composition root: wires the real adapters into the search-knowledge use case. import { createClient } from '@supabase/supabase-js' import { requireUser } from '../_shared/auth.ts' import { createServiceRoleClient } from '../_shared/quota.ts' import { readProviderKey } from '../_shared/provider-key.ts' import { createOpenAIEmbeddingProvider } from '../_shared/openai-embeddings.ts' import { createSupabaseEmbeddingUsageStore } from '../_shared/embedding-quota.ts' import { type ChunkSearcher, createSearchKnowledgeHandler, SIMILARITY_THRESHOLD } from './handler.ts' let serviceClient: ReturnType | null = null function service(): ReturnType { serviceClient ??= createServiceRoleClient() return serviceClient } /** match_knowledge_chunks runs as the caller so RLS limits results to their own chunks. */ const searchChunks: ChunkSearcher = async (req, embedding, count) => { 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 { ok: false, reason: '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: embedding, match_count: count, similarity_threshold: SIMILARITY_THRESHOLD, }) if (result.error) return { ok: false, reason: 'failed' } return { ok: true, results: Array.isArray(result.data) ? result.data : [] } } Deno.serve(createSearchKnowledgeHandler({ authenticate: requireUser, embeddingProvider: () => { const key = readProviderKey('OPENAI_API_KEY') return key ? createOpenAIEmbeddingProvider(key) : null }, usageStore: () => createSupabaseEmbeddingUsageStore(service()), searchChunks, }))