packages feed

langchain-hs-0.0.5.0: src/Langchain/PromptTemplate/Chat/ChatPromptTemplate.hs

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedStrings #-}

{- |
Module      : Langchain.PromptTemplate.Chat.ChatPromptTemplate
Description : ChatPromptTemplate prompt template
Copyright   : (c) 2025-2026 Tushar Adhatrao
License     : MIT
Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>
Stability   : experimental
-}
module Langchain.PromptTemplate.Chat.ChatPromptTemplate
  ( ChatPromptTemplate (..)
  , ChatPromptMessage
  , ContentPromptBlock (..)
  , ChatPromptInput (..)
  , ChatPromptValue (..)
  , PartialValue (..)
  , fromTemplate
  , fromTemplateWithOptions
  , fromMessages
  , message
  , templateMessage
  , templateMessageWithFormat
  , contentMessage
  , messagesPlaceholder
  , messagesPlaceholderWithOptions
  , append
  , extend
  , partial
  , invoke
  , formatPrompt
  , format
  , toMessages
  , toString
  ) where

import Data.Aeson (FromJSON (..), ToJSON (..), Value (..), object, withObject, (.:), (.:?), (.=))
import Data.Aeson.Types (Parser)
import Data.Either (fromRight)
import qualified Data.List.NonEmpty as NonEmpty
import qualified Data.Map.Strict as Map
import Data.Maybe (isJust)
import Data.Text (Text)
import qualified Data.Text as T
import GHC.Generics (Generic)

import Langchain.Core.Error (LangchainError, validationError)
import Langchain.Core.Model.Types
  ( ContentBlock (..)
  , ImageContent (..)
  , ImageSource (..)
  , Message (..)
  , Role (..)
  , formatMessageString
  , textMessage
  , userMessage
  )
import Langchain.PromptTemplate.Chat (BaseMessagePromptTemplate (formatMessages))
import Langchain.PromptTemplate.Chat.MessagesPlaceholder
  ( MessagesPlaceholder (..)
  , messagesPlaceholderVariableName
  )
import qualified Langchain.PromptTemplate.Chat.MessagesPlaceholder as MessagesPlaceholder
import Langchain.PromptTemplate.Prompt (PromptTemplateOptions, TemplateFormat (..))
import qualified Langchain.PromptTemplate.Prompt as Prompt

-- | A single chat message template inside a chat prompt.
data ChatPromptMessage
  = HumanMessagePrompt Prompt.PromptTemplate
  | SystemMessagePrompt Prompt.PromptTemplate
  | AIMessagePrompt Prompt.PromptTemplate
  | ChatMessagePrompt Role Prompt.PromptTemplate
  | ContentMessagePrompt Role [ContentPromptBlock]
  | MessagesPlaceholderPrompt MessagesPlaceholder (Maybe [Message])
  | StaticMessage Message
  deriving (Show, Eq, Generic, ToJSON, FromJSON)

-- | A templated block inside a multipart chat message.
data ContentPromptBlock
  = TextPromptBlock TemplateFormat Text
  | ImagePromptBlock TemplateFormat ImageContent
  deriving (Show, Eq, Generic)

instance ToJSON ContentPromptBlock where
  toJSON (TextPromptBlock templateFormat template) =
    object
      [ "type" .= ("text_prompt" :: Text)
      , "templateFormat" .= templateFormat
      , "template" .= template
      ]
  toJSON (ImagePromptBlock templateFormat imageContent) =
    object
      [ "type" .= ("image_prompt" :: Text)
      , "templateFormat" .= templateFormat
      , "imageContent" .= imageContentToJSON imageContent
      ]

instance FromJSON ContentPromptBlock where
  parseJSON = withObject "ContentPromptBlock" $ \value -> do
    blockType <- value .: "type"
    case (blockType :: Text) of
      "text_prompt" -> TextPromptBlock <$> value .: "templateFormat" <*> value .: "template"
      "image_prompt" ->
        ImagePromptBlock <$> value .: "templateFormat" <*> (value .: "imageContent" >>= parseImageContent)
      other -> fail $ "Unknown ContentPromptBlock type: " ++ show other

imageContentToJSON :: ImageContent -> Value
imageContentToJSON ImageContent {imageSource = source, imageDetail = detail, imageMetadata = metadata} =
  object
    [ "source" .= imageSourceToJSON source
    , "detail" .= detail
    , "metadata" .= metadata
    ]

