packages feed

langchain-hs-0.0.5.0: src/Langchain/PromptTemplate/String.hs

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

{- |
Module      : Langchain.PromptTemplate.String
Description : String prompt template formatting helpers
Copyright   : (c) 2025-2026 Tushar Adhatrao
License     : MIT
Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>
Stability   : experimental

String template parsing, variable extraction, and interpolation helpers.
-}
module Langchain.PromptTemplate.String
  ( TemplateFormat (..)
  , renderTemplateWithFormat
  , renderFStringTemplate
  , extractTemplateVariables
  , extractTemplateVariablesWithFormat
  ) where

import Data.Aeson (FromJSON, ToJSON)
import Data.Char (isDigit)
import Data.Foldable (traverse_)
import qualified Data.Map.Strict as Map
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Format.Heavy.Build (formatEither)
import Data.Text.Format.Heavy.Instances ()
import Data.Text.Format.Heavy.Parse (FormatParseItem (..), parse, parseFormat)
import qualified Data.Text.Lazy as TL
import GHC.Generics (Generic)

import Langchain.Core.Error (LangchainError, validationError)

data TemplateFormat
  = FString
  deriving (Show, Eq, Generic, ToJSON, FromJSON)

renderTemplateWithFormat ::
  TemplateFormat -> Map.Map Text Text -> Text -> Either LangchainError Text
renderTemplateWithFormat FString = renderFStringTemplate

renderFStringTemplate :: Map.Map Text Text -> Text -> Either LangchainError Text
renderFStringTemplate vars source = do
  items <- parseFStringTemplate source
  traverse_ validateFStringItem items
  format <- mapParseError $ parseFormat (TL.fromStrict source)
  mapFormatError $ TL.toStrict <$> formatEither format (toFStringVars vars)
  where
    mapFormatError :: Either String a -> Either LangchainError a
    mapFormatError (Left err) = Left $ validationError (T.pack err) (Just "PromptTemplate") Nothing
    mapFormatError (Right result) = Right result

toFStringVars :: Map.Map Text Text -> Map.Map TL.Text Text
toFStringVars = Map.mapKeys TL.fromStrict

parseFStringTemplate :: Text -> Either LangchainError [FormatParseItem]
parseFStringTemplate source = mapParseError $ parse (TL.fromStrict source)

mapParseError :: (Show err) => Either err a -> Either LangchainError a
mapParseError (Left err) = Left $ validationError (T.pack $ show err) (Just "PromptTemplate") Nothing
mapParseError (Right result) = Right result

validateFStringItem :: FormatParseItem -> Either LangchainError ()
validateFStringItem (FormatString _) = Right ()
validateFStringItem (FormatReplacementField variableName formatSpec) = do
  validateFStringVariableName variableName
  traverse_ validateFStringFormatSpec formatSpec

validateFStringVariableName :: TL.Text -> Either LangchainError ()
validateFStringVariableName variableName
  | TL.all isDigit variableName =
      Left $
        validationError "Positional arguments are not supported" (Just $ TL.toStrict variableName) Nothing
  | TL.any (== '.') variableName =
      Left $ validationError "Attribute access is not supported" (Just $ TL.toStrict variableName) Nothing
  | TL.any (`elem` ['[', ']']) variableName =
      Left $ validationError "Index access is not supported" (Just $ TL.toStrict variableName) Nothing
  | otherwise = Right ()

validateFStringFormatSpec :: TL.Text -> Either LangchainError ()
validateFStringFormatSpec formatSpec
  | TL.any (`elem` ['{', '}']) formatSpec =
      Left $ validationError "Nested replacement fields are not allowed" (Just "PromptTemplate") Nothing
  | otherwise = Right ()

extractTemplateVariables :: Text -> [Text]
extractTemplateVariables = extractTemplateVariablesWithFormat FString

extractTemplateVariablesWithFormat :: TemplateFormat -> Text -> [Text]
extractTemplateVariablesWithFormat FString source =
  case parseFStringTemplate source of
    Left _ -> []
    Right parts -> unique [TL.toStrict variableName | FormatReplacementField variableName _ <- parts]

unique :: [Text] -> [Text]
unique = foldl addIfMissing []

addIfMissing :: [Text] -> Text -> [Text]
addIfMissing variableNames variableName
  | variableName `elem` variableNames = variableNames
  | otherwise = variableNames <> [variableName]