baikai-0.9.0.0: test/JudgmentSpec.hs
module JudgmentSpec (tests) where
import Baikai hiding (confidence, format, kind, name, probabilities)
import Baikai.Judgment qualified as J
import Baikai.Models.Generated qualified as Models
import Control.Lens ((&), (.~), (^.))
import Control.Monad (forM_)
import Data.Aeson qualified as A
import Data.Aeson.Key qualified
import Data.Aeson.KeyMap qualified as KM
import Data.ByteString.Lazy qualified as BL
import Data.Either (isLeft)
import Data.Generics.Labels ()
import Data.Map.Strict qualified as M
import Data.Text (Text)
import Data.Text qualified
import Data.Vector qualified as V
import FetchModelsCore qualified as F
import GenModelsCore qualified as G
import Test.Tasty
import Test.Tasty.HUnit
schemaFixture :: A.Value
schemaFixture =
A.object
[ "type" A..= ("object" :: Text),
"additionalProperties" A..= False,
"required" A..= (["urgent", "category", "severity"] :: [Text]),
"properties"
A..= A.object
[ "urgent" A..= A.object ["type" A..= ("boolean" :: Text), "description" A..= ("Is service blocked?" :: Text)],
"category" A..= branches [A.String "billing", A.String "technical"] [A.String "Payment issue", A.String "Product malfunction"] "Classify the issue.",
"severity" A..= branches (map A.toJSON [0 :: Int, 1, 2]) (map A.String ["Minor", "Disruptive", "Blocking"]) "Rate impact."
]
]
branches :: [A.Value] -> [A.Value] -> Text -> A.Value
branches vs ds instr = A.object ["description" A..= instr, "anyOf" A..= [A.object ["const" A..= v, "description" A..= d] | (v, d) <- zip vs ds]]
format :: A.Value -> JsonSchemaFormat
format = jsonSchemaFormat "judgments"
opts :: Options
opts = emptyOptions & #responseFormat .~ Just (JsonSchema (format schemaFixture))
ctx :: Context
ctx = contextOf [userData (A.object ["ticket" A..= ("Checkout is unavailable." :: Text)])]
answers :: M.Map Text JudgmentAnswer
answers =
M.fromList
[ ("urgent", PredicateAnswer 0.83),
("category", ChoiceAnswer "technical" (M.fromList [("billing", 0.2), ("technical", 0.7)]) (Just 0.65)),
("severity", ScoreAnswer (Just 1.5) (M.fromList [("0", 0.1), ("1", 0.3), ("2", 0.6)]) (Just 0.55))
]
withProperty :: A.Value -> A.Value
withProperty v = A.object ["type" A..= ("object" :: Text), "properties" A..= A.object ["x" A..= v]]
enum :: [A.Value] -> A.Value
enum vs = A.object ["enum" A..= vs]
compiled :: IO Judgments
compiled = either (assertFailure . show) pure (compileJudgments (format schemaFixture))
assertPath :: Text -> Either BaikaiError a -> IO ()
assertPath p r = case r of
Left e -> e ^. #requestProblem @?= Just (UnsupportedFeature p)
Right _ -> assertFailure "expected refusal"
tests :: TestTree
tests =
testGroup
"neutral judgments"
[ testCase "fetch render preserves curated judgment output and data modality" $ do
let m = F.CatalogModel "jev-latest" "Jev" False [InputText, InputData] JudgmentOutput (F.CatalogCost 0 0 0 0) Nothing Nothing 0 0 Nothing Nothing
c = F.Catalog "typesafe" "https://api.typesafe.ai" "typesafe-system-one" [m]
case A.eitherDecode (BL.fromStrict (F.renderCatalog c)) :: Either String G.CatalogFile of
Left e -> assertFailure e
Right catalog -> case G.flattenEntries catalog of
[(_, g)] -> do G.outputKind g @?= JudgmentOutput; G.input g @?= [InputText, InputData]
_ -> assertFailure "one model required",
testCase "catalog facts and generative legacy default" $ do
forM_ [Models.typesafe_jev_latest, Models.typesafe_jev_preview] $ \m -> do
isJudgmentOnly m @?= True
m ^. #api @?= TypeSafeSystemOne
m ^. #input @?= [InputText, InputData]
A.eitherDecode (A.encode m) @?= Right m
forM_ Models.allModels $ \m -> isJudgmentOnly m @?= (m ^. #provider == "typesafe")
let old = case A.toJSON emptyModel of A.Object o -> A.Object (KM.delete "outputKind" o); v -> v
fmap (^. #outputKind) ((A.fromJSON old :: A.Result Model)) @?= A.Success GenerativeOutput,
testCase "ordered fixture with exact instructions and criteria; deterministic fallback" $ do
js <- compiled
V.toList (judgmentList js)
@?= [ Judgment "urgent" (A.String "Is service blocked?") (PredicateJudgment Nothing),
Judgment "category" (A.String "Classify the issue.") (ChoiceJudgment (V.fromList [("billing", Just (A.String "Payment issue")), ("technical", Just (A.String "Product malfunction"))])),
Judgment "severity" (A.String "Rate impact.") (ScoreJudgment (V.fromList (map (Just . A.String) ["Minor", "Disruptive", "Blocking"])))
]
let noRequired = case schemaFixture of A.Object o -> A.Object (KM.delete "required" o); v -> v
fallback <- either (assertFailure . show) pure (compileJudgments (format noRequired))
map (^. #name) (V.toList (judgmentList fallback)) @?= ["category", "severity", "urgent"],
testCase "predicate criteria and neutral non-string content are preserved" $ do
forM_ [A.String "", A.Bool False, A.Number 0, A.object [], A.toJSON ([] :: [Int])] $ \d -> do
let p = A.object ["description" A..= d, "oneOf" A..= [A.object ["const" A..= False, "description" A..= ("no" :: Text)], A.object ["const" A..= True, "description" A..= ("yes" :: Text)]]]
js <- either (assertFailure . show) pure (compileJudgments (format (withProperty p)))
lookupJudgment "x" js @?= Just (Judgment "x" d (PredicateJudgment (Just (A.String "yes", A.String "no")))),
testCase "all unsupported schema shapes and boundary sizes" $ do
forM_ [A.object [], A.object ["type" A..= ("string" :: Text)], A.object ["type" A..= ("object" :: Text)], enum [], enum [A.String "x", A.String "x"], enum [A.Bool True, A.String "x"], enum (map A.toJSON [0 :: Int, 2]), enum (map A.toJSON [1 :: Int, 2, 3]), enum [A.Number 0], enum (map A.toJSON [0 :: Int .. 10]), enum [A.Number 0, A.Number 1.5], A.object ["allOf" A..= ([] :: [Int])], A.object ["anyOf" A..= [A.object []]], A.object ["type" A..= ("boolean" :: Text), "enum" A..= [A.String "x"]]] $ \p ->
assertBool (show p) (isLeft (compileJudgments (format (withProperty p))))
forM_ [1, 255] $ \n -> assertBool "choice boundary accepted" (not (isLeft (compileJudgments (format (withProperty (enum [A.String (fromString (show i)) | i <- [1 .. n :: Int]]))))))
forM_ [2, 10] $ \n -> assertBool "score boundary accepted" (not (isLeft (compileJudgments (format (withProperty (enum (map A.toJSON [0 .. n - 1 :: Int])))))))
assertPath "Options.responseFormat.schema.properties.x" (compileJudgments (format (withProperty (enum [A.String (fromString (show i)) | i <- [1 .. 256 :: Int]]))))
forM_ [("required", A.toJSON (["missing"] :: [Text])), ("additionalProperties", A.Bool True), ("$ref", A.String "#defs/x")] $ \(k, v) -> do
let s = case schemaFixture of A.Object o -> A.Object (KM.insert k v o); x -> x
assertPath ("Options.responseFormat.schema." <> fromString (showKey k)) (compileJudgments (format s)),
testCase "every explicit generation control is refused by path" $ do
let controls = [("maxTokens", opts & #maxTokens .~ Just 1), ("temperature", opts & #temperature .~ Just 0), ("topP", opts & #topP .~ Just 1), ("stopSequences", opts & #stopSequences .~ ["x"]), ("seed", opts & #seed .~ Just 1), ("frequencyPenalty", opts & #frequencyPenalty .~ Just 0), ("presencePenalty", opts & #presencePenalty .~ Just 0), ("thinking", opts & #thinking .~ Just ThinkingLow), ("cacheRetention", opts & #cacheRetention .~ Just CacheRetentionShort), ("speed", opts & #speed .~ Just SpeedStandard)]
forM_ controls $ \(field, o) -> assertPath ("Options." <> field) (prepareJudgment ctx o)
forM_ [ToolChoiceAuto, ToolChoiceNone, ToolChoiceRequired, ToolChoiceSpecific "x"] $ \choice -> assertPath "Options.toolChoice" (prepareJudgment ctx (opts & #toolChoice .~ Just choice))
assertBool "explicit no caching accepted" (not (isLeft (prepareJudgment ctx (opts & #cacheRetention .~ Just CacheRetentionNone)))),
testCase "context checks precede option and schema checks" $ do
assertPath "Context.systemPrompt" (prepareJudgment (ctx & #systemPrompt .~ Just "") emptyOptions)
forM_ [contextOf [], contextOf [user "x", user "y"]] $ \c -> assertPath "Context.messages" (prepareJudgment c opts)
assertPath "Context.messages[0]" (prepareJudgment (contextOf [assistant "x"]) opts)
assertPath "Context.messages[0]" (prepareJudgment (contextOf [toolResult "call" "tool" "result" False]) opts)
assertPath "Context.tools" (prepareJudgment (ctx & #tools .~ V.singleton emptyTool) opts)
forM_ [emptyOptions, emptyOptions & #responseFormat .~ Just JsonObject] $ \o -> assertPath "Options.responseFormat" (prepareJudgment ctx o),
testCase "complete data preserves provider selection, expected score and non-normalized probabilities" $ do
js <- compiled
d <- either (assertFailure . show) pure (validateAnswers js answers)
d ^. #value @?= A.object ["urgent" A..= True, "category" A..= ("technical" :: Text), "severity" A..= (1.5 :: Double)]
d ^. #confidence @?= Just (M.fromList [("category", 0.65), ("severity", 0.55)])
A.eitherDecode (A.encode d) @?= Right d
let other = M.insert "category" (ChoiceAnswer "billing" (M.fromList [("billing", 0.2), ("technical", 0.7)]) Nothing) answers
selected <- either (assertFailure . show) pure (validateAnswers js other)
case selected ^. #value of A.Object o -> KM.lookup "category" o @?= Just (A.String "billing"); _ -> assertFailure "object required"
tied <- either (assertFailure . show) pure (validateAnswers js (M.insert "severity" (ScoreAnswer Nothing (M.fromList [("0", 0.5), ("1", 0.5), ("2", 0)]) Nothing) answers))
case tied ^. #value of A.Object o -> KM.lookup "severity" o @?= Just (A.Number 0); _ -> assertFailure "object required",
testCase "malformed answer names, kinds, distributions, selected labels and numeric evidence" $ do
js <- compiled
forM_ [M.delete "urgent" answers, M.insert "extra" (PredicateAnswer 0.5) answers, M.insert "category" (PredicateAnswer 0.5) answers, M.insert "category" (ChoiceAnswer "other" (M.fromList [("billing", 0.2), ("technical", 0.7)]) Nothing) answers, M.insert "category" (ChoiceAnswer "billing" M.empty Nothing) answers] $ \as ->
case validateAnswers js as of Left e -> e ^. #category @?= DecodeFailure; Right _ -> assertFailure "expected failure"
forM_ [-0.1, 1.1, 1 / 0, 0 / 0] $ \bad -> forM_ [M.insert "urgent" (PredicateAnswer bad) answers, M.insert "category" (ChoiceAnswer "billing" (M.fromList [("billing", 0.2), ("technical", 0.7)]) (Just bad)) answers] $ \as ->
case validateAnswers js as of Left e -> e ^. #category @?= DecodeFailure; Right _ -> assertFailure "expected numeric failure"
case validateAnswers js (M.insert "urgent" (RefusedAnswer (Just "policy")) answers) of
Left e -> do e ^. #category @?= ContentFiltered; e ^. #refusalCategory @?= Just "policy"
Right _ -> assertFailure "refusal required"
]
where
fromString = Data.Text.pack
showKey = Data.Aeson.Key.toString