langchain-hs-gemini-0.0.6.0: src/Langchain/Provider/Gemini.hs
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeFamilies #-}
{- |
Module : Langchain.Provider.Gemini
Description : Google Gemini provider implementing ChatModel via Servant Interactions v1 API
Copyright : (c) 2025-2026 Tushar Adhatrao
License : MIT
Maintainer : Tushar Adhatrao <tusharadhatrao@gmail.com>
Stability : experimental
Gemini provider built on the Google Gemini Interactions v1 API using Servant client bindings.
Supports multi-modal content, multi-turn dialogues, tool calling, and SSE streaming.
-}
module Langchain.Provider.Gemini
( Gemini (..)
, GeminiConfig (..)
, defaultGeminiConfig
, newGemini
, messagesToInteractionsInput
, interactionToMessage
) where
import Control.Applicative ((<|>))
import Control.Monad.Except (throwError)
import Control.Monad.IO.Class (liftIO)
import Data.Aeson (FromJSON, ToJSON, decodeStrict, object, toJSON, (.=))
import Data.Conduit (await, yield, (.|))
import qualified Data.List as List
import qualified Data.List.NonEmpty as NonEmpty
import qualified Data.Map.Strict as Map
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import GHC.Generics (Generic)
import Gemini.Client
( CreateInteractionRequest (..)
, GenerationConfig (..)
, Interaction (..)
, InteractionSseEvent (..)
, InteractionsInput (..)
, ResponseFormat (..)
, Step (..)
, StepDeltaData (..)
, Usage (..)
, createInteraction
, defaultCreateInteractionRequest
, newGeminiClientEnv
, newGeminiClientEnvWithBaseUrl
, runGeminiClient
, streamGeminiInteraction
)
import qualified Gemini.Client.Types as GTypes
import Langchain.Cache.Core (CacheableChatModel (..))
import Langchain.Error (llmError)
import Langchain.Model
import Langchain.Stream (StreamEvent (..), TokenUsage (..))
import qualified Langchain.Tool as CoreTool
import Langchain.Tool.Binding (ToolBinder (..))
-- | Runtime configuration for Gemini interactions.
data GeminiConfig = GeminiConfig
{ configTools :: !(Maybe [GTypes.Tool])
-- ^ Tools (functions, Google Search, Code Execution, etc.) available to the model.
, configGenerationConfig :: !(Maybe GenerationConfig)
-- ^ Generation parameters such as token limits, stop sequences, and thinking level.
, configResponseFormat :: !(Maybe ResponseFormat)
-- ^ Output format constraints, e.g. JSON schema enforcement.
, configSystemInstruction :: !(Maybe Text)
-- ^ System instructions directing the model's persona or constraints.
, configPreviousInteractionId :: !(Maybe Text)
-- ^ Interaction ID to continue an ongoing session.
}
deriving (Eq, Show, Generic, ToJSON, FromJSON)
-- | Default empty Gemini configuration.
defaultGeminiConfig :: GeminiConfig
defaultGeminiConfig =
GeminiConfig
{ configTools = Nothing
, configGenerationConfig = Nothing
, configResponseFormat = Nothing
, configSystemInstruction = Nothing
, configPreviousInteractionId = Nothing
}
-- | Gemini ChatModel provider instance.
data Gemini = Gemini
{ apiKey :: !Text
, model :: !Text
, baseUrl :: !(Maybe Text)
}
deriving (Eq, Show, Generic)
-- | Create a new Gemini provider instance.
newGemini ::
-- | Google Gemini API key
Text ->
-- | Model name (e.g. @"gemini-3.6-flash"@)
Text ->
-- | Optional custom base URL
Maybe Text ->
Gemini
newGemini = Gemini
-- | Convert Langchain ContentBlock to Gemini Content.
contentBlockToGemini :: ContentBlock -> GTypes.Content
contentBlockToGemini (TextBlock t) = GTypes.TextContent t
contentBlockToGemini (ImageBlock ImageContent {imageSource = ImageBase64 (Just mime) b64}) =
GTypes.ImageContent mime b64
contentBlockToGemini (ImageBlock ImageContent {imageSource = ImageBase64 Nothing b64}) =
GTypes.ImageContent "image/jpeg" b64
contentBlockToGemini (ImageBlock ImageContent {imageSource = ImageUrl url}) =
GTypes.TextContent ("[Image URL: " <> url <> "]")
contentBlockToGemini (AudioBlock mime b64) = GTypes.AudioContent mime b64
contentBlockToGemini (DataBlock _) = GTypes.TextContent "[Data block]"
-- | Convert a Langchain Message to Gemini Step(s).
messageToSteps :: [ToolCall] -> Message -> [Step]
messageToSteps priorToolCalls msg = case messageRole msg of
User ->
let contents = map contentBlockToGemini (NonEmpty.toList (messageContents msg))
in [UserInputStep contents]
Assistant ->
let contents = map contentBlockToGemini (NonEmpty.toList (messageContents msg))
textContent = [c | c@(GTypes.TextContent t) <- contents, not (T.null t)]
outputSteps = [ModelOutputStep textContent | not (null textContent)]
toolSteps = case messageToolCalls msg of
Nothing -> []
Just tcs ->
[ FunctionCallStep
{ callName = toolCallName tc
, callArguments = toolCallArguments tc
, callId = toolCallId tc
}
| tc <- tcs
]
in outputSteps ++ toolSteps
Tool ->
let payload = parseOrWrap (extractMessageText msg)
tId = fromMaybe "" (messageToolId msg)
tName = messageName msg <|> lookupToolName tId priorToolCalls
in [FunctionResultStep tId tName payload Nothing]
Function ->
let payload = parseOrWrap (extractMessageText msg)
tId = fromMaybe "" (messageToolId msg)
tName = messageName msg <|> lookupToolName tId priorToolCalls
in [FunctionResultStep tId tName payload Nothing]
System ->
-- System instructions are extracted separately when possible
let contents = map contentBlockToGemini (NonEmpty.toList (messageContents msg))
in [UserInputStep contents]
Developer ->
let contents = map contentBlockToGemini (NonEmpty.toList (messageContents msg))
in [UserInputStep contents]
where
lookupToolName toolId calls =
toolCallName <$> List.find ((== toolId) . toolCallId) calls
parseOrWrap rawTxt =
case decodeStrict (TE.encodeUtf8 rawTxt) of
Just val -> val
Nothing -> object ["result" .= rawTxt]
-- | Convert a list of Langchain Messages to an 'InteractionsInput'.
messagesToInteractionsInput :: [Message] -> (Maybe Text, InteractionsInput)
messagesToInteractionsInput msgs =
case msgs of
[singleMsg]
| messageRole singleMsg == User
, [TextBlock t] <- NonEmpty.toList (messageContents singleMsg) ->
(Nothing, TextInput t)
_ ->
let (sysMsgs, nonSysMsgs) = List.partition isSystem msgs
extractedSys = case sysMsgs of
[] -> Nothing
_ -> Just $ T.intercalate "\n" (map extractMessageText sysMsgs)
priorToolCalls = concatMap (fromMaybe [] . messageToolCalls) msgs
allSteps = concatMap (messageToSteps priorToolCalls) nonSysMsgs
in (extractedSys, StepListInput allSteps)
where
isSystem m = messageRole m `elem` [System, Developer]
-- | Convert an t'Interaction' result to a Langchain t'Message'.
interactionToMessage :: Interaction -> Message
interactionToMessage Interaction {..} =
let outputTexts =
[ t
| ModelOutputStep cs <- interactionSteps
, GTypes.TextContent t <- cs
]
toolCalls =
[ ToolCall
{ toolCallId = callId
, toolCallType = "function"
, toolCallName = callName
, toolCallArguments = callArguments
}
| FunctionCallStep {..} <- interactionSteps
]
thoughtSigs =
[ sig
| ThoughtStep {thoughtSignature = Just sig} <- interactionSteps
]
base = assistantMessage (T.intercalate "\n" outputTexts)
withCalls =
if null toolCalls
then base
else base {messageToolCalls = Just toolCalls}
withMeta =
if null thoughtSigs
then withCalls
else
withCalls
{ messageMetadata =
Map.insert
"langchain.gemini.thoughtSignatures"
(toJSON thoughtSigs)
(messageMetadata withCalls)
}
in withMeta
-- | Build a t'CreateInteractionRequest' from Gemini configuration and messages.
buildCreateRequest :: Gemini -> [Message] -> Maybe GeminiConfig -> CreateInteractionRequest
buildCreateRequest Gemini {..} msgs mbConfig =
let (mbExtractedSys, input) = messagesToInteractionsInput msgs
cfg = fromMaybe defaultGeminiConfig mbConfig
finalSys = configSystemInstruction cfg <|> mbExtractedSys
baseReq = defaultCreateInteractionRequest input
in baseReq
{ reqModel = Just model
, reqSystemInstruction = finalSys
, reqTools = configTools cfg
, reqGenerationConfig = configGenerationConfig cfg
, reqResponseFormat = configResponseFormat cfg
, reqPreviousInteractionId = configPreviousInteractionId cfg
}
instance ChatModel Gemini where
type ModelConfig Gemini = GeminiConfig
invoke provider msgs mbConfig = do
let req = buildCreateRequest provider msgs mbConfig
env <- liftIO $ case baseUrl provider of
Just u -> newGeminiClientEnvWithBaseUrl u (apiKey provider)
Nothing -> newGeminiClientEnv (apiKey provider)
eRes <- liftIO $ runGeminiClient env (createInteraction (apiKey provider) req)
case eRes of
Left err ->
throwError $
llmError (redactKey (apiKey provider) (T.pack (show err))) Nothing Nothing
Right interaction -> pure $ interactionToMessage interaction
stream provider msgs mbConfig = do
let rId = "gemini-stream-run"
mName = model provider
yield $ LLMStart rId mName msgs
env <- liftIO $ case baseUrl provider of
Just u -> newGeminiClientEnvWithBaseUrl u (apiKey provider)
Nothing -> newGeminiClientEnv (apiKey provider)
let req = buildCreateRequest provider msgs mbConfig
(accumulated, mbCompletedInter) <-
streamGeminiInteraction env req .| consumeStream rId "" Nothing
case mbCompletedInter of
Just completedInter -> do
let finalMsg = interactionToMessage completedInter
usage = toTokenUsage (interactionUsage completedInter)
yield $ LLMEnd rId finalMsg usage
Nothing -> do
let finalMsg = assistantMessage accumulated
yield $ LLMEnd rId finalMsg Nothing
where
consumeStream rId acc mbCompleted =
await >>= \case
Nothing -> pure (acc, mbCompleted)
Just (SseStepDelta _ (TextDelta txt)) -> do
yield $ LLMChunk rId txt Nothing
consumeStream rId (acc <> txt) mbCompleted
Just (SseInteractionCompleted inter) ->
consumeStream rId acc (Just inter)
Just (SseError code msg) ->
throwError $
llmError (code <> ": " <> msg) Nothing Nothing
Just _ ->
consumeStream rId acc mbCompleted
toTokenUsage Nothing = Nothing
toTokenUsage (Just Usage {..}) =
Just $
TokenUsage
{ promptTokens = fromMaybe 0 totalInputTokens
, completionTokens = fromMaybe 0 totalOutputTokens
, totalTokens = fromMaybe 0 totalTokens
}
instance ToolBinder Gemini m where
bindToolsConfig tools mbConfig =
let cfg = fromMaybe defaultGeminiConfig mbConfig
geminiTools = map toGeminiTool tools
mergedTools = case configTools cfg of
Nothing -> Just geminiTools
Just existing -> Just (existing ++ geminiTools)
in Just cfg {configTools = mergedTools}
where
toGeminiTool t =
GTypes.FunctionTool
(CoreTool.toolName t)
(Just $ CoreTool.toolDescription t)
(Just $ CoreTool.toolSchema t)
instance CacheableChatModel Gemini where
cacheModelIdentity Gemini {model = mName} _ =
object
[ "provider" .= ("gemini" :: Text)
, "model" .= mName
]
-- | Redact API key from strings to prevent accidental leakage in errors.
redactKey :: Text -> Text -> Text
redactKey key txt
| T.null key = txt
| otherwise = T.replace key "[REDACTED]" txt