langchain-hs-core-0.0.6.0: src/Langchain/Runnable.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
{- |
Module : Langchain.Runnable
Description : Pure pipeline GADT (RunnableTree) and algebraic composition
Copyright : (c) 2025-2026 Tushar Adhatrao
License : MIT
Maintainer : Tushar Adhatrao <tusharadhatrao@gmail.com>
Stability : experimental
Pure, AST-based pipeline representation ('RunnableTree') where building pipelines performs
NO side effects. Execution is strictly deferred to 'interpret'.
Provides full configuration binding for 'ChatModel' instances.
-}
module Langchain.Runnable
( Runnable (..)
, RunnableTree (..)
, (|>>)
, (&>&)
, interpret
, runLambda
, runPrim
, runPure
, runPassthrough
, runIdent
, runBranch
, runFallback
, runChat
, runChatWithConfig
, runModel
, runModelWithConfig
, runConfiguredModel
, ModelRunnable (..)
, modelRunnable
, modelRunnableWithConfig
, bindConfig
, TextModelRunnable (..)
, textModelRunnable
, textModelRunnableWithConfig
, bindTextConfig
, ConfiguredModelRunnable (..)
) where
import Control.Concurrent.Async (concurrently)
import Control.Monad.Except (ExceptT, MonadError, catchError, runExceptT, throwError)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Aeson (Value)
import Data.Kind (Type)
import Data.Text (Text)
import Langchain.Error (LangchainError)
import Langchain.Model (ChatModel, ModelConfig, extractMessageText, userMessage)
import qualified Langchain.Model as M (invoke)
import Langchain.Model.Types (Message)
import Langchain.Tool (Tool (..))
-- | Fundamental Runnable interface for components wrapped in 'Prim'.
class Runnable r m where
type RunnableInput r :: Type
type RunnableOutput r :: Type
invoke :: r -> RunnableInput r -> m (Either LangchainError (RunnableOutput r))
-- | Any Tool can be executed as a primitive Runnable taking JSON 'Value' to 'Text'.
instance Runnable (Tool m) m where
type RunnableInput (Tool m) = Value
type RunnableOutput (Tool m) = Text
invoke = toolExecute
-- ---------------------------------------------------------------------------
-- Model Runnables with Configuration Support
-- ---------------------------------------------------------------------------
-- | Wrapper to treat any 'ChatModel' as a Runnable over '[Message]' -> 'Message' with optional bound configuration.
data ModelRunnable c = ModelRunnable
{ modelRunnableModel :: !c
, modelRunnableConfig :: !(Maybe (ModelConfig c))
}
deriving instance (Show c, Show (ModelConfig c)) => Show (ModelRunnable c)
deriving instance (Eq c, Eq (ModelConfig c)) => Eq (ModelRunnable c)
-- | Construct a ModelRunnable without pre-set configuration.
modelRunnable :: c -> ModelRunnable c
modelRunnable c = ModelRunnable c Nothing
-- | Construct a ModelRunnable with pre-bound model configuration.
modelRunnableWithConfig :: c -> ModelConfig c -> ModelRunnable c
modelRunnableWithConfig c cfg = ModelRunnable c (Just cfg)
-- | Bind or replace the configuration on a ModelRunnable (equivalent to LangChain's model.bind).
bindConfig :: ModelConfig c -> ModelRunnable c -> ModelRunnable c
bindConfig cfg (ModelRunnable c _) = ModelRunnable c (Just cfg)
instance (ChatModel c, MonadIO m) => Runnable (ModelRunnable c) m where
type RunnableInput (ModelRunnable c) = [Message]
type RunnableOutput (ModelRunnable c) = Message
invoke (ModelRunnable c mCfg) msgs = runExceptT (M.invoke c msgs mCfg)
-- | Wrapper to treat any 'ChatModel' as a simple 'Text' -> 'Text' Runnable with optional bound configuration.
data TextModelRunnable c = TextModelRunnable
{ textModelRunnableModel :: !c
, textModelRunnableConfig :: !(Maybe (ModelConfig c))
}
deriving instance (Show c, Show (ModelConfig c)) => Show (TextModelRunnable c)
deriving instance (Eq c, Eq (ModelConfig c)) => Eq (TextModelRunnable c)
-- | Construct a TextModelRunnable without pre-set configuration.
textModelRunnable :: c -> TextModelRunnable c
textModelRunnable c = TextModelRunnable c Nothing
-- | Construct a TextModelRunnable with pre-bound model configuration.
textModelRunnableWithConfig :: c -> ModelConfig c -> TextModelRunnable c
textModelRunnableWithConfig c cfg = TextModelRunnable c (Just cfg)
-- | Bind or replace the configuration on a TextModelRunnable.
bindTextConfig :: ModelConfig c -> TextModelRunnable c -> TextModelRunnable c
bindTextConfig cfg (TextModelRunnable c _) = TextModelRunnable c (Just cfg)
instance (ChatModel c, MonadIO m) => Runnable (TextModelRunnable c) m where
type RunnableInput (TextModelRunnable c) = Text
type RunnableOutput (TextModelRunnable c) = Text
invoke (TextModelRunnable c mCfg) prompt = do
res <- runExceptT $ M.invoke c [userMessage prompt] mCfg
pure (extractMessageText <$> res)
-- | Dynamic model runnable allowing model configuration to be passed per-request.
newtype ConfiguredModelRunnable c = ConfiguredModelRunnable c
deriving (Eq, Show)
instance (ChatModel c, MonadIO m) => Runnable (ConfiguredModelRunnable c) m where
type RunnableInput (ConfiguredModelRunnable c) = ([Message], Maybe (ModelConfig c))
type RunnableOutput (ConfiguredModelRunnable c) = Message
invoke (ConfiguredModelRunnable c) (msgs, mCfg) = runExceptT (M.invoke c msgs mCfg)
-- ---------------------------------------------------------------------------
-- RunnableTree Pipeline AST
-- ---------------------------------------------------------------------------
{- | Pure GADT representing a composable pipeline AST.
@i@ = input type, @o@ = output type, @m@ = monad context.
-}
data RunnableTree m i o where
-- | Identity: passes input through unchanged
Id :: RunnableTree m a a
-- | Lift a component implementing Runnable into the tree
Prim ::
(Runnable r m, RunnableInput r ~ i, RunnableOutput r ~ o) =>
r ->
RunnableTree m i o
-- | Lift a monadic function into the tree
Lambda :: (i -> m (Either LangchainError o)) -> RunnableTree m i o
-- | Sequential composition AST node
Seq :: RunnableTree m i mid -> RunnableTree m mid o -> RunnableTree m i o
-- | Parallel composition AST node
Par ::
RunnableTree (ExceptT LangchainError IO) i o1 ->
RunnableTree (ExceptT LangchainError IO) i o2 ->
RunnableTree (ExceptT LangchainError IO) i (o1, o2)
-- | Conditional branching AST node
Branch ::
(i -> m Bool) ->
-- | True branch
RunnableTree m i o ->
-- | False branch
RunnableTree m i o ->
RunnableTree m i o
-- | Fallback node: if primary fails, executes fallback
Fallback :: RunnableTree m i o -> RunnableTree m i o -> RunnableTree m i o
-- | Sequential composition operator — PURE AST builder.
(|>>) :: RunnableTree m a b -> RunnableTree m b c -> RunnableTree m a c
(|>>) = Seq
infixl 1 |>>
-- | Parallel composition operator — PURE AST builder.
(&>&) ::
RunnableTree (ExceptT LangchainError IO) a b ->
RunnableTree (ExceptT LangchainError IO) a c ->
RunnableTree (ExceptT LangchainError IO) a (b, c)
(&>&) = Par
infixl 2 &>&
-- | Helper to create a lambda runnable node.
runLambda :: (i -> m (Either LangchainError o)) -> RunnableTree m i o
runLambda = Lambda
-- | Helper to create a primitive runnable node.
runPrim ::
(Runnable r m, RunnableInput r ~ i, RunnableOutput r ~ o) =>
r ->
RunnableTree m i o
runPrim = Prim
-- | Helper to convert Either to MonadError
liftEither :: MonadError LangchainError m => Either LangchainError a -> m a
liftEither (Left err) = throwError err
liftEither (Right x) = pure x
-- | Sole execution engine for 'RunnableTree' AST pipelines.
interpret ::
(MonadIO m, MonadError LangchainError m) =>
RunnableTree m i o ->
i ->
m o
interpret Id input = pure input
interpret (Prim r) input = invoke r input >>= liftEither
interpret (Lambda f) input = f input >>= liftEither
interpret (Seq t1 t2) input = interpret t1 input >>= interpret t2
interpret (Par t1 t2) input = do
(r1, r2) <-
liftIO $
concurrently
(runExceptT $ interpret t1 input)
(runExceptT $ interpret t2 input)
o1 <- liftEither r1
o2 <- liftEither r2
pure (o1, o2)
interpret (Branch cond tTrue tFalse) input = do
b <- cond input
if b then interpret tTrue input else interpret tFalse input
interpret (Fallback t1 t2) input =
catchError (interpret t1 input) (\_ -> interpret t2 input)
-- | Lift a pure function into a 'RunnableTree' node.
runPure :: Monad m => (i -> o) -> RunnableTree m i o
runPure f = runLambda (pure . Right . f)
-- | Identity node in a 'RunnableTree' (passes input through unchanged, like LangChain's @RunnablePassthrough@).
runPassthrough :: RunnableTree m a a
runPassthrough = Id
-- | Alias for 'runPassthrough'.
runIdent :: RunnableTree m a a
runIdent = Id
-- | Construct a conditional branch AST node.
runBranch :: (i -> m Bool) -> RunnableTree m i o -> RunnableTree m i o -> RunnableTree m i o
runBranch = Branch
-- | Construct a self-healing fallback AST node (tries first, catches errors and runs second).
runFallback :: RunnableTree m i o -> RunnableTree m i o -> RunnableTree m i o
runFallback = Fallback
-- | Lift any 'ChatModel' into a simple 'Text' -> 'Text' pipeline step (without configuration).
runChat :: (ChatModel c, MonadIO m) => c -> RunnableTree m Text Text
runChat c = runPrim (TextModelRunnable c Nothing)
-- | Lift any 'ChatModel' into a simple 'Text' -> 'Text' pipeline step with bound model configuration.
runChatWithConfig :: (ChatModel c, MonadIO m) => c -> ModelConfig c -> RunnableTree m Text Text
runChatWithConfig c cfg = runPrim (TextModelRunnable c (Just cfg))
-- | Lift any 'ChatModel' into a structured '[Message]' -> 'Message' pipeline step (without configuration).
runModel :: (ChatModel c, MonadIO m) => c -> RunnableTree m [Message] Message
runModel c = runPrim (ModelRunnable c Nothing)
-- | Lift any 'ChatModel' into a structured '[Message]' -> 'Message' pipeline step with bound model configuration.
runModelWithConfig ::
(ChatModel c, MonadIO m) => c -> ModelConfig c -> RunnableTree m [Message] Message
runModelWithConfig c cfg = runPrim (ModelRunnable c (Just cfg))
-- | Lift any 'ChatModel' into a pipeline step taking dynamic messages and configuration per request.
runConfiguredModel ::
(ChatModel c, MonadIO m) => c -> RunnableTree m ([Message], Maybe (ModelConfig c)) Message
runConfiguredModel c = runPrim (ConfiguredModelRunnable c)