imageSourceToJSON :: ImageSource -> Value
imageSourceToJSON (ImageBase64 mime sourceData) =
  object
    [ "type" .= ("base64" :: Text)
    , "mimeType" .= mime
    , "data" .= sourceData
    ]
imageSourceToJSON (ImageUrl url) =
  object
    [ "type" .= ("url" :: Text)
    , "url" .= url
    ]

parseImageContent :: Value -> Parser ImageContent
parseImageContent = withObject "ImageContent" $ \value ->
  ImageContent
    <$> (value .: "source" >>= parseImageSource)
    <*> value .:? "detail"
    <*> value .:? "metadata"

parseImageSource :: Value -> Parser ImageSource
parseImageSource = withObject "ImageSource" $ \value -> do
  sourceType <- value .: "type"
  case (sourceType :: Text) of
    "base64" -> ImageBase64 <$> value .:? "mimeType" <*> value .: "data"
    "url" -> ImageUrl <$> value .: "url"
    other -> fail $ "Unknown ImageSource type: " ++ show other

-- | A chat prompt template made of ordered message templates.
data ChatPromptTemplate = ChatPromptTemplate
  { messages :: [ChatPromptMessage]
  , inputVariables :: [Text]
  }
  deriving (Show, Eq, Generic, ToJSON, FromJSON)

-- | A rendered chat prompt as concrete messages.
newtype ChatPromptValue = ChatPromptValue
  { messages :: [Message]
  }
  deriving (Show, Eq, Generic, ToJSON, FromJSON)

-- | Inputs accepted by 'invoke' for chat prompts.
data ChatPromptInput
  = ChatPromptVariables (Map.Map Text Text)
  | ChatPromptMessageList [Message]
  | ChatPromptInputs (Map.Map Text Text) (Map.Map Text [Message])
  deriving (Show, Eq, Generic, ToJSON, FromJSON)

-- | Partial values that can pre-bind text or message placeholders.
data PartialValue
  = PartialText Text
  | PartialMessages [Message]
  deriving (Show, Eq, Generic, ToJSON, FromJSON)

-- | Create a single-message user chat prompt from raw text.
fromTemplate :: Text -> ChatPromptTemplate
fromTemplate template = fromTemplateWithOptions template Prompt.defaultPromptTemplateOptions

-- | Create a single-message user chat prompt with partial variables.
fromTemplateWithOptions :: Text -> PromptTemplateOptions -> ChatPromptTemplate
fromTemplateWithOptions template options =
  let promptTemplate = Prompt.fromTemplateWithOptions template options
   in ChatPromptTemplate
        { messages = [ChatMessagePrompt User promptTemplate]
        , inputVariables = Prompt.inputVariables promptTemplate
        }

-- | Create a chat prompt from an explicit list of message templates.
fromMessages :: [ChatPromptMessage] -> ChatPromptTemplate
fromMessages promptMessages =
  ChatPromptTemplate
    { messages = promptMessages
    , inputVariables = unique $ concatMap messageInputVariables promptMessages
    }

-- | Wrap a concrete message as part of a chat prompt.
message :: Message -> ChatPromptMessage
message = StaticMessage

-- | Create a templated message for a specific role.
templateMessage :: Role -> Text -> ChatPromptMessage
templateMessage role = ChatMessagePrompt role . Prompt.fromTemplate

-- | Create a templated message for a specific role and template format.
templateMessageWithFormat :: Role -> TemplateFormat -> Text -> ChatPromptMessage
templateMessageWithFormat role templateFormat template =
  ChatMessagePrompt role $
    Prompt.fromTemplateWithFormat template templateFormat Map.empty

-- | Create a multipart content message for a specific role.
contentMessage :: Role -> [ContentPromptBlock] -> ChatPromptMessage
contentMessage = ContentMessagePrompt

-- | Create a placeholder for an injected message list.
messagesPlaceholder :: Text -> ChatPromptMessage
messagesPlaceholder name = messagesPlaceholderWithOptions $ MessagesPlaceholder.messagesPlaceholderOptions name

-- | Create a message-list placeholder with explicit options.
messagesPlaceholderWithOptions ::
  MessagesPlaceholder.MessagesPlaceholderOptions -> ChatPromptMessage
