Docs / Components / Runnables & AST Composition

Runnables & AST Composition

First-class AST composition via pipe (|>>), parallel (&>&), and fallback (>>>#) operators.

Langchain.Core.Runnable

First-class AST composition via pipe (|>>), parallel (&>&), and fallback (>>>#) operators.

Key Concepts

  • First-Class AST: Pipelines construct an inspectable GADT rather than opaque closures, enabling graph visualization and tracing.
  • Type-Safe Operators: |>> for sequential composition, &>& for parallel fan-out, and >>># for fallback chains.
  • Uniform Invocation: Every componentβ€”models, prompts, tools, chains, retrieversβ€”implements the Runnable typeclass.

Working Code Example

Compare local execution via Ollama and cloud API execution via OpenAI / OpenRouter. Use the toggle tabs or the global provider switcher in the header to switch:

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}

{- |
  Pure Runnable AST Pipeline Example (LCEL in idiomatic Haskell).

  Demonstrates how every LangChain component implements 'Runnable' and can be
  algebraically composed into a tree/graph shape using pure functional operators:

    - '(|>>)': Sequential pipe (Arrow composition)
    - '(&>&)': Concurrent parallel fan-out (runs branches concurrently via async)
    - 'runPassthrough': Identity passthrough (like LangChain's RunnablePassthrough)
    - 'runPure': Pure function lifting (zero side-effects)
    - 'runBranch': Predicate-based conditional routing AST node
    - 'runFallback': Failure recovery fallback AST node
    - 'runPrim': Lifts any component with a 'Runnable' instance into the tree

  Pipeline Architecture:

                               β”Œβ”€β”€β–Ί runPrim bm25Index |>> runPure formatDocs ────────┐
                               β”‚                                                      β”‚
    Question: Text ────────────┼──► runPure parseMathExpr |>> runPrim calculatorTool  ┼──► Par (&>&)
                               β”‚                                                      β”‚
                               └──► runPassthrough (preserves question) β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                                                      β”‚
                                                                                      β–Ό
                                                                       runPure packPromptVars
                                                                                      β”‚
                                                                                      β–Ό
                                                                       runPrim promptTemplate
                                                                                      β”‚
                                                                                      β–Ό
                                                                       runChat primaryModel
                                                                         `runFallback`
                                                                       runChat backupModel
                                                                                      β”‚
                                                                                      β–Ό
                                                                       runBranch hasExplanation
                                                                         formatFinalOutput
                                                                         refinePipeline
-}
module Ollama.Runnable (runApp) where

import Control.Monad.Except (ExceptT, runExceptT)
import Data.Aeson (Value, object, (.=))
import qualified Data.Map.Strict as Map
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import qualified Data.Text.Lazy as TL

import Langchain.Core.Runnable (runIdent)
import Langchain.DocumentLoader.Core (Document (..))
import Langchain.Prelude
import Langchain.PromptTemplate.Prompt
  ( PromptTemplate (..)
  , TemplateFormat (..)
  , fromTemplateWithFormat
  )
import Langchain.Provider.Ollama (defaultConfig, newOllama)
import Langchain.Retriever.BM25 (newBM25Index)
import Langchain.Tool.Calculator (calculatorTool)

-- ---------------------------------------------------------------------------
-- Domain Knowledge Base (Mock Documents for Retrieval)
-- ---------------------------------------------------------------------------

knowledgeDocs :: [Document]
knowledgeDocs =
  [ Document
      "Haskell concurrency is built on lightweight threads (threads managed by the GHC runtime). \
      \They cost only a few hundred bytes each, allowing millions of concurrent threads. \
      \Channels (TChan) and MVar provide synchronized communication with zero data races."
      Map.empty
  , Document
      "Backpressure in stream processing ensures producers do not overwhelm consumers. \
      \In Haskell, bounded channels (TBQueue in STM) or Conduit/Pipes provide automatic backpressure. \
      \When a TBQueue is full, the producer thread automatically blocks inside STM."
      Map.empty
  , Document
      "Queue throughput sizing: To calculate optimal throughput Q = R * T, where R is arrival rate \
      \and T is average processing latency. For example: 5000 requests/sec * 0.040 sec = 200 items in flight."
      Map.empty
  ]

runApp :: IO ()
runApp = do
  T.putStrLn "=== Pure RunnableTree Pipeline (LCEL in Haskell) ==="

  -- 1. Initialize LLM Models
  primaryModel <- newOllama "gemma3" defaultConfig
  backupModel <- newOllama "gemma3" defaultConfig

  -- 2. Component 1: BM25 Knowledge Retriever (implements Runnable)
  let bm25 = newBM25Index knowledgeDocs

  -- 3. Component 2: Calculator Tool (implements Runnable Value -> Text)
  let calcTool = calculatorTool :: Tool (ExceptT LangchainError IO)

  -- 4. Component 3: PromptTemplate (implements Runnable Map Text Text -> Text)
  let ragTemplate =
        fromTemplateWithFormat
          ( T.unlines
              [ "You are an expert distributed systems architect."
              , "Use the retrieved documentation and computed metric to solve the engineering query."
              , ""
              , "--- System Documentation ---"
              , "{context}"
              , ""
              , "--- Calculated Capacity Metric ---"
              , "Required In-Flight Buffer: {capacity} items"
              , ""
              , "--- Engineering Requirement ---"
              , "{question}"
              , ""
              , "Provide a concise design recommendation with a code sketch:"
              ]
          )
          FString
          Map.empty

  -- -------------------------------------------------------------------------
  -- Functional Adapters & Pure Transformations
  -- -------------------------------------------------------------------------

  -- Formats retrieved documents into a single text block
  let formatDocs :: [Document] -> Text
      formatDocs docs =
        T.intercalate "\n---\n" (map (TL.toStrict . pageContent) docs)

  -- Extracts an arithmetic expression from question to compute buffer capacity
  let extractMathExpr :: Text -> Value
      extractMathExpr _ = object ["expression" .= ("5000 * 0.040" :: Text)]

  -- Combines outputs from parallel branches into prompt template variables
  let packVars :: ((Text, Text), Text) -> Map.Map Text Text
      packVars ((ctx, cap), q) =
        Map.fromList
          [ ("context", ctx)
          , ("capacity", T.strip cap)
          , ("question", q)
          ]

  -- Formats the final answer
  let formatFinal :: Text -> Text
      formatFinal ans =
        T.unlines
          [ "\n======================================================="
          , "  Synthesized Design Recommendation via RunnableTree"
          , "======================================================="
          , T.strip ans
          ]

  -- -------------------------------------------------------------------------
  -- Assemble the Composed Tree AST (PURE - No execution happens here!)
  -- -------------------------------------------------------------------------
  T.putStrLn "Constructing pure RunnableTree AST..."

  -- Sub-tree A: Parallel Knowledge Retrieval + Tool Computation
  -- Input: Text -> Output: (Text, Text)
  let retrievalAndToolBranch =
        (runPrim bm25 |>> runPure formatDocs)
          &>& (runPure extractMathExpr |>> runPrim calcTool)

  -- Sub-tree B: Fan-out combining Sub-tree A with Passthrough User Question
  -- Input: Text -> Output: ((Context, Capacity), Question)
  let fanOutBranch =
        retrievalAndToolBranch &>& runIdent

  -- Sub-tree C: Self-healing LLM Invocation with Fallback
  -- If primaryModel fails (timeout, network, OOM), backupModel seamlessly catches it
  let robustModelStep =
        runChat primaryModel `runFallback` runChat backupModel

  -- Sub-tree D: Conditional Refinement Node
  -- If response is too brief (< 80 chars), route through an elaboration node
  let isBriefAnswer ans = pure (T.length (T.strip ans) < 80)
      elaborationStep =
        runPure ("Elaborate in detail on: " <>)
          |>> runChat backupModel

  let conditionalOutputStep =
        runBranch isBriefAnswer elaborationStep (runPure id)

  -- Complete Pipeline Composition
  let fullPipeline =
        fanOutBranch
          |>> runPure packVars
          |>> runPrim ragTemplate
          |>> robustModelStep
          |>> conditionalOutputStep
          |>> runPure formatFinal

  -- -------------------------------------------------------------------------
  -- Execute the Pipeline via 'interpret'
  -- -------------------------------------------------------------------------
  let userQuery =
        "How should I size and implement a bounded channel queue for 5000 req/s with 40ms latency?"

  T.putStrLn $ "\n[Input Query]: " <> userQuery
  T.putStrLn "Executing pure pipeline through 'interpret'..."

  res <- runExceptT $ interpret fullPipeline userQuery
  case res of
    Left err -> T.putStrLn $ "Pipeline Error: " <> errorMessage err
    Right output -> T.putStrLn output
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}

{- |
  Pure Runnable AST Pipeline Example (LCEL in idiomatic Haskell) with OpenAI / OpenRouter.

  Demonstrates how every LangChain component implements 'Runnable' and can be
  algebraically composed into a tree/graph shape using pure functional operators:

    - '(|>>)': Sequential pipe (Arrow composition)
    - '(&>&)': Concurrent parallel fan-out (runs branches concurrently via async)
    - 'runIdent': Identity passthrough (like LangChain's RunnablePassthrough)
    - 'runPure': Pure function lifting (zero side-effects)
    - 'runBranch': Predicate-based conditional routing AST node
    - 'runFallback': Failure recovery fallback AST node
    - 'runPrim': Lifts any component with a 'Runnable' instance into the tree

  Pipeline Architecture:

                               β”Œβ”€β”€β–Ί runPrim bm25Index |>> runPure formatDocs ────────┐
                               β”‚                                                      β”‚
    Question: Text ────────────┼──► runPure parseMathExpr |>> runPrim calculatorTool  ┼──► Par (&>&)
                               β”‚                                                      β”‚
                               └──► runIdent (preserves question) β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                                                      β”‚
                                                                                      β–Ό
                                                                       runPure packPromptVars
                                                                                      β”‚
                                                                                      β–Ό
                                                                       runPrim promptTemplate
                                                                                      β”‚
                                                                                      β–Ό
                                                                       runChat primaryModel
                                                                         `runFallback`
                                                                       runChat backupModel
                                                                                      β”‚
                                                                                      β–Ό
                                                                       runBranch isBriefAnswer
                                                                         elaborationStep
                                                                         (runPure id)
-}
module OpenAI.Runnable (runApp) where

import Control.Monad.Except (ExceptT, runExceptT)
import Data.Aeson (Value, object, (.=))
import qualified Data.Map.Strict as Map
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import qualified Data.Text.Lazy as TL

import Langchain.Prelude
import Langchain.Tool.Calculator (calculatorTool)
import OpenAI.Common (defaultModelName, getOpenRouterModel)

-- ---------------------------------------------------------------------------
-- Domain Knowledge Base (Mock Documents for Retrieval)
-- ---------------------------------------------------------------------------

knowledgeDocs :: [Document]
knowledgeDocs =
  [ Document
      "Haskell concurrency is built on lightweight threads (threads managed by the GHC runtime). \
      \They cost only a few hundred bytes each, allowing millions of concurrent threads. \
      \Channels (TChan) and MVar provide synchronized communication with zero data races."
      Map.empty
  , Document
      "Backpressure in stream processing ensures producers do not overwhelm consumers. \
      \In Haskell, bounded channels (TBQueue in STM) or Conduit/Pipes provide automatic backpressure. \
      \When a TBQueue is full, the producer thread automatically blocks inside STM."
      Map.empty
  , Document
      "Queue throughput sizing: To calculate optimal throughput Q = R * T, where R is arrival rate \
      \and T is average processing latency. For example: 5000 requests/sec * 0.040 sec = 200 items in flight."
      Map.empty
  ]

runApp :: IO ()
runApp = do
  T.putStrLn "=== Pure RunnableTree Pipeline (LCEL in Haskell) with OpenAI / OpenRouter ==="

  -- 1. Initialize LLM Models
  primaryModel <- getOpenRouterModel defaultModelName
  backupModel <- getOpenRouterModel defaultModelName

  -- 2. Component 1: BM25 Knowledge Retriever (implements Runnable)
  let bm25 = newBM25Index knowledgeDocs

  -- 3. Component 2: Calculator Tool (implements Runnable Value -> Text)
  let calcTool = calculatorTool :: Tool (ExceptT LangchainError IO)

  -- 4. Component 3: PromptTemplate (implements Runnable Map Text Text -> Text)
  let ragTemplate =
        fromTemplateWithFormat
          ( T.unlines
              [ "You are an expert distributed systems architect."
              , "Use the retrieved documentation and computed metric to solve the engineering query."
              , ""
              , "--- System Documentation ---"
              , "{context}"
              , ""
              , "--- Calculated Capacity Metric ---"
              , "Required In-Flight Buffer: {capacity} items"
              , ""
              , "--- Engineering Requirement ---"
              , "{question}"
              , ""
              , "Provide a concise design recommendation with a code sketch:"
              ]
          )
          FString
          Map.empty

  -- -------------------------------------------------------------------------
  -- Functional Adapters & Pure Transformations
  -- -------------------------------------------------------------------------

  -- Formats retrieved documents into a single text block
  let formatDocs :: [Document] -> Text
      formatDocs docs =
        T.intercalate "\n---\n" (map (TL.toStrict . pageContent) docs)

  -- Extracts an arithmetic expression from question to compute buffer capacity
  let extractMathExpr :: Text -> Value
      extractMathExpr _ = object ["expression" .= ("5000 * 0.040" :: Text)]

  -- Combines outputs from parallel branches into prompt template variables
  let packVars :: ((Text, Text), Text) -> Map.Map Text Text
      packVars ((ctx, cap), q) =
        Map.fromList
          [ ("context", ctx)
          , ("capacity", T.strip cap)
          , ("question", q)
          ]

  -- Formats the final answer
  let formatFinal :: Text -> Text
      formatFinal ans =
        T.unlines
          [ "\n======================================================="
          , "  Synthesized Design Recommendation via RunnableTree"
          , "======================================================="
          , T.strip ans
          ]

  -- -------------------------------------------------------------------------
  -- Assemble the Composed Tree AST (PURE - No execution happens here!)
  -- -------------------------------------------------------------------------
  T.putStrLn "Constructing pure RunnableTree AST..."

  -- Sub-tree A: Parallel Knowledge Retrieval + Tool Computation
  -- Input: Text -> Output: (Text, Text)
  let retrievalAndToolBranch =
        (runPrim bm25 |>> runPure formatDocs)
          &>& (runPure extractMathExpr |>> runPrim calcTool)

  -- Sub-tree B: Fan-out combining Sub-tree A with Passthrough User Question
  -- Input: Text -> Output: ((Context, Capacity), Question)
  let fanOutBranch =
        retrievalAndToolBranch &>& runIdent

  -- Sub-tree C: Self-healing LLM Invocation with Fallback
  -- If primaryModel fails (timeout, network, OOM), backupModel seamlessly catches it
  let robustModelStep =
        runChat primaryModel `runFallback` runChat backupModel

  -- Sub-tree D: Conditional Refinement Node
  -- If response is too brief (< 80 chars), route through an elaboration node
  let isBriefAnswer ans = pure (T.length (T.strip ans) < 80)
      elaborationStep =
        runPure ("Elaborate in detail on: " <>)
          |>> runChat backupModel

  let conditionalOutputStep =
        runBranch isBriefAnswer elaborationStep (runPure id)

  -- Complete Pipeline Composition
  let fullPipeline =
        fanOutBranch
          |>> runPure packVars
          |>> runPrim ragTemplate
          |>> robustModelStep
          |>> conditionalOutputStep
          |>> runPure formatFinal

  -- -------------------------------------------------------------------------
  -- Execute the Pipeline via 'interpret'
  -- -------------------------------------------------------------------------
  let userQuery =
        "How should I size and implement a bounded channel queue for 5000 req/s with 40ms latency?"

  T.putStrLn $ "\n[Input Query]: " <> userQuery
  T.putStrLn "Executing pure pipeline through 'interpret'..."

  res <- runExceptT $ interpret fullPipeline userQuery
  case res of
    Left err -> T.putStrLn $ "Pipeline Error: " <> errorMessage err
    Right output -> T.putStrLn output

Core Types & Functions

Runnable r => r a b -> a -> ExceptT LangchainError IO b
Runnable r => r a b -> r b c -> RunnableTree a c
Runnable r => r a b -> r a c -> RunnableTree a (b, c)
Runnable r => r a b -> r a b -> RunnableTree a b

Running This Example

Local Ollama

Ensure your Ollama daemon is running locally with the target model:

ollama run gemma3 # or your desired model
stack run runnableollama

OpenAI / OpenRouter

Ensure your OPENROUTER_API_KEY or OPENAI_API_KEY is exported:

export OPENROUTER_API_KEY="your-api-key"
stack run runnableopenai
ESC