packages feed

langchain-hs-0.0.2.0: test/Test/Langchain/Tool/Core.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}

module Test.Langchain.Tool.Core (tests) where

import Data.Aeson (decode)
import qualified Data.Map as M
import Data.Text (Text)
import qualified Data.Text as T
import Test.Tasty
import Test.Tasty.HUnit
import Data.Either (isLeft)

import Langchain.Tool.Core
import Langchain.Tool.WebScraper
import Langchain.Tool.WikipediaTool
import Langchain.Tool.Calculator

data MockTool = MockTool Text
  deriving (Show, Eq)

instance Tool MockTool where
  type Input MockTool = Text
  type Output MockTool = Text
  toolName (MockTool name) = name
  toolDescription _ = "A mock tool for testing"
  runTool _ input = return $ "Processed: " <> input

tests :: TestTree
tests =
  testGroup
    "Tool Tests"
    [ testCase "MockTool implements Tool interface correctly" testMockTool
    , testCase "WikipediaTool default values" testWikipediaToolDefaults
    , testCase "WikipediaTool tool name and description" testWikipediaToolMetadata
    , testCase "WikipediaTool search functionality" testWikipediaToolSearch
    , testCase "SearchResponse parsing" testSearchResponseParsing
    , testCase "PageResponse parsing" testPageResponseParsing
    , testCase "WebScraper Tool" testWebScraperTool
    , testCalculatorTool
    ]

testCalculatorTool :: TestTree
testCalculatorTool = testGroup "Langchain.Tool.Calculator"
  [ parseExpressionTests
  , evaluateExpressionTests
  , calculatorToolTests
  ]

-- | Test cases for parseExpression
parseExpressionTests :: TestTree
parseExpressionTests = testGroup "parseExpression"
  [ testCase "Parses integer" $
      parseExpression "123" @?= Right (Number_ 123.0)

  , testCase "Parses decimal" $
      parseExpression "45.67" @?= Right (Number_ 45.67)

  , testCase "Handles addition" $
      parseExpression "2+3" @?= Right (Add (Number_ 2) (Number_ 3))

  , testCase "Handles subtraction" $
      parseExpression "5 - 1" @?= Right (Sub (Number_ 5) (Number_ 1))

  , testCase "Handles multiplication" $
      parseExpression "4*2" @?= Right (Mul (Number_ 4) (Number_ 2))

  , testCase "Handles division" $
      parseExpression "8 / 2" @?= Right (Div (Number_ 8) (Number_ 2))

  , testCase "Handles exponentiation" $
      parseExpression "2^3" @?= Right (Pow (Number_ 2) (Number_ 3))

  , testCase "Respects operator precedence" $
      parseExpression "2 + 3 * 4" @?= Right (Add (Number_ 2) (Mul (Number_ 3) (Number_ 4)))

  , testCase "Respects parentheses" $
      parseExpression "(2 + 3) * 4" @?= Right (Mul (Add (Number_ 2) (Number_ 3)) (Number_ 4))

  , testCase "Fails on invalid input" $
      isLeft (parseExpression "hello") @? "Expected parse failure for 'hello'"
  ]

-- | Test cases for evaluateExpression
evaluateExpressionTests :: TestTree
evaluateExpressionTests = testGroup "evaluateExpression"
  [ testCase "Evaluates Num" $
      evaluateExpression (Number_ 5) @?= 5.0

  , testCase "Evaluates Add" $
      evaluateExpression (Add (Number_ 2) (Number_ 3)) @?= 5.0

  , testCase "Evaluates Mul" $
      evaluateExpression (Mul (Number_ 3) (Number_ 4)) @?= 12.0

  , testCase "Evaluates Pow" $
      evaluateExpression (Pow (Number_ 2) (Number_ 3)) @?= 8.0
  ]

