# frozen_string_literal: true # Embedder — turns text into vectors via OpenRouter's /embeddings endpoint (OpenAI-compatible), # reusing the same key + HTTP/auth pattern as the summariser. Standard library only. The HTTP transport is # injectable so the RAG index/retrieval is unit-testable offline (no network/key). require "json" require "net/http" require "uri" module Helpdesk class Embedder MODEL = "openai/text-embedding-3-small" # 1536-dim, ~$0.02/M tokens, handles Czech URL = "https://openrouter.ai/api/v1/embeddings" def initialize(api_key: ENV["OPENROUTER_API_KEY"], model: MODEL, transport: nil) @api_key = api_key @model = model @transport = transport # ->(request_body_json) { response_body_json } — for tests end # texts: String | Array → Array> (order preserved). Raises on API error. def embed(texts) arr = texts.is_a?(Array) ? texts : [texts] return [] if arr.empty? body = JSON.generate(model: @model, input: arr) raw = @transport ? @transport.call(body) : post(body) data = JSON.parse(raw) raise "embedding error: #{data["error"]}" if data["error"] data.fetch("data").sort_by { |d| d["index"] }.map { |d| d.fetch("embedding") } end def embed_one(text) = embed(text).first private def post(body) uri = URI(URL) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true http.open_timeout = 15 http.read_timeout = 60 req = Net::HTTP::Post.new(uri) req["Authorization"] = "Bearer #{@api_key}" req["Content-Type"] = "application/json" req.body = body http.request(req).body end end end