packages feed

shikumi-eval-0.1.0.0: test/MetricLMSpec.hs

-- | Unit tests for the LM-backed metrics, run against mock interpreters with no
-- network: 'semanticSimilarity' over a pure 'Embedding' interpreter (identical /
-- orthogonal / opposite vectors), and 'modelJudge' over a constant mock LLM that
-- returns a fixed grade.
module MetricLMSpec (tests) where

import Data.Text (Text)
import Data.Vector (Vector)
import Data.Vector qualified as V
import Effectful (runEff, runPureEff)
import Effectful.Error.Static (runErrorNoCallStack)
import EvalFixtures (markerResponse, runConstLLM)
import Shikumi.Error (ShikumiError)
import Shikumi.Eval.Metric
  ( modelJudge,
    runEmbedding,
    semanticSimilarity,
  )
import Shikumi.Eval.Types
  ( Prediction,
    Score,
    mkScore,
    prediction,
    scoreOne,
    scoreZero,
  )
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertFailure, testCase, (@?=))

-- | A fixed embedding table: each label maps to a 2-D vector. "a" and "b" are
-- orthogonal (cosine 0 -> score 0.5); "c" is the opposite of "a" (cosine -1 ->
-- score 0); identical labels share a vector (cosine 1 -> score 1); "z" is the
-- zero vector (no signal -> score 0).
embedTable :: Text -> Vector Double
embedTable "a" = V.fromList [1, 0]
embedTable "b" = V.fromList [0, 1]
embedTable "c" = V.fromList [-1, 0]
embedTable _ = V.fromList [0, 0]

-- | Score the similarity of two text labels under the fixed embedding table.
sim :: Text -> Prediction Text -> Score
sim expd predd =
  runPureEff . runEmbedding embedTable $ semanticSimilarity id expd predd

tests :: TestTree
tests =
  testGroup
    "Metric (LM)"
    [ testCase "semanticSimilarity identical -> 1" $
        sim "a" (prediction "a") @?= scoreOne,
      testCase "semanticSimilarity orthogonal -> 0.5" $
        sim "a" (prediction "b") @?= mkScore 0.5,
      testCase "semanticSimilarity opposite -> 0" $
        sim "a" (prediction "c") @?= scoreZero,
      testCase "semanticSimilarity zero-norm -> 0" $
        sim "z" (prediction "z") @?= scoreZero,
      testCase "modelJudge returns the model's grade" $ do
        result <-
          runEff . runErrorNoCallStack @ShikumiError $
            runConstLLM (markerResponse [("grade", "0.7")]) $
              modelJudge "Grade the answer." id ("ref" :: Text) (prediction "cand")
        case result of
          Left e -> assertFailure ("unexpected error: " <> show e)
          Right s -> s @?= mkScore 0.7
    ]