packages feed

langchain-hs-0.0.3.0: src/Langchain/LLM/Ollama.hs

{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeFamilies #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}

{- |
Module      : Langchain.LLM.Ollama
Description : Ollama integration for LangChain Haskell
Copyright   : (c) 2025 Tushar Adhatrao
License     : MIT
Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>
Stability   : experimental

Ollama implementation of LangChain's LLM interface , supporting:

- Text generation
- Chat interactions
- Streaming responses
- Callback integration

Example usage:

@
-- Create Ollama configuration
ollamaLLM = Ollama "gemma3" [stdOutCallback]

-- Generate text
response <- generate ollamaLLM "Explain Haskell monads" Nothing
-- Right "Monads in Haskell..."

-- Chat interaction
let messages = UserMessage "What's the capital of France?" :| []
chatResponse <- chat ollamaLLM messages Nothing
-- Right "The capital of France is Paris."

-- Streaming
streamHandler = StreamHandler print (putStrLn "Done")
streamResult <- stream ollamaLLM messages streamHandler Nothing
@
-}
module Langchain.LLM.Ollama
  ( Ollama (..)
  , defaultOllama

    -- * Re-export
  , module Langchain.LLM.Core
  ) where

import qualified Data.List.NonEmpty as NonEmpty
import Data.Maybe (fromMaybe)
import qualified Data.Ollama.Chat as OllamaChat
import qualified Data.Ollama.Common.Types as O
import Data.Text (Text)
import qualified Data.Text as T
import Langchain.Callback (Callback, Event (..))
import Langchain.Error (llmError)
import qualified Langchain.Error as Error
import Langchain.LLM.Core
import qualified Langchain.Runnable.Core as Run

{- | Ollama LLM configuration
Contains:

- Model name (e.g., "llama3:latest")
- Callbacks for event tracking

Example:

>>> Ollama "nomic-embed" [logCallback]
Ollama "nomic-embed"
-}
data Ollama = Ollama
  { modelName :: Text
  -- ^ The name of the Ollama model
  , callbacks :: [Callback]
  -- ^ Event handlers for LLM operations
  }

instance Show Ollama where
  show (Ollama modelName _) = "Ollama " ++ show modelName

{- | Ollama implementation of the LLM typeclass
Example instance usage:

@
-- Generate text with error handling
case generate ollamaLLM "Hello" Nothing of
  Left err -> putStrLn $ "Error: " ++ err
  Right res -> putStrLn res
@
-}
instance LLM Ollama where
  type LLMParams Ollama = OllamaChat.ChatOps
  type LLMStreamTokenType Ollama = OllamaChat.ChatResponse

  -- \| Generate text from a prompt
  --  Returns Left on API errors, Right on success.
  --
  --  Example:
  --  >>> generate (Ollama "llama3.2" []) "Hello" Nothing
  --  Right "Hello! How can I assist you today?"
  generate (Ollama model cbs) prompt mbOllamaParams = do
    mapM_ (\cb -> cb LLMStart) cbs
    let chatOps_ = fromMaybe OllamaChat.defaultChatOps mbOllamaParams
        msg = OllamaChat.userMessage prompt
        chatOps =
          chatOps_
            { OllamaChat.modelName = model
            , OllamaChat.messages = [msg]
            }

    eRes <- OllamaChat.chat chatOps Nothing
    case eRes of
      Left err -> do
        mapM_ (\cb -> cb (LLMError $ show err)) cbs
        return $ Left (llmError (T.pack $ show err) Nothing Nothing)
      Right chatResponse -> do
        mapM_ (\cb -> cb LLMEnd) cbs
        case OllamaChat.message chatResponse of
          Nothing -> pure $ Left (Error.fromString "Message not found in response")
          Just m -> pure $ Right $ OllamaChat.content m

  -- \| Chat interaction with message history.
  --  Uses Ollama's chat API for multi-turn conversations.
  --
  --  Example:
  --  >>> let msgs = UserMessage "Hi" :| [AssistantMessage "Hello!"]
  --  >>> chat (Ollama "llama3" []) msgs Nothing
  --  Right "How are you today?"
  chat (Ollama model cbs) messages mbOllamaParams = do
    mapM_ (\cb -> cb LLMStart) cbs
    let chatOps_ = fromMaybe OllamaChat.defaultChatOps mbOllamaParams
        chatOps =
          chatOps_
            { OllamaChat.modelName = model
            , OllamaChat.messages = NonEmpty.map to messages
            }
    eRes <- OllamaChat.chat chatOps Nothing
    case eRes of
      Left err -> do
        mapM_ (\cb -> cb (LLMError $ show err)) cbs
        return $ Left (llmError (T.pack $ show err) Nothing Nothing)
      Right res -> do
        mapM_ (\cb -> cb LLMEnd) cbs
        case OllamaChat.message res of
          Nothing ->
            return $
              Left $
                llmError
                  (T.pack $ "Message field not found: " <> show res)
                  Nothing
                  Nothing
          Just ollamaMsg -> return $ Right (from ollamaMsg)

  -- \| Streaming response handling.
  --  Processes tokens in real-time via StreamHandler.
  --
  --  Example:
  --  >>> let handler = StreamHandler (putStr . ("Token: " ++)) (putStrLn "Complete")
  --  >>> stream (Ollama "llama3" []) messages handler Nothing
  --  Token: H Token: i Complete
  --
  -- Note: Don't pass streamHandler in ChatOps's stream field. It will be overridden.
  stream
    (Ollama model_ cbs)
    messages
    StreamHandler {onToken, onComplete}
    mbOllamaParams = do
      let chatOps_ = fromMaybe OllamaChat.defaultChatOps mbOllamaParams
          chatOps =
            chatOps_
              { OllamaChat.modelName = model_
              , OllamaChat.messages = NonEmpty.map to messages
              , OllamaChat.stream =
                  Just
                    ( onToken
                    , pure ()
                    )
              }
      mapM_ (\cb -> cb LLMStart) cbs
      eRes <- OllamaChat.chat chatOps Nothing
      case eRes of
        Left err -> do
          mapM_ (\cb -> cb (LLMError $ show err)) cbs
          return $ Left (llmError (T.pack $ show err) Nothing Nothing)
        Right _ -> do
          onComplete
          mapM_ (\cb -> cb LLMEnd) cbs
          return $ Right ()

toOllamaRole :: Role -> OllamaChat.Role
toOllamaRole User = OllamaChat.User
toOllamaRole System = OllamaChat.System
toOllamaRole Assistant = OllamaChat.Assistant
toOllamaRole Tool = OllamaChat.Tool
toOllamaRole _ = OllamaChat.User -- Ollama only supports above 4 Roles, others will be defaulted to user

fromOllamaRole :: OllamaChat.Role -> Role
fromOllamaRole OllamaChat.User = User
fromOllamaRole OllamaChat.System = System
fromOllamaRole OllamaChat.Assistant = Assistant
fromOllamaRole OllamaChat.Tool = Tool

instance MessageConvertible OllamaChat.Message where
  to Message {..} =
    OllamaChat.Message
      (toOllamaRole role)
      content
      (messageImages messageData)
      (fmap toOllamaToolCall <$> toolCalls messageData)
      (thinking messageData)
    where
      toOllamaToolCall :: ToolCall -> O.ToolCall
      toOllamaToolCall ToolCall {..} =
        O.ToolCall
          { O.outputFunction =
              O.OutputFunction
                { O.outputFunctionName = toolFunctionName toolCallFunction
                , O.arguments = toolFunctionArguments toolCallFunction
                }
          }

  from (OllamaChat.Message role' content' imgs tools think) =
    Message
      { role = fromOllamaRole role'
      , content = content'
      , messageData =
          MessageData
            { messageImages = imgs
            , toolCalls = fmap toToolCall <$> tools
            , thinking = think
            , name = Nothing
            }
      }
    where
      toToolCall :: O.ToolCall -> ToolCall
      toToolCall O.ToolCall {..} =
        ToolCall
          { toolCallId = ""
          , toolCallType = "function"
          , toolCallFunction =
              ToolFunction
                { toolFunctionName = O.outputFunctionName outputFunction
                , toolFunctionArguments = O.arguments outputFunction
                }
          }

instance Run.Runnable Ollama where
  type RunnableInput Ollama = (ChatHistory, Maybe OllamaChat.ChatOps)
  type RunnableOutput Ollama = Message

  invoke = uncurry . chat

-- | Default values for Ollama
defaultOllama :: Ollama
defaultOllama = Ollama "llama3.2" []