Chat & Conduit Streaming

Learn how to execute multi-turn conversations and stream tokens in real-time with Conduit.

Overview

The ollama-haskell library provides two primary ways to interact with chat models: 1. Synchronous Chat (chat): Blocks until the full response is generated and returned. 2. Streaming Chat (chatStream): Streams chunks token-by-token using conduit for interactive user interfaces and terminal CLI apps.


1. Basic Multi-Turn Conversation

Construct chat requests using NonEmpty Message:

{-# LANGUAGE OverloadedStrings #-}
module Main where

import Data.List.NonEmpty (NonEmpty ((:|)))
import Data.Text.IO qualified as TIO
import Ollama

main :: IO ()
main = do
  client <- defaultClient

  -- Build a multi-turn message history
  let history =
        systemMessage "You are a concise Haskell expert."
        :| [ userMessage "What is a Functor?"
           , assistantMessage "A Functor is a type class that can be mapped over using fmap."
           , userMessage "Can you give an example?"
           ]
      req = chatRequest "qwen3.5:2b" history

  res <- chat client req
  case res of
    Left err   -> putStrLn $ "Error: " <> show err
    Right resp -> case crMessage resp of
      Just msg -> TIO.putStrLn (messageContent msg)
      Nothing  -> putStrLn "No response message"

2. Real-Time Streaming with Conduit

Stream response tokens to the terminal as they are generated by the model:

{-# LANGUAGE OverloadedStrings #-}
module Main where

import Conduit
import Data.List.NonEmpty (NonEmpty ((:|)))
import Data.Text.IO qualified as TIO
import Ollama

main :: IO ()
main = do
  client <- defaultClient

  let req = chatRequest "qwen3.5:2b" (userMessage "Write a poem about functional programming." :| [])

  -- Stream tokens directly to stdout
  runConduit $
    chatStream client req
    .| mapM_C (\chunk -> do
        case crMessage chunk of
          Just msg -> liftIO $ do
            TIO.putStr (messageContent msg)
            hFlush stdout
          Nothing  -> pure ()
      )

  putStrLn "\n--- Stream complete ---"

3. Stream Combinators (collectStream & foldStream)

If you want to consume stream results into a list or accumulate text without setting up custom conduits:

-- Collect all chunks into a list
chunks <- collectStream (chatStream client req)

-- Or fold text tokens into a single Text string
fullText <- foldStream (\acc chunk ->
  acc <> maybe "" messageContent (crMessage chunk)) "" (chatStream client req) 

4. Multi-Turn Session State with STM (InMemoryStore)

For interactive chatbots, Ollama.Conversation provides a thread-safe transactional store:

{-# LANGUAGE OverloadedStrings #-}
module Main where

import Data.List.NonEmpty (NonEmpty ((:|)))
import Data.Time (getCurrentTime)
import Ollama
import Ollama.Conversation

main :: IO ()
main = do
  client <- defaultClient
  store  <- initInMemoryStore
  now    <- getCurrentTime

  let sessionId = "101"
      initialConv = Conversation sessionId [userMessage "My favorite language is Haskell."] "qwen3.5:9b" now now

  -- Save initial conversation
  saveConversationInMemory store initialConv

  -- Later in another request, load existing history:
  prevConv <- loadConversationInMemory store sessionId
  case prevConv of
    Nothing -> putStrLn "No conversation found"
    Just prev -> do
      let newMsg  = userMessage "What is my favorite language?"
          allMsgs = messages prev <> [newMsg]

      case allMsgs of
        (firstMsg : rest) -> do
          let req = chatRequest "qwen3.5:9b" (firstMsg :| rest)
          res <- chat client req
          case res of
            Left err -> print err
            Right resp -> case crMessage resp of
              Nothing -> pure ()
              Just botMsg -> do
                updatedTime <- getCurrentTime
                let updatedConvo = prev
                      { messages    = allMsgs <> [assistantMessage (messageContent botMsg)]
                      , lastUpdated = updatedTime
                      }
                saveConversationInMemory store updatedConvo
                putStrLn "History Saved!"
        [] -> pure ()