packages feed

shikumi-0.1.0.0: test/AdapterSpec.hs

-- | M5: 'Shikumi.Adapter'. Capability selection is a pure function of the model;
-- the fallback adapter renders @[[ ## field ## ]]@ sections and re-parses them
-- into a typed output, with a missing marker producing 'MissingField'. The native
-- path's schema attachment is pending EP-2.
module AdapterSpec (tests) where

import Baikai
  ( Api (..),
    AssistantContent (..),
    Model,
    Response,
    _Model,
    _Response,
    _TextContent,
  )
import Control.Lens ((&), (.~), (^.))
import Data.Generics.Labels ()
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Vector qualified as V
import Fixtures (Article, Author (..), Sentiment (..), Summary (..), sampleArticle, sampleSummary)
import Shikumi.Adapter
  ( Adapter (..),
    ModelCapability (..),
    capabilityFor,
    fallbackAdapter,
    nativeAdapter,
  )
import Shikumi.Error (ShikumiError (..))
import Shikumi.Schema.Types (field)
import Shikumi.Signature (Demo (..), Signature, mkSignature, setDemos)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (testCase, (@?=))

anthropicModel :: Model
anthropicModel = _Model & #provider .~ "anthropic" & #api .~ AnthropicMessages

ollamaModel :: Model
ollamaModel = _Model & #provider .~ "ollama" & #api .~ Custom "ollama"

sig :: Signature Article Summary
sig = setDemos [Demo sampleArticle sampleSummary] (mkSignature "Summarize the article")

-- | Build a response whose single assistant text block carries the given body.
mkResponse :: Text -> Response
mkResponse t =
  _Response & #message . #content .~ V.singleton (AssistantText (_TextContent & #text .~ t))

markerBody :: Text
markerBody =
  T.intercalate
    "\n"
    [ "[[ ## headline ## ]]",
      "Shikumi types LM programs",
      "[[ ## bullets ## ]]",
      "[\"records in\", \"records out\", \"errors are typed\"]",
      "[[ ## author ## ]]",
      "{\"name\": \"Ada\"}",
      "[[ ## sentiment ## ]]",
      "Positive",
      "[[ ## note ## ]]",
      "null",
      "[[ ## completed ## ]]"
    ]

markerBodyNoBullets :: Text
markerBodyNoBullets =
  T.intercalate
    "\n"
    [ "[[ ## headline ## ]]",
      "Shikumi types LM programs",
      "[[ ## author ## ]]",
      "{\"name\": \"Ada\"}",
      "[[ ## sentiment ## ]]",
      "Positive",
      "[[ ## note ## ]]",
      "null",
      "[[ ## completed ## ]]"
    ]

expectedSummary :: Summary
expectedSummary =
  Summary
    { headline = field "Shikumi types LM programs",
      bullets = field ["records in", "records out", "errors are typed"],
      author = Author {name = field "Ada"},
      sentiment = Positive,
      note = Nothing
    }

sysOf :: Adapter Article Summary -> Text
sysOf adapter = fromMaybe "" (fst (render adapter sig sampleArticle) ^. #systemPrompt)

tests :: TestTree
tests =
  testGroup
    "AdapterSpec"
    [ testCase "capabilityFor: Anthropic messages -> NativeSchema" $
        capabilityFor anthropicModel @?= NativeSchema,
      testCase "capabilityFor: Custom (ollama) host -> PromptFallback" $
        capabilityFor ollamaModel @?= PromptFallback,
      testCase "fallback render: system prompt has the instruction and field markers" $ do
        T.isInfixOf "Summarize the article" (sysOf fallbackAdapter) @?= True
        T.isInfixOf "[[ ## headline ## ]]" (sysOf fallbackAdapter) @?= True,
      testCase "fallback render: a demo becomes a user/assistant message pair" $
        -- one demo (2 messages) + the actual input (1 message)
        V.length (fst (render fallbackAdapter sig sampleArticle) ^. #messages) @?= 3,
      testCase "fallback parse: marker body decodes to the expected Summary" $
        parse fallbackAdapter sig (mkResponse markerBody) @?= Right expectedSummary,
      testCase "fallback parse: a missing marker -> MissingField (located)" $
        parse fallbackAdapter sig (mkResponse markerBodyNoBullets) @?= Left (MissingField "bullets"),
      testCase "native render: system prompt carries the instruction (schema attach pending EP-2)" $
        T.isInfixOf "Summarize the article" (sysOf nativeAdapter) @?= True
    ]