Function & Tool Calling

Give local LLMs access to custom Haskell functions and external APIs with structured tool calling.

What is Tool Calling?

Tool calling enables the language model to invoke your custom Haskell functions when it needs external data (such as querying a database, calculating math, or checking the current weather).

The model does not run the code itself; instead, it returns a structured ToolCall containing the function name and arguments. Your Haskell program executes the function and passes the result back to the model.


1. Define a Tool in Haskell

Use the Tool and FunctionDef types to declare your tool signature:

{-# LANGUAGE OverloadedStrings #-}
module Main where

import Data.Aeson (Value (..), decode)
import Data.List.NonEmpty (NonEmpty ((:|)))
import Data.Map.Strict qualified as Map
import Data.Text (Text)
import Data.Text.Lazy qualified as TL
import Data.Text.Lazy.Encoding qualified as TLE
import Ollama

-- 1. Define the Calculator Tool
calculatorTool :: Tool
calculatorTool = Tool "function" $ FunctionDef
  { fnName = "get_stock_price"
  , fnDescription = Just "Fetch the current stock price for a given ticker symbol."
  , fnParameters = Just FunctionParameters
      { fpType = "object"
      , fpProperties = Just $ Map.fromList
          [ ( "symbol"
            , FunctionParameters
                { fpType = "string"
                , fpProperties = Nothing
                , fpRequired = Nothing
                , fpDescription = Just "Stock ticker symbol (e.g. AAPL, GOOGL)"
                , fpEnum = Nothing
                }
            )
          ]
      , fpRequired = Just ["symbol"]
      , fpDescription = Nothing
      , fpEnum = Nothing
      }
  , fnStrict = Just True
  }

2. Multi-Turn Tool Execution Loop

Pass the tool in chatTools and inspect the model’s response:

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

  let userMsg = userMessage "What is the stock price of AAPL?"
      req = (chatRequest "qwen3.5:2b" (userMsg :| []))
        { chatTools = Just [calculatorTool] }

  putStrLn "Sending user prompt to model..."
  res <- chat client req

  case res of
    Left err -> print err
    Right resp -> case crMessage resp of
      Just assistantMsg -> do
        case messageToolCalls assistantMsg of
          Just (toolCall : _) -> do
            let fn = tcFunction toolCall
            putStrLn $ "LLM requested tool call: " <> show (tcfName fn)
            putStrLn $ "With arguments: " <> show (tcfArguments fn)

            -- 3. Execute custom Haskell logic
            let resultText = "AAPL is currently trading at $234.50 USD (+1.8%)"

            -- 4. Send tool result back to the model (content and tool name)
            let toolRespMsg = toolResultMessage resultText (tcfName fn)
                followUpHistory = userMsg :| [assistantMsg, toolRespMsg]
                followUpReq = chatRequest "qwen3.5:2b" followUpHistory

            finalRes <- chat client followUpReq
            case finalRes of
              Right finalResp -> mapM_ (print . messageContent) (crMessage finalResp)
              Left err        -> print err

          Nothing ->
            putStrLn $ "Model responded directly: " <> show (messageContent assistantMsg)
      Nothing -> putStrLn "No message returned"