Barazo AppView backend barazo.forum
at main 85 lines 2.7 kB view raw
1import type { Logger } from '../lib/logger.js' 2 3// --------------------------------------------------------------------------- 4// Types 5// --------------------------------------------------------------------------- 6 7export interface EmbeddingService { 8 /** Generate an embedding vector for the given text. Returns null on failure or when disabled. */ 9 generateEmbedding(text: string): Promise<number[] | null> 10 /** Whether the embedding service is configured and available. */ 11 isEnabled(): boolean 12} 13 14/** OpenAI-compatible embedding response. */ 15interface EmbeddingResponse { 16 data: ReadonlyArray<{ embedding: number[] }> 17} 18 19// --------------------------------------------------------------------------- 20// Implementation 21// --------------------------------------------------------------------------- 22 23/** 24 * Create an embedding service that calls an OpenAI-compatible embedding API. 25 * 26 * If `embeddingUrl` is undefined or empty, the service operates in disabled mode: 27 * `isEnabled()` returns false and `generateEmbedding()` always returns null. 28 * 29 * On network or API errors the service logs a warning and returns null -- it 30 * never throws. This allows the search route to gracefully degrade to 31 * full-text-only search when the embedding backend is unavailable. 32 */ 33export function createEmbeddingService( 34 embeddingUrl: string | undefined, 35 dimensions: number, 36 logger: Logger 37): EmbeddingService { 38 const enabled = typeof embeddingUrl === 'string' && embeddingUrl.length > 0 39 40 return { 41 isEnabled(): boolean { 42 return enabled 43 }, 44 45 async generateEmbedding(text: string): Promise<number[] | null> { 46 if (!enabled || !embeddingUrl) { 47 return null 48 } 49 50 try { 51 const response = await fetch(embeddingUrl, { 52 method: 'POST', 53 headers: { 'Content-Type': 'application/json' }, 54 body: JSON.stringify({ 55 input: text, 56 model: 'default', 57 dimensions, 58 }), 59 signal: AbortSignal.timeout(10_000), 60 }) 61 62 if (!response.ok) { 63 logger.warn( 64 { status: response.status, url: embeddingUrl }, 65 'Embedding API returned non-OK status' 66 ) 67 return null 68 } 69 70 const body = (await response.json()) as EmbeddingResponse 71 const embedding = body.data[0]?.embedding 72 73 if (!Array.isArray(embedding) || embedding.length === 0) { 74 logger.warn('Embedding API returned empty or invalid embedding') 75 return null 76 } 77 78 return embedding 79 } catch (err: unknown) { 80 logger.warn({ err }, 'Failed to generate embedding') 81 return null 82 } 83 }, 84 } 85}