# LangChain Haskell (langchain-hs)
> Type-Safe AI Agents & Multi-Agent Graphs in Haskell
A functional, effect-polymorphic Haskell ecosystem for building production AI applications, featuring pure AST pipelines (`RunnableTree`), cyclic state graphs (`StateGraph`), Model Context Protocol (MCP), and OpenTelemetry observability.
[](https://hackage.haskell.org/package/langchain-hs)
[](https://www.haskell.org/ghc/)
[](LICENSE)
[](https://tusharad.github.io/langchain-hs/)
---
## Installation
Install `langchain-hs` and provider packages via **Cabal**:
```bash
cabal update
cabal install langchain-hs langchain-hs-ollama langchain-hs-openai
```
Or add to your `stack.yaml`:
```yaml
extra-deps:
- langchain-hs-0.0.6.0
- langchain-hs-core-0.0.6.0
- langchain-hs-graph-0.0.6.0
- langchain-hs-ollama-0.0.6.0
- langchain-hs-openai-0.0.6.0
```
Package reference on Hackage: [https://hackage.haskell.org/package/langchain-hs](https://hackage.haskell.org/package/langchain-hs)
---
## 1. Simple Chat Calling
Send a prompt to an LLM (local Ollama, OpenAI, or Gemini) and retrieve a typed response:
```haskell
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Control.Monad.Except (runExceptT)
import qualified Data.Text.IO as T
import Langchain.Prelude
import Langchain.Provider.Ollama
main :: IO ()
main = do
-- Connect to local Ollama instance (DeepSeek, Llama 3, Gemma, etc.)
model <- newOllama "llama3.2" defaultConfig
let messages = [userMessage "Explain in two sentences why Haskell makes AI agents more reliable."]
result <- runExceptT $ invoke model messages Nothing
case result of
Left err -> T.putStrLn $ errorMessage err
Right resp -> T.putStrLn $ extractMessageText resp
```
### Output
```text
Haskell's static type system prevents common agent failures like missing dictionary keys or unexpected nulls before code ever reaches production. Furthermore, pure state transitions guarantee that autonomous agents cannot silently mutate variables behind your back.
```
---
## 2. JSON Structured Output
Guarantee that the LLM response conforms to a typed Haskell record using `FromJSON` and `ToSchema`:
```haskell
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
module Main where
import Data.Aeson (FromJSON)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import GHC.Generics (Generic)
import Langchain.Prelude
import Langchain.Provider.Ollama
-- Define your target data schema
data Developer = Developer
{ name :: T.Text
, preferredLanguage :: T.Text
, yearsOfExperience :: Int
, favoriteLibrary :: T.Text
} deriving (Show, Generic, FromJSON, ToSchema)
main :: IO ()
main = do
model <- newOllama "llama3.2" defaultConfig
let prompt =
[ userMessage "Extract profile: Alan has been writing Haskell for 8 years and loves langchain-hs." ]
options = withStructuredOutput (Proxy @Developer) defaultOllamaConfig
result <- runLangchainT () $ invoke model prompt (Just options)
case result of
Left err -> T.putStrLn $ errorMessage err
Right msg -> T.putStrLn $ extractMessageText msg
```
### Output
```json
{
"name": "Alan",
"preferredLanguage": "Haskell",
"yearsOfExperience": 8,
"favoriteLibrary": "langchain-hs"
}
```
---
## 3. Tool Calling
Equip models with effectful Haskell tools. The LLM automatically emits structured tool calls, which your application executes and feeds back for final synthesis:
```haskell
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
module Main where
import Control.Monad.IO.Class (liftIO)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Langchain.Prelude
import Langchain.Provider.Ollama
main :: IO ()
main = do
model <- newOllama "qwen2.5:7b" defaultConfig
-- Instantiate typed filesystem tool
let fsTool = findFilesTool @IO defaultSecurityPolicy
toolCfg = withTools [fsTool] defaultOllamaConfig
prompt = [userMessage "Search for Haskell source files using find_files with pattern '*.hs'."]
res <- runLangchainT () $ do
response <- invoke model prompt (Just toolCfg)
case messageToolCalls response of
Just (tCall:_) -> do
liftIO $ T.putStrLn $ "Tool Invoked: " <> toolCallName tCall
liftIO $ T.putStrLn $ "Arguments: " <> T.pack (show (toolCallArguments tCall))
-- Execute tool with LLM arguments
execResult <- liftIO $ toolExecute fsTool (toolCallArguments tCall)
let observation = case execResult of
Left err -> "Error: " <> errorMessage err
Right out -> out
liftIO $ T.putStrLn $ "Observation: " <> observation
-- Supply tool observation back to model for final response
let toolMsg = (toolMessage observation) { messageName = Just (toolCallName tCall) }
thread = prompt ++ [response, toolMsg]
finalMsg <- invoke model thread (Just toolCfg)
liftIO $ T.putStrLn "\nFinal Assistant Answer:"
liftIO $ T.putStrLn $ extractMessageText finalMsg
_ -> do
liftIO $ T.putStrLn $ extractMessageText response
case res of
Left err -> T.putStrLn $ "Error: " <> errorMessage err
Right () -> pure ()
```
### Output
```text
Tool Invoked: find_files
Arguments: fromList [("pattern",String "*.hs")]
Observation: ["src/Main.hs","src/Agent.hs","src/Tools.hs"]
Final Assistant Answer:
I scanned the repository and identified 3 Haskell source files matching your pattern:
- src/Main.hs
- src/Agent.hs
- src/Tools.hs
```
---
## 4. ReAct Agent
Run an autonomous **Reason + Act** agent loop that decomposes user tasks, selects tools, evaluates observations, and iterates until completion:
```haskell
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Control.Monad.Except (runExceptT)
import qualified Data.Text.IO as T
import Langchain.Prelude
import Langchain.Provider.Ollama
main :: IO ()
main = do
-- Setup sample file for demonstration
writeFile "/tmp/server_status.txt" "Service: api-gateway, Status: Degraded, Latency: 420ms"
model <- newOllama "qwen2.5:7b" defaultConfig
-- Equip agent with file inspection tools
let policy = defaultSecurityPolicy
tools = [readFileTool Nothing policy, findFilesTool policy]
agent = defaultReActAgent model tools
prompt = [userMessage "Inspect /tmp/server_status.txt using read_file and summarize the service health."]
res <- runExceptT $ runReActAgent agent prompt
case res of
Left err -> T.putStrLn $ errorMessage err
Right msg -> T.putStrLn $ extractMessageText msg
```
### Output
```text
[ReAct Iteration 1]
Thought: I should read the contents of /tmp/server_status.txt to check the current health status.
Action: read_file {"path": "/tmp/server_status.txt"}
Observation: Service: api-gateway, Status: Degraded, Latency: 420ms
[ReAct Iteration 2]
Thought: The file has been read. The api-gateway service is in a degraded state with elevated latency. I will now summarize this for the user.
Final Answer:
The api-gateway service is currently experiencing degraded health with high latency (420ms). Immediate investigation of the gateway service is recommended.
```
---
## Documentation & Resources
- **Hackage Package:** [https://hackage.haskell.org/package/langchain-hs](https://hackage.haskell.org/package/langchain-hs)
- **Documentation Website:** [https://tusharad.github.io/langchain-hs/](https://tusharad.github.io/langchain-hs/)
- **Whitepaper:** [whitepaper.md](whitepaper.md) (Effect polymorphism, categorical design, state graphs)
- **Examples Directory:** [`examples/`](examples/)
---
## Development & Testing
```bash
# Build project
stack build
# Run test suite
stack test
# Code quality checks
make lint
make format-check
```
---
## License
Distributed under the **MIT License**. See [LICENSE](LICENSE) for details.