openai-2.3.0: examples/responses-tool-call-example/Main.hs
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Data.Aeson (FromJSON (..), (.:), (.=), withObject)
import Data.Foldable (toList)
import Data.Text (Text)
import OpenAI.V1 (Methods (..))
import OpenAI.V1.Responses (Tool(..))
import System.Environment (getEnv)
import qualified Data.Aeson as Aeson
import qualified Data.ByteString.Lazy as LBS
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text.Encoding
import qualified Data.Text.IO as TextIO
import qualified Data.Vector as Vector
import qualified OpenAI.V1 as V1
import qualified OpenAI.V1.Responses as Responses
-- | Simple JSON payload for parsing function arguments
newtype HoroscopeArgs = HoroscopeArgs { sign :: Text }
instance FromJSON HoroscopeArgs where
parseJSON = withObject "HoroscopeArgs" $ \obj -> do
sign <- obj .: "sign"
pure HoroscopeArgs{ sign }
getHoroscope :: Text -> Text
getHoroscope sign = sign <> ": Next Tuesday you will befriend a baby otter."
horoscopeTool :: Tool
horoscopeTool =
Tool_Function
{ description = Just "Get today's horoscope for an astrological sign."
, name = "get_horoscope"
, parameters =
Just . Aeson.object $
[ "type" .= ("object" :: Text)
, "properties"
.= Aeson.object
[ "sign"
.= Aeson.object
[ "type" .= ("string" :: Text)
, "description" .= ("An astrological sign like Taurus or Aquarius" :: Text)
]
]
, "required" .= (["sign"] :: [Text])
, "additionalProperties" .= False
]
, strict = Just True
}
main :: IO ()
main = do
key <- Text.pack <$> getEnv "OPENAI_KEY"
env <- V1.getClientEnv "https://api.openai.com"
let Methods{ createResponse } = V1.makeMethods env key Nothing Nothing
userMessage =
Responses.Item_Input_Message
{ Responses.role = Responses.User
, Responses.content = Vector.singleton Responses.Input_Text{ Responses.text = "What is my horoscope? I am an Aquarius." }
, Responses.status = Nothing
}
initialItems = [userMessage]
initialInput = Responses.Input (Vector.fromList initialItems)
firstRequest =
Responses._CreateResponse
{ Responses.model = "gpt-5"
, Responses.input = Just initialInput
, Responses.tools = Just [horoscopeTool]
}
firstResponse <- createResponse firstRequest
let Responses.ResponseObject{ Responses.output = outputItems } = firstResponse
outputList = toList outputItems
hasFunctionCall = Prelude.any isFunctionCall outputList
if not hasFunctionCall
then mapM_ TextIO.putStrLn (collectText firstResponse)
else do
additionalInputs <- gatherInputs outputList
let finalItems = initialItems <> additionalInputs
finalInput = Responses.Input (Vector.fromList finalItems)
secondRequest =
Responses._CreateResponse
{ Responses.model = "gpt-5"
, Responses.instructions = Just "Respond only with a horoscope generated by a tool."
, Responses.input = Just finalInput
, Responses.tools = Just [horoscopeTool]
}
finalResponse <- createResponse secondRequest
mapM_ TextIO.putStrLn (collectText finalResponse)
isFunctionCall :: Responses.OutputItem -> Bool
isFunctionCall Responses.Item_FunctionToolCall{} = True
isFunctionCall _ = False
gatherInputs :: [Responses.OutputItem] -> IO [Responses.InputItem]
gatherInputs [] = pure []
gatherInputs (item : rest) =
case item of
Responses.Item_Reasoning{ Responses.reasoning_id } -> do
restInputs <- gatherInputs rest
let reference = Responses.Item_Input_Item_Reference{ Responses.id = Just reasoning_id }
pure (reference : restInputs)
call@Responses.Item_FunctionToolCall{} -> do
(callInput, callOutput) <- processFunctionCall call
restInputs <- gatherInputs rest
pure (callInput : callOutput : restInputs)
_ -> gatherInputs rest
processFunctionCall
:: Responses.OutputItem
-> IO (Responses.InputItem, Responses.InputItem)
processFunctionCall
Responses.Item_FunctionToolCall
{ Responses.function_id = functionId
, Responses.function_call_id = callId
, Responses.function_name = functionName
, Responses.function_arguments = argumentsText
, Responses.function_status = statusText
} = do
let callInput =
Responses.Item_Input_Function_Call
{ Responses.id = functionId
, Responses.call_id = callId
, Responses.name = functionName
, Responses.arguments = argumentsText
, Responses.status = statusText
}
argumentsBytes = Text.Encoding.encodeUtf8 argumentsText
makeOutput status payload =
Responses.Item_Input_Function_Call_Output
{ Responses.id = Nothing
, Responses.call_id = callId
, Responses.output = encodePayload payload
, Responses.status = Just status
}
encodePayload = Text.Encoding.decodeUtf8 . LBS.toStrict . Aeson.encode
result <- case Aeson.eitherDecodeStrict' argumentsBytes of
Left err ->
pure $ makeOutput Responses.statusIncomplete (Aeson.object ["error" .= Text.pack err])
Right HoroscopeArgs{ sign } -> do
let horoscope = getHoroscope sign
pure $ makeOutput Responses.statusCompleted (Aeson.object ["horoscope" .= horoscope])
pure (callInput, result)
processFunctionCall other =
error $ "Unexpected output item: " <> show other
collectText :: Responses.ResponseObject -> [Text]
collectText Responses.ResponseObject{ Responses.output } =
[ text
| Responses.Item_OutputMessage{ Responses.message_content } <- toList output
, Responses.Output_Text{ Responses.text } <- toList message_content
]