packages feed

shikumi-eval-0.1.0.0: test/EvalFixtures.hs

-- | Shared, network-free fixtures for the @evaluate@ / golden specs: a
-- @Question -> Answer@ signature and program, canned fallback-adapter responses,
-- and two mock @LLM@ interpreters — a constant one that returns a fixed response
-- for every call (order-independent, used for the aggregate-score acceptance) and
-- a scripted one that pops replies in order and can inject a typed failure (used
-- for the failure-policy cases under @concurrency = 1@).
module EvalFixtures
  ( -- * Records
    Question (..),
    Answer (..),

    -- * Signature and program
    qaSig,
    qaProg,

    -- * Canned responses
    answerResponse,
    markerResponse,

    -- * Mock LLM interpreters
    MockReply (..),
    runConstLLM,
    runScriptedLLM,
  )
where

import Baikai (AssistantContent (..), Response, _Response, _TextContent)
import Control.Lens ((&), (.~))
import Data.Generics.Labels ()
import Data.IORef (atomicModifyIORef', newIORef)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Vector qualified as V
import Effectful (Eff, IOE, liftIO, type (:>))
import Effectful.Dispatch.Dynamic (interpret)
import Effectful.Error.Static (Error, throwError)
import GHC.Generics (Generic)
import Shikumi.Adapter (ToPrompt)
import Shikumi.Error (ShikumiError (..))
import Shikumi.LLM (LLM (..))
import Shikumi.Module (predict)
import Shikumi.Program (Program)
import Shikumi.Schema (FromModel, ToSchema, Validatable)
import Shikumi.Signature (Signature, mkSignature)

-- ---------------------------------------------------------------------------
-- Records
-- ---------------------------------------------------------------------------

newtype Question = Question {question :: Text}
  deriving stock (Generic, Show, Eq)

instance ToSchema Question

instance FromModel Question

instance ToPrompt Question

instance Validatable Question

newtype Answer = Answer {answer :: Text}
  deriving stock (Generic, Show, Eq)

instance ToSchema Answer

instance FromModel Answer

instance ToPrompt Answer

instance Validatable Answer

-- ---------------------------------------------------------------------------
-- Signature and program
-- ---------------------------------------------------------------------------

qaSig :: Signature Question Answer
qaSig = mkSignature "Answer the question."

-- | The simplest program: a single predict node. Its behaviour is fully
-- determined by the mock LLM the test wires underneath it.
qaProg :: Program Question Answer
qaProg = predict qaSig

-- ---------------------------------------------------------------------------
-- Canned responses (rendered as the fallback adapter's [[ ## field ## ]] form)
-- ---------------------------------------------------------------------------

-- | An assistant 'Response' whose body decodes (via the fallback adapter) to
-- @Answer t@.
answerResponse :: Text -> Response
answerResponse t = mkResponse (markerBody [("answer", t)])

-- | An assistant 'Response' whose body decodes (via the fallback adapter) to a
-- record with the given @(field, value)@ sections.
markerResponse :: [(Text, Text)] -> Response
markerResponse = mkResponse . markerBody

-- | Build a fallback-style response body from @(field, value)@ sections.
markerBody :: [(Text, Text)] -> Text
markerBody fields = T.unlines (concatMap sect fields ++ ["[[ ## completed ## ]]"])
  where
    sect (k, v) = ["[[ ## " <> k <> " ## ]]", v]

-- | An assistant 'Response' carrying @t@ as its single text block.
mkResponse :: Text -> Response
mkResponse t =
  _Response & #message . #content .~ V.singleton (AssistantText (_TextContent & #text .~ t))

-- ---------------------------------------------------------------------------
-- Mock LLM interpreters
-- ---------------------------------------------------------------------------

-- | One scripted reply: a canned success or a typed failure.
data MockReply
  = MockOk Response
  | MockFail ShikumiError

-- | Return @resp@ for every completion, ignoring the request. Order-independent,
-- so it is safe under any concurrency.
runConstLLM :: Response -> Eff (LLM : es) a -> Eff es a
runConstLLM resp = interpret $ \_ -> \case
  Complete {} -> pure resp
  Stream {} -> pure []

-- | Pop scripted replies in order; a 'MockFail' is thrown through
-- @Error ShikumiError@. Deterministic only under @concurrency = 1@.
runScriptedLLM ::
  (IOE :> es, Error ShikumiError :> es) =>
  [MockReply] ->
  Eff (LLM : es) a ->
  Eff es a
runScriptedLLM replies act = do
  ref <- liftIO (newIORef replies)
  interpret
    ( \_ -> \case
        Complete {} -> do
          r <- liftIO (atomicModifyIORef' ref pop)
          case r of
            MockOk resp -> pure resp
            MockFail e -> throwError e
        Stream {} -> pure []
    )
    act
  where
    pop (x : xs) = (xs, x)
    pop [] = ([], MockFail (ProviderFailure "mock: script exhausted"))