packages feed

shikumi-trace-0.2.0.0: test/TraceFixtures.hs

-- | Shared, network-free fixtures for the EP-7 trace tests: a stub 'Model', a
-- request builder, canned responses, and two base @LLM@ interpreters — a /fixed/
-- one that returns the same response for every call, and a /keyed/ one that picks
-- a response from the request 'Context' (so a two-stage pipeline gets distinct
-- responses, hence distinct cache keys).
module TraceFixtures
  ( stubModel,
    ctxFor,
    optsFor,
    mkResponse,
    responseText,
    lastUserText,
    runFixedLLM,
    runKeyedLLM,
    runKeyedCountingLLM,
    runSequencedLLM,
  )
where

import Baikai
  ( Api (Custom),
    AssistantContent (..),
    Context,
    Message (UserMessage),
    Model,
    Options,
    Response,
    TextContent (..),
    UserContent (UserText),
    user,
    _Context,
    _Model,
    _Options,
    _Response,
    _TextContent,
  )
import Control.Lens ((&), (.~), (^.))
import Data.Generics.Labels ()
import Data.IORef (IORef, atomicModifyIORef')
import Data.Text (Text)
import Data.Vector qualified as V
import Effectful (Eff, IOE, liftIO, (:>))
import Effectful.Dispatch.Dynamic (interpret)
import Shikumi.LLM (LLM (..))

-- | A stub model with real-looking routing identity, so LM-call span labels read
-- @stub/stub-model@.
stubModel :: Model
stubModel =
  _Model
    & #provider .~ "stub"
    & #modelId .~ "stub-model"
    & #api .~ Custom "stub"

-- | A one-user-turn request context carrying @t@ (varies the cache key per stage).
ctxFor :: Text -> Context
ctxFor t = _Context & #messages .~ V.singleton (user t)

-- | Default options.
optsFor :: Options
optsFor = _Options

-- | A response carrying @t@ as its single assistant text block, with small fixed
-- token counts and latency so span attributes are populated.
mkResponse :: Text -> Response
mkResponse t =
  _Response
    & #message . #content .~ V.singleton (AssistantText (_TextContent & #text .~ t))
    & #message . #usage . #inputTokens .~ 10
    & #message . #usage . #outputTokens .~ 5
    & #latencyMs .~ 12

-- | Concatenate the assistant text blocks of a response.
responseText :: Response -> Text
responseText resp =
  mconcat [t | AssistantText (TextContent t) <- V.toList (resp ^. #message . #content)]

-- | Concatenate the text of every user message in a request context.
lastUserText :: Context -> Text
lastUserText c =
  mconcat
    [ t
    | UserMessage up <- V.toList (c ^. #messages),
      UserText (TextContent t) <- V.toList (up ^. #content)
    ]

-- | A base @LLM@ interpreter that returns the same response for every completion.
runFixedLLM :: Response -> Eff (LLM : es) a -> Eff es a
runFixedLLM resp = interpret $ \_ -> \case
  Complete {} -> pure resp
  Stream {} -> pure []

-- | A base @LLM@ interpreter that picks a response from the request context.
runKeyedLLM :: (Context -> Response) -> Eff (LLM : es) a -> Eff es a
runKeyedLLM f = interpret $ \_ -> \case
  Complete _ c _ -> pure (f c)
  Stream {} -> pure []

-- | Like 'runKeyedLLM' but bumps a counter on every completion (used to assert how
-- many provider calls a live run made — and that a replay run makes zero).
runKeyedCountingLLM :: (IOE :> es) => IORef Int -> (Context -> Response) -> Eff (LLM : es) a -> Eff es a
runKeyedCountingLLM ref f = interpret $ \_ -> \case
  Complete _ c _ -> liftIO (atomicModifyIORef' ref (\n -> (n + 1, ()))) >> pure (f c)
  Stream {} -> pure []

-- | A base @LLM@ interpreter that returns and removes the next response from an
-- 'IORef' list on each completion. Used for retry tests that need attempt one
-- and attempt two to receive different payloads.
runSequencedLLM :: (IOE :> es) => IORef [Response] -> Eff (LLM : es) a -> Eff es a
runSequencedLLM ref = interpret $ \_ -> \case
  Complete {} ->
    liftIO $
      atomicModifyIORef' ref $ \case
        r : rs -> (rs, r)
        [] -> ([], mkResponse "runSequencedLLM: exhausted")
  Stream {} -> pure []