packages feed

llm-simple-0.1.0.1: test/LLM/GenericConversationTest.hs

module LLM.GenericConversationTest (createSpec, GenericConversationTextOps (..)) where

import Data.Map qualified as Map
import Data.Text qualified as T
import Heptapod (generate)
import LLM (LLMHooks (..))
import LLM.Agent.Events (noEventObserver)
import LLM.Agent.ToolUtils (toTool)
import LLM.Agent.Types (Agent (..), RuntimeArgs (..))
import LLM.Core.LLMProvider (LLMProvider, toGateway)
import LLM.Core.Types (LLMGateway, ThinkingMode (..))
import LLM.Core.Usage (PricingInfo (..))
import LLM.Generate.Logger (noHooks)
import LLM.Generate.ModelConfig (ModelConfig (..), ModelWithFallbacks (ModelWithFallbacks))
import LLM.TestKit
  ( loadRecordedConversation,
    mockProvider,
    recordedConversationSystemPrompt,
    streamChatLoop,
  )
import LLM.WeatherTool (weatherToolTyped)
import Test.Hspec (Spec, describe, it, shouldBe)

data GenericConversationTextOps = GenericConversationTextOps
  { specTitle :: String,
    specProvider :: LLMProvider,
    modelName :: String,
    specThinking :: Maybe ThinkingMode,
    filePathGenerated :: String,
    filePathStreamed :: String
  }

mkModelConfig :: GenericConversationTextOps -> LLMGateway -> ModelConfig
mkModelConfig opts provider =
  ModelConfig
    { mcGateway = provider,
      mcModel = T.pack opts.modelName,
      mcPricing = PricingInfo {pricePerMillionInput = 0.0, pricePerMillionOutput = 0.0},
      mcMaxTokens = 1024,
      mcTemperature = Nothing,
      mcThinking = opts.specThinking,
      mcRequestTimeout = Nothing,
      mcThrottleDelay = Nothing,
      mcRetryCount = 3,
      mcJitterBackoff = 1_000
    }

createSpec :: GenericConversationTextOps -> Spec
createSpec opts = describe opts.specTitle $ do
  let toolMap = Map.fromList [("get_weather", toTool weatherToolTyped)]

  it "generateText" $ do
    (m, p) <- loadRecordedConversation opts.filePathGenerated
    uuid1 <- generate
    let provider = toGateway $ mockProvider m opts.specProvider
        modelConf = mkModelConfig opts provider
        systemPrompt = recordedConversationSystemPrompt
        agent :: Agent =
          Agent
            { agName = "test",
              agSystemPrompt = Just systemPrompt,
              agTools =
                [ "get_weather"
                ],
              agMaxToolRounds = 3,
              agContextWindow = Nothing
            }
        models = ModelWithFallbacks modelConf []
        rt =
          RuntimeArgs
            { rtGenerationId = uuid1,
              rtAbortSignal = Nothing,
              rtLLMHooks = LLMHooks {onLLMRequest = \_ _ -> pure (), onLLMResponse = \_ _ -> pure (), onLLMResponseError = \_ _ -> pure ()},
              rtHooks = noHooks,
              rtOnEvent = noEventObserver,
              rtReadonly = False
            }
    turns <- streamChatLoop False (agent, models, toolMap, rt) p
    length turns `shouldBe` 8

  it "streamText" $ do
    (m, p) <- loadRecordedConversation opts.filePathStreamed
    uuid1 <- generate
    let provider = toGateway $ mockProvider m opts.specProvider
        modelConf = mkModelConfig opts provider
        systemPrompt = recordedConversationSystemPrompt
        models = ModelWithFallbacks modelConf []
        agent =
          Agent
            { agName = "test",
              agSystemPrompt = Just systemPrompt,
              agTools = ["get_weather"],
              agMaxToolRounds = 3,
              agContextWindow = Nothing
            }
        runtime =
          RuntimeArgs
            { rtGenerationId = uuid1,
              rtAbortSignal = Nothing,
              rtLLMHooks =
                LLMHooks
                  { onLLMRequest = \_ _ -> pure (),
                    onLLMResponse = \_ _ -> pure (),
                    onLLMResponseError = \_ _ -> pure ()
                  },
              rtHooks = noHooks,
              rtOnEvent = noEventObserver,
              rtReadonly = False
            }
    turns <- streamChatLoop True (agent, models, toolMap, runtime) p
    length turns `shouldBe` 8