packages feed

shikumi-trace-0.3.2.0: test/DecisionReplaySpec.hs

module DecisionReplaySpec (tests) where

import Baikai qualified as B
import Control.Lens ((^.))
import Data.Aeson (eitherDecode, encode)
import Data.Generics.Labels ()
import Data.Map.Strict qualified as Map
import Data.Vector qualified as V
import Effectful (runEff)
import Effectful.Dispatch.Dynamic (interpret)
import Effectful.Error.Static (runErrorNoCallStack)
import Effectful.Exception (try)
import Effectful.Prim (runPrim)
import Shikumi.Decision
import Shikumi.Decision.Setting
import Shikumi.Effect.Time (runTime)
import Shikumi.Error (ShikumiError)
import Shikumi.LLM (LLM (..))
import Shikumi.LLM qualified
import Shikumi.Program
import Shikumi.Routing
import Shikumi.Schema (field)
import Shikumi.Stream
import Shikumi.Testing.DecisionFixtures
import Shikumi.Testing.Response (mkTextResponse)
import Shikumi.Testing.SystemOne
import Shikumi.Trace (runTrace, tracedLLM)
import Shikumi.Trace.Replay (ReplayDivergence, runLLMReplay)
import Shikumi.Trace.Store (replayIndex)
import Test.Tasty
import Test.Tasty.HUnit

tests :: TestTree
tests =
  testGroup "Decision replay" $
    [ testCase ("capture " ++ show capturedStream ++ ", replay " ++ show replayStream) $ do
        let program = Predict assessmentSig emptyParams
            input = Ticket (field "x")
            response = systemOneResponse [("urgent", FieldNoul (NoulEvidence 0.6)), ("severity", FieldScore (ScoreEvidence (V.fromList [0.1, 0.3, 0.6]) (Just 0.55))), ("category", FieldChoice (ChoiceEvidence (Map.fromList [("billing", 0.2), ("technical", 0.8)]) (Just 0.65)))]
            execute streaming p = if streaming then streamProgram p input (const (pure ())) else runProgram p input
            handler = interpret $ \_ -> \case
              Complete {} -> pure response
              Stream {} -> maybe (error "fixture") pure (atomicEventsFromResponse response)
        recorded <- runEff . runPrim . runTime . runErrorNoCallStack @ShikumiError . runRouting (systemOneModel "jev-latest") . runTrace . handler . tracedLLM . routeLLM $ execute capturedStream program
        case recorded of
          Left err -> assertFailure (show err)
          Right (before, tree) -> do
            -- Serialization is part of acceptance, including distributions,
            -- confidence and method. The response codec deliberately drops evidence.
            eitherDecode (encode response) @?= Right response
            restored <- either assertFailure pure (eitherDecode (encode tree))
            case replayIndex restored of
              Left err -> assertFailure (show err)
              Right index -> do
                replayed <- runEff . runErrorNoCallStack @ShikumiError . runRouting (systemOneModel "jev-latest") . runLLMReplay index . routeLLM $ execute replayStream program
                replayed @?= Right before
                calibrated <-
                  runEff . runErrorNoCallStack @ShikumiError . runRouting (systemOneModel "jev-latest") . runLLMReplay index . routeLLM $
                    execute replayStream (mapParams (setDecisionSetting "severity" (ScoreSetting [0.5, 1.6])) program)
                case calibrated of
                  Left err -> assertFailure (show err)
                  Right after -> assertBool "cuts rederive level" (after /= before)
    | capturedStream <- [False, True],
      replayStream <- [False, True]
    ]
      ++ [ testCase "text-only recording cannot become a structured stream" $ do
             let text = mkTextResponse "hello"
             atomicEventsFromResponse text @?= Nothing
             result <- runEff . try @ReplayDivergence . runLLMReplay Map.empty $ B.emptyResponse <$ Shikumi.LLM.stream B.emptyModel B.emptyContext B.emptyOptions
             assertBool "divergence" (case result of Left _ -> True; _ -> False)
         ]