messagesPlaceholderWithOptions options =
  MessagesPlaceholderPrompt (MessagesPlaceholder.messagesPlaceholderWithOptions options) Nothing

-- | Append one message template to the end of a chat prompt.
append :: ChatPromptTemplate -> ChatPromptMessage -> ChatPromptTemplate
append chatPromptTemplate promptMessage = extend chatPromptTemplate [promptMessage]

-- | Append multiple message templates to the end of a chat prompt.
extend :: ChatPromptTemplate -> [ChatPromptMessage] -> ChatPromptTemplate
extend ChatPromptTemplate {messages = promptMessages} newMessages =
  fromMessages $ promptMessages <> newMessages

-- | Apply partial text and message bindings to a chat prompt.
partial :: ChatPromptTemplate -> Map.Map Text PartialValue -> ChatPromptTemplate
partial ChatPromptTemplate {messages = promptMessages} partialVariables =
  fromMessages $ map (`partialMessage` partialVariables) promptMessages

-- | Render a chat prompt to concrete messages.
formatPrompt :: ChatPromptTemplate -> Map.Map Text Text -> Either LangchainError ChatPromptValue
formatPrompt ChatPromptTemplate {messages = promptMessages} variables =
  formatPromptWithMessages promptMessages variables Map.empty

-- | Render a chat prompt with either variables or message-list inputs.
invoke :: ChatPromptTemplate -> ChatPromptInput -> Either LangchainError ChatPromptValue
invoke chatPromptTemplate (ChatPromptVariables variables) = formatPrompt chatPromptTemplate variables
invoke ChatPromptTemplate {messages = [MessagesPlaceholderPrompt placeholder _]} (ChatPromptMessageList promptMessages) =
  ChatPromptValue
    <$> formatMessages
      placeholder
      (Map.singleton (messagesPlaceholderVariableName placeholder) promptMessages)
invoke ChatPromptTemplate {messages = promptMessages} (ChatPromptInputs variables messageVariables) =
  formatPromptWithMessages promptMessages variables messageVariables
invoke _ (ChatPromptMessageList _) =
  Left $
    validationError
      "List input is only supported for a single MessagesPlaceholder"
      (Just "ChatPromptTemplate")
      (Just "invoke")

-- | Render a chat prompt to a single formatted text value.
format :: ChatPromptTemplate -> Map.Map Text Text -> Either LangchainError Text
format chatPromptTemplate variables = toString <$> formatPrompt chatPromptTemplate variables

-- | Extract the concrete messages from a rendered chat prompt.
toMessages :: ChatPromptValue -> [Message]
toMessages (ChatPromptValue promptMessages) = promptMessages

-- | Render a chat prompt as newline-separated message text.
toString :: ChatPromptValue -> Text
toString (ChatPromptValue promptMessages) =
  T.intercalate "\n" $ map formatMessageString promptMessages

