packages feed

langchain-hs-core-0.0.6.0: test/Test/Langchain/Retriever.hs

{-# LANGUAGE OverloadedStrings #-}

module Test.Langchain.Retriever (tests) where

import Control.Monad.Except (runExceptT)
import qualified Data.Map.Strict as Map
import qualified Data.Text.Lazy as TL
import Test.Tasty
import Test.Tasty.HUnit

import Langchain.DocumentLoader.Core (Document (..))
import Langchain.Retriever

data DummyRetriever = DummyRetriever
  deriving (Show, Eq)

instance Retriever DummyRetriever where
  getRelevantDocuments _ query =
    pure [Document (TL.fromStrict $ query <> " result") Map.empty]

tests :: TestTree
tests =
  testGroup
    "Langchain.Retriever"
    [ testGroup
        "Core"
        [ testCase "DummyRetriever retrieves documents" $ do
            res <- runExceptT $ getRelevantDocuments DummyRetriever "test"
            case res of
              Left err -> assertFailure ("Error: " ++ show err)
              Right docs -> map pageContent docs @?= ["test result"]
        ]
    , testGroup
        "BM25"
        [ testCase "BM25 finds exact matching document" $ do
            let doc1 =
                  Document
                    { pageContent = "Haskell is a functional programming language with strong static types."
                    , metadata = Map.empty
                    }
                doc2 =
                  Document
                    { pageContent = "Python is a dynamic language used for machine learning and web scripts."
                    , metadata = Map.empty
                    }
                doc3 =
                  Document
                    { pageContent = "Rust guarantees memory safety without garbage collection."
                    , metadata = Map.empty
                    }
                index = newBM25Index [doc1, doc2, doc3]
                results = bm25Search index "functional static types" 2
            case results of
              (topResult : _) -> topResult @?= doc1
              [] -> assertFailure "Expected non-empty search results"
        ]
    , testGroup
        "Hybrid"
        [ testCase "reciprocalRankFusion prioritizes documents appearing in both lists" $ do
            let docA = Document {pageContent = "Document A about quantum algorithms", metadata = Map.empty}
                docB = Document {pageContent = "Document B about classical physics", metadata = Map.empty}
                docC = Document {pageContent = "Document C about neural networks", metadata = Map.empty}
                denseList = [docA, docB]
                sparseList = [docC, docA]
                fused = reciprocalRankFusion 60.0 [(denseList, 1.0), (sparseList, 1.0)]
            length fused @?= 3
            case fused of
              ((topDoc, _) : _) -> topDoc @?= docA
              [] -> assertFailure "Expected non-empty fused results"
        , testCase "searchHybrid executes dense and sparse searches" $ do
            let doc1 = Document {pageContent = "Haskell state monad and effects", metadata = Map.empty}
                doc2 = Document {pageContent = "Rust borrow checker and lifetimes", metadata = Map.empty}
                bm25 = newBM25Index [doc1, doc2]
                mockVecSearch _ _ = pure [doc2, doc1]
                hybrid = newHybridRetriever bm25 mockVecSearch
            results <- searchHybrid hybrid "Haskell" 2
            length results @?= 2
            case results of
              (topDoc : _) -> topDoc @?= doc1
              [] -> assertFailure "Expected non-empty results"
        ]
    ]