package semantic import ( "bytes" "encoding/json" "fmt" "io" "net/http" "os" "time" ) // EmbedClient handles embedding API calls type EmbedClient struct { Endpoint string APIKey string Model string client *http.Client } // NewEmbedClient creates a new embedding client func NewEmbedClient() *EmbedClient { apiKey := os.Getenv("OPENAI_API_KEY") if apiKey == "" { apiKey = os.Getenv("BOE_EMBEDDING_KEY") } endpoint := os.Getenv("BOE_EMBEDDING_ENDPOINT") if endpoint == "" { endpoint = "https://api.openai.com/v1" } model := os.Getenv("BOE_EMBEDDING_MODEL") if model == "" { model = "text-embedding-3-small" } return &EmbedClient{ Endpoint: endpoint, APIKey: apiKey, Model: model, client: &http.Client{ Timeout: 30 * time.Second, }, } } // IsAvailable returns true if the client has an API key configured func (c *EmbedClient) IsAvailable() bool { return c.APIKey != "" } // QueryPrefix is prepended to search queries before embedding. // This bridges the vocabulary gap between casual user queries and formal legal titles. // Benchmarked at +14% retrieval accuracy (73% → 87%) with zero index rebuild. const QueryPrefix = "Legislación española, ley, real decreto sobre: " // Embed generates an embedding vector for the given text func (c *EmbedClient) Embed(text string) ([]float32, error) { return c.embed(text) } // EmbedQuery generates an embedding vector for a search query, // prepending the legal domain prefix for better retrieval. func (c *EmbedClient) EmbedQuery(query string) ([]float32, error) { return c.embed(QueryPrefix + query) } func (c *EmbedClient) embed(text string) ([]float32, error) { if !c.IsAvailable() { return nil, fmt.Errorf("no API key configured (set OPENAI_API_KEY or BOE_EMBEDDING_KEY)") } payload := map[string]interface{}{ "model": c.Model, "input": text, "dimensions": 1536, } body, err := json.Marshal(payload) if err != nil { return nil, err } req, err := http.NewRequest("POST", c.Endpoint+"/embeddings", bytes.NewReader(body)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+c.APIKey) resp, err := c.client.Do(req) if err != nil { return nil, fmt.Errorf("embedding API call failed: %w", err) } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != 200 { return nil, fmt.Errorf("embedding API error %d: %s", resp.StatusCode, string(respBody[:min(200, len(respBody))])) } var result struct { Data []struct { Embedding []float32 `json:"embedding"` } `json:"data"` } if err := json.Unmarshal(respBody, &result); err != nil { return nil, err } if len(result.Data) == 0 { return nil, fmt.Errorf("no embedding returned") } return result.Data[0].Embedding, nil } func min(a, b int) int { if a < b { return a } return b }