packages feed

langchain-hs-0.0.5.0: test/Test/Langchain/Integration/StateGraphE2ESpec.hs

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}

{- |
Module      : Test.Langchain.Integration.StateGraphE2ESpec
Description : StateGraph multi-node pipeline integration tests (Gemini or Ollama)
Copyright   : (c) 2025-2026 Tushar Adhatrao
License     : MIT
Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>
Stability   : experimental
-}
module Test.Langchain.Integration.StateGraphE2ESpec (tests) where

import Control.Monad.Except (runExceptT)
import Data.Aeson (FromJSON, ToJSON)
import Data.Text (Text)
import qualified Data.Text as T
import GHC.Generics (Generic)
import Test.Tasty
import Test.Tasty.HUnit

import Langchain.Core.Model
import Langchain.Graph.StateGraph
import Test.Langchain.TestHelpers (withAnyModel)

data GraphPipelineTestState = GraphPipelineTestState
  { originalPrompt :: Text
  , draftResponse :: Text
  , reviewNotes :: Text
  }
  deriving (Show, Eq, Generic, ToJSON, FromJSON)

graphStateReducer :: StateReducer GraphPipelineTestState
graphStateReducer old new =
  GraphPipelineTestState
    { originalPrompt = if T.null (originalPrompt new) then originalPrompt old else originalPrompt new
    , draftResponse = if T.null (draftResponse new) then draftResponse old else draftResponse new
    , reviewNotes = if T.null (reviewNotes new) then reviewNotes old else reviewNotes new
    }

assertStateGraph :: ChatModel m => m -> IO ()
assertStateGraph provider = do
  let draftNode s = do
        let prompt = [userMessage $ "Answer concisely in one sentence: " <> originalPrompt s]
        res <- invoke provider prompt Nothing
        pure $ Right (s {draftResponse = extractMessageText res})

      reviewNode s = do
        let prompt = [userMessage $ "Review and confirm this answer: " <> draftResponse s]
        res <- invoke provider prompt Nothing
        pure $ Right (s {reviewNotes = extractMessageText res})

      g =
        addEdge "draft" "review" $
          addEdge "review" endNodeId $
            addNode "draft" draftNode $
              addNode "review" reviewNode $
                emptyStateGraph graphStateReducer

  case compileGraph g of
    Left err -> assertFailure ("Graph compilation failed: " ++ show err)
    Right cg -> do
      let initState = GraphPipelineTestState "What is 2 + 2?" "" ""
      res <- runExceptT $ runGraph cg "draft" initState
      case res of
        Left err -> assertFailure ("StateGraph run failed: " ++ show err)
        Right finalState -> do
          assertBool "Draft response generated" (not (T.null $ draftResponse finalState))
          assertBool "Review notes generated" (not (T.null $ reviewNotes finalState))

tests :: TestTree
tests =
  testGroup
    "Langchain.Integration.StateGraphE2ESpec"
    [ testCase "StateGraph multi-node pipeline (Gemini or Ollama)" $
        withAnyModel assertStateGraph assertStateGraph
    ]