messageInputVariables :: ChatPromptMessage -> [Text]
messageInputVariables (HumanMessagePrompt promptTemplate) = Prompt.inputVariables promptTemplate
messageInputVariables (SystemMessagePrompt promptTemplate) = Prompt.inputVariables promptTemplate
messageInputVariables (AIMessagePrompt promptTemplate) = Prompt.inputVariables promptTemplate
messageInputVariables (ChatMessagePrompt _ promptTemplate) = Prompt.inputVariables promptTemplate
messageInputVariables (ContentMessagePrompt _ blocks) = unique $ concatMap contentBlockInputVariables blocks
messageInputVariables
  ( MessagesPlaceholderPrompt
      MessagesPlaceholder
        { variableName = variableName'
        , optional = optional'
        }
      storedMessages
    )
    | optional' || isJust storedMessages = []
    | otherwise = [variableName']
messageInputVariables (StaticMessage _) = []

partialMessage :: ChatPromptMessage -> Map.Map Text PartialValue -> ChatPromptMessage
partialMessage (HumanMessagePrompt promptTemplate) partialVariables =
  HumanMessagePrompt $
    Prompt.partialPromptTemplate promptTemplate (textPartialVariables partialVariables)
partialMessage (SystemMessagePrompt promptTemplate) partialVariables =
  SystemMessagePrompt $
    Prompt.partialPromptTemplate promptTemplate (textPartialVariables partialVariables)
partialMessage (AIMessagePrompt promptTemplate) partialVariables =
  AIMessagePrompt $
    Prompt.partialPromptTemplate promptTemplate (textPartialVariables partialVariables)
partialMessage (ChatMessagePrompt role promptTemplate) partialVariables =
  ChatMessagePrompt role $
    Prompt.partialPromptTemplate promptTemplate (textPartialVariables partialVariables)
partialMessage (ContentMessagePrompt role blocks) partialVariables =
  ContentMessagePrompt role $
    map (\block -> partialContentBlock block (textPartialVariables partialVariables)) blocks
partialMessage (MessagesPlaceholderPrompt placeholder storedMessages) partialVariables =
  MessagesPlaceholderPrompt placeholder $
    case Map.lookup (messagesPlaceholderVariableName placeholder) partialVariables of
      Just (PartialMessages promptMessages) -> Just promptMessages
      _ -> storedMessages
partialMessage (StaticMessage staticMessage) _ = StaticMessage staticMessage

textPartialVariables :: Map.Map Text PartialValue -> Map.Map Text Text
textPartialVariables = Map.mapMaybe toText
  where
    toText :: PartialValue -> Maybe Text
    toText (PartialText value) = Just value
    toText (PartialMessages _) = Nothing

formatPromptWithMessages ::
  [ChatPromptMessage] ->
  Map.Map Text Text ->
  Map.Map Text [Message] ->
  Either LangchainError ChatPromptValue
formatPromptWithMessages promptMessages variables messageVariables =
  ChatPromptValue . concat
    <$> traverse (\promptMessage -> formatMessage promptMessage variables messageVariables) promptMessages

formatMessage ::
  ChatPromptMessage -> Map.Map Text Text -> Map.Map Text [Message] -> Either LangchainError [Message]
formatMessage (HumanMessagePrompt promptTemplate) variables _ =
  (: []) . userMessage <$> Prompt.renderPrompt promptTemplate variables
formatMessage (SystemMessagePrompt promptTemplate) variables _ =
  (: []) . textMessage System <$> Prompt.renderPrompt promptTemplate variables
formatMessage (AIMessagePrompt promptTemplate) variables _ =
  (: []) . textMessage Assistant <$> Prompt.renderPrompt promptTemplate variables
formatMessage (ChatMessagePrompt role promptTemplate) variables _ =
  (: []) . textMessage role <$> Prompt.renderPrompt promptTemplate variables
formatMessage (ContentMessagePrompt role blocks) variables _ = do
  renderedBlocks <- concat <$> traverse (renderContentBlock variables) blocks
  case NonEmpty.nonEmpty renderedBlocks of
    Nothing -> Right []
    Just nonEmptyBlocks -> Right [Message role nonEmptyBlocks Nothing Nothing Nothing Map.empty]
formatMessage (MessagesPlaceholderPrompt placeholder storedMessages) _ messageVariables =
  formatMessages placeholder $
    case storedMessages of
      Nothing -> messageVariables
      Just promptMessages ->
        messageVariables
          `Map.union` Map.singleton (messagesPlaceholderVariableName placeholder) promptMessages
formatMessage (StaticMessage staticMessage) _ _ = Right [staticMessage]

contentBlockInputVariables :: ContentPromptBlock -> [Text]
contentBlockInputVariables (TextPromptBlock templateFormat template) =
  Prompt.extractTemplateVariablesWithFormat templateFormat template
contentBlockInputVariables (ImagePromptBlock templateFormat imageContent) =
  imageContentInputVariables templateFormat imageContent

partialContentBlock :: ContentPromptBlock -> Map.Map Text Text -> ContentPromptBlock
partialContentBlock (TextPromptBlock templateFormat template) partials =
  TextPromptBlock templateFormat $ renderPartial templateFormat partials template
partialContentBlock (ImagePromptBlock templateFormat imageContent) partials =
  ImagePromptBlock templateFormat $ partialImageContent templateFormat partials imageContent

renderContentBlock ::
  Map.Map Text Text -> ContentPromptBlock -> Either LangchainError [ContentBlock]
renderContentBlock variables (TextPromptBlock templateFormat template) = do
  rendered <- renderTemplate templateFormat variables template
  pure [TextBlock rendered | not (T.null rendered)]
renderContentBlock variables (ImagePromptBlock templateFormat imageContent) = do
  renderedImage <- renderImageContent templateFormat variables imageContent
  pure [ImageBlock renderedImage]

imageContentInputVariables :: TemplateFormat -> ImageContent -> [Text]
imageContentInputVariables templateFormat ImageContent {imageSource = source, imageDetail = detail, imageMetadata = metadata} =
  imageSourceInputVariables templateFormat source
    <> maybe [] (Prompt.extractTemplateVariablesWithFormat templateFormat) detail
    <> maybe [] (valueInputVariables templateFormat) metadata

imageSourceInputVariables :: TemplateFormat -> ImageSource -> [Text]
imageSourceInputVariables templateFormat (ImageBase64 _ imageTemplate) =
  Prompt.extractTemplateVariablesWithFormat templateFormat imageTemplate
imageSourceInputVariables templateFormat (ImageUrl url) =
  Prompt.extractTemplateVariablesWithFormat templateFormat url

partialImageContent :: TemplateFormat -> Map.Map Text Text -> ImageContent -> ImageContent
partialImageContent templateFormat partials ImageContent {imageSource = source, imageDetail = detail, imageMetadata = metadata} =
  ImageContent
    { imageSource = partialImageSource templateFormat partials source
    , imageDetail = renderPartial templateFormat partials <$> detail
    , imageMetadata = renderPartialValue templateFormat partials <$> metadata
    }

partialImageSource :: TemplateFormat -> Map.Map Text Text -> ImageSource -> ImageSource
partialImageSource templateFormat partials (ImageBase64 mime imageTemplate) =
  ImageBase64 mime $ renderPartial templateFormat partials imageTemplate
partialImageSource templateFormat partials (ImageUrl url) =
  ImageUrl $ renderPartial templateFormat partials url

renderImageContent ::
  TemplateFormat -> Map.Map Text Text -> ImageContent -> Either LangchainError ImageContent
renderImageContent templateFormat variables ImageContent {imageSource = source, imageDetail = detail, imageMetadata = metadata} = do
  renderedSource <- renderImageSource templateFormat variables source
  renderedDetail <- traverse (renderTemplate templateFormat variables) detail
  renderedMetadata <- traverse (renderValue templateFormat variables) metadata
  pure $ ImageContent renderedSource renderedDetail renderedMetadata

renderImageSource ::
  TemplateFormat -> Map.Map Text Text -> ImageSource -> Either LangchainError ImageSource
renderImageSource templateFormat variables (ImageBase64 mime imageTemplate) =
  ImageBase64 mime <$> renderTemplate templateFormat variables imageTemplate
renderImageSource templateFormat variables (ImageUrl url) =
  ImageUrl <$> renderTemplate templateFormat variables url

renderTemplate :: TemplateFormat -> Map.Map Text Text -> Text -> Either LangchainError Text
renderTemplate templateFormat variables template =
  Prompt.renderPrompt
    (Prompt.fromTemplateWithFormat template templateFormat Map.empty)
    variables

renderPartial :: TemplateFormat -> Map.Map Text Text -> Text -> Text
renderPartial templateFormat partials template =
  fromRight template $ renderTemplate templateFormat partials template

valueInputVariables :: TemplateFormat -> Value -> [Text]
valueInputVariables templateFormat (String value) =
  Prompt.extractTemplateVariablesWithFormat templateFormat value
valueInputVariables templateFormat (Array values) =
  concatMap (valueInputVariables templateFormat) values
valueInputVariables templateFormat (Object objectValue) =
  concatMap (valueInputVariables templateFormat) objectValue
valueInputVariables _ _ = []

renderValue :: TemplateFormat -> Map.Map Text Text -> Value -> Either LangchainError Value
renderValue templateFormat variables (String value) =
  String <$> renderTemplate templateFormat variables value
renderValue templateFormat variables (Array values) =
  Array <$> traverse (renderValue templateFormat variables) values
renderValue templateFormat variables (Object objectValue) =
  Object <$> traverse (renderValue templateFormat variables) objectValue
renderValue _ _ value = Right value

renderPartialValue :: TemplateFormat -> Map.Map Text Text -> Value -> Value
renderPartialValue templateFormat partials value =
  fromRight value $ renderValue templateFormat partials value

unique :: [Text] -> [Text]
unique = foldl addIfMissing []
  where
    addIfMissing :: [Text] -> Text -> [Text]
    addIfMissing variableNames name
      | name `elem` variableNames = variableNames
      | otherwise = variableNames <> [name]