packages feed

shikumi-eval-0.1.0.0: src/Shikumi/Eval/Embedding.hs

{-# LANGUAGE TypeApplications #-}

-- | A real embeddings interpreter for the existing 'Embedding' effect (EP-15).
--
-- 'Shikumi.Eval.Metric' ships only the pure 'Shikumi.Eval.Metric.runEmbedding'
-- (a @Text -> Vector Double@ table), so @semanticSimilarity@ has no way to call a
-- real backend. This module adds interpreters that drive the upstream
-- 'Baikai.Embedding' client (an OpenAI-compatible @\/v1\/embeddings@ endpoint),
-- so @semanticSimilarity@ runs end to end. The @Embedding@ effect itself is
-- unchanged (integration point #5); only new interpreters are added.
--
-- 'runEmbeddingBy' injects the effectful boundary (the same batching shape as
-- 'Baikai.Embedding.embed') so the production interpreter ('runEmbeddingWith') and
-- a hermetic stub share identical effect-handling, error-wrapping, and
-- vector-extraction logic — only the embedder differs. A transport failure becomes
-- a typed 'Shikumi.Error.ProviderFailure' (hence the @Error ShikumiError@ row),
-- and @IOE@ is introduced only here, at the bottom of the stack.
module Shikumi.Eval.Embedding
  ( runEmbeddingBy,
    runEmbeddingWith,
    runEmbeddingLLM,
  )
where

import Baikai.Embedding (EmbeddingModel, embed, openAIEmbeddingModel)
import Control.Exception (SomeException, displayException, try)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Vector (Vector)
import Data.Vector qualified as V
import Effectful (Eff, IOE, liftIO, (:>))
import Effectful.Dispatch.Dynamic (interpret)
import Effectful.Error.Static (Error, throwError)
import Shikumi.Error (ShikumiError (..))
import Shikumi.Eval.Metric (Embedding (..))

-- | Interpret 'Embedding' with an explicit batching embedder (the same shape as
-- 'Baikai.Embedding.embed'). The production interpreter fixes it to the baikai
-- client; a deterministic stub drives it in hermetic tests. A transport exception
-- is caught and rethrown as a typed 'ProviderFailure'.
runEmbeddingBy ::
  (IOE :> es, Error ShikumiError :> es) =>
  ([Text] -> IO (Vector (Vector Double))) ->
  Eff (Embedding : es) a ->
  Eff es a
runEmbeddingBy embedder = interpret $ \_ -> \case
  EmbedText t -> do
    r <- liftIO (try @SomeException (embedder [t]))
    case r of
      Left e -> throwError (ProviderFailure (T.pack (displayException e)))
      Right vs -> case V.toList vs of
        (v : _) -> pure v
        [] -> throwError (ProviderFailure "embedding backend returned no vector")

-- | Interpret 'Embedding' against a concrete embeddings backend.
runEmbeddingWith ::
  (IOE :> es, Error ShikumiError :> es) =>
  EmbeddingModel ->
  Eff (Embedding : es) a ->
  Eff es a
runEmbeddingWith m = runEmbeddingBy (embed m)

-- | Interpret 'Embedding' against OpenAI's @text-embedding-3-small@ (the default).
runEmbeddingLLM ::
  (IOE :> es, Error ShikumiError :> es) =>
  Eff (Embedding : es) a ->
  Eff es a
runEmbeddingLLM = runEmbeddingWith (openAIEmbeddingModel "text-embedding-3-small")