-- | Test cases for CalculatorTool
calculatorToolTests :: TestTree
calculatorToolTests = testGroup "CalculatorTool"
  [ testCase "Computes 2 + 3 * 4" $ do
      result <- runTool CalculatorTool "2 + 3 * 4"
      result @?= Right 14.0

  , testCase "Computes (2 + 3) * 4" $ do
      result <- runTool CalculatorTool "(2 + 3) * 4"
      result @?= Right 20.0

  , testCase "Computes 2 ^ 3" $ do
      result <- runTool CalculatorTool "2 ^ 3"
      result @?= Right 8.0

  , testCase "Fails on invalid expression" $ do
      let badExpr = "2 +"
      errOrRes <- runTool CalculatorTool badExpr
      case errOrRes of
        Left _ -> return ()
        Right _ -> assertFailure "Expected error when parsing invalid expression"
  ]

testWebScraperTool :: Assertion
testWebScraperTool = do
  eRes <- runTool WebScraper "https://hackage.haskell.org/package/scalpel-0.6.2.2"
  assertBool "Scraper should contain stuff like title" $ do
    case eRes of 
      Left _ -> False
      Right r -> do
        T.isInfixOf "Scalpel is a web scraping library inspired by libraries like" r

testMockTool :: Assertion
testMockTool = do
  let mockTool = MockTool "TestTool"

  assertEqual "toolName should return the name" "TestTool" (toolName mockTool)

  assertEqual
    "toolDescription should return description"
    "A mock tool for testing"
    (toolDescription mockTool)

  result <- runTool mockTool "test input"
  assertEqual
    "runTool should process input correctly"
    "Processed: test input"
    result

testWikipediaToolDefaults :: Assertion
testWikipediaToolDefaults = do
  let tool = defaultWikipediaTool

  assertEqual
    "Default topK should be 2"
    defaultTopK
    (topK tool)

  assertEqual
    "Default docMaxChars should be 2000"
    defaultDocMaxChars
    (docMaxChars tool)

  assertEqual
    "Default language code should be 'en'"
    defaultLanguageCode
    (languageCode tool)

testWikipediaToolMetadata :: Assertion
testWikipediaToolMetadata = do
  let tool = defaultWikipediaTool

  assertEqual
    "WikipediaTool name should be 'Wikipedia'"
    "Wikipedia"
    (toolName tool)

  assertBool
    "WikipediaTool description should mention Wikipedia"
    (T.isInfixOf "Wikipedia" (toolDescription tool))

-- TODO: Actually use the WikipediaTool here
testWikipediaToolSearch :: Assertion
testWikipediaToolSearch = do
  let customTool =
        WikipediaTool
          { topK = 1
          , docMaxChars = 10
          , languageCode = "en"
          }

  assertEqual "Custom tool should have topK = 1" 1 (topK customTool)
  assertEqual "Custom tool should truncate to 10 chars" 10 (docMaxChars customTool)

-- Test JSON parsing for SearchResponse
testSearchResponseParsing :: Assertion
testSearchResponseParsing = do
  let jsonStr =
        "{\"query\": {\"search\": [{\"ns\": 0, \"title\": \"Haskell\", \"pageid\": 12345, \"size\": 1000, \"wordcount\": 200, \"snippet\": \"<span>Haskell</span> is a functional language\", \"timestamp\": \"2023-01-01\"}]}}"
      parsed = decode jsonStr :: Maybe SearchResponse

  case parsed of
    Nothing -> assertFailure "Failed to parse SearchResponse JSON"
    Just SearchResponse {..} -> do
      let searchResults = search query
      assertBool "Should have at least one search result" (not $ null searchResults)
      case searchResults of
        (firstResult : _) -> do
          assertEqual "Page ID should match" 12345 (pageid firstResult)
          assertEqual "Title should match" "Haskell" (title_ firstResult)
        _ -> pure ()

testPageResponseParsing :: Assertion
testPageResponseParsing = do
  let jsonStr =
        "{\"query\": {\"pages\": {\"12345\": {\"title\": \"Haskell\", \"extract\": \"Haskell is a functional programming language.\"}}}}"
      parsed = decode jsonStr :: Maybe PageResponse

  case parsed of
    Nothing -> assertFailure "Failed to parse PageResponse JSON"
    Just (PageResponse (Pages pagesMap)) -> do
      let maybePage = M.lookup "12345" pagesMap
      case maybePage of
        Nothing -> assertFailure "Expected page with ID 12345 not found"
        Just page -> do
          assertEqual "Page title should match" "Haskell" (title page)
          assertEqual
            "Page extract should match"
            "Haskell is a functional programming language."
            (extract page)