claude-1.0.1: examples/claude-structured-outputs-example/Main.hs
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
-- | Example demonstrating structured outputs with Claude
--
-- This example shows how to use:
-- 1. JSON outputs (output_format) to get structured JSON responses
-- 2. Strict tool use (strict: true) to validate tool parameters
--
-- Requires the structured-outputs-2025-11-13 beta header.
module Main where
import Data.Aeson (FromJSON(..), (.:), (.=))
import Data.Foldable (toList)
import Data.Text (Text)
import qualified Claude.V1 as V1
import qualified Claude.V1.Messages as Messages
import qualified Claude.V1.Tool as Tool
import qualified Data.Aeson as Aeson
import qualified Data.Aeson.KeyMap as KeyMap
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 Text.IO
import qualified System.Environment as Environment
-- | Structured output type for contact extraction
data ContactInfo = ContactInfo
{ name :: Text
, email :: Text
, company :: Maybe Text
, interested_in :: Text
} deriving Show
instance FromJSON ContactInfo where
parseJSON = Aeson.withObject "ContactInfo" $ \obj -> do
n <- obj .: "name"
e <- obj .: "email"
c <- obj .: "company"
i <- obj .: "interested_in"
pure ContactInfo{ name = n, email = e, company = c, interested_in = i }
main :: IO ()
main = do
key <- Text.pack <$> Environment.getEnv "ANTHROPIC_KEY"
env <- V1.getClientEnv "https://api.anthropic.com"
-- Use makeMethodsWith to pass the structured outputs beta header
let options = V1.defaultClientOptions
{ V1.apiKey = key
, V1.anthropicBeta = Just "structured-outputs-2025-11-13"
}
let V1.Methods{ V1.createMessage } = V1.makeMethodsWith env options
-- Example 1: JSON outputs for data extraction
Text.IO.putStrLn "=== Example 1: JSON Outputs (Data Extraction) ===\n"
jsonOutputsExample createMessage
Text.IO.putStrLn "\n"
-- Example 2: Strict tool use for validated parameters
Text.IO.putStrLn "=== Example 2: Strict Tool Use ===\n"
strictToolUseExample createMessage
-- | Example using output_format to get structured JSON responses
jsonOutputsExample :: (Messages.CreateMessage -> IO Messages.MessageResponse) -> IO ()
jsonOutputsExample createMessage = do
let emailText = "Hi there! I'm John Smith from Acme Corp (john.smith@acme.com). \
\I saw your demo at the conference and I'm very interested in \
\your Enterprise plan. Could we schedule a call next week?"
-- Define the JSON schema for our expected output
let outputSchema = Aeson.object
[ "type" .= ("object" :: Text)
, "properties" .= Aeson.object
[ "name" .= Aeson.object
[ "type" .= ("string" :: Text)
, "description" .= ("Full name of the contact" :: Text)
]
, "email" .= Aeson.object
[ "type" .= ("string" :: Text)
, "description" .= ("Email address" :: Text)
]
, "company" .= Aeson.object
[ "type" .= ("string" :: Text)
, "description" .= ("Company name if mentioned" :: Text)
]
, "interested_in" .= Aeson.object
[ "type" .= ("string" :: Text)
, "description" .= ("What product/service they're interested in" :: Text)
]
]
, "required" .= (["name", "email", "interested_in"] :: [Text])
, "additionalProperties" .= False
]
let message = Messages.Message
{ Messages.role = Messages.User
, Messages.content = [Messages.textContent $ "Extract contact information from this email:\n\n" <> emailText]
, Messages.cache_control = Nothing
}
Text.IO.putStrLn $ "Input email: " <> emailText
Text.IO.putStrLn "\nExtracting structured data..."
response <- createMessage Messages._CreateMessage
{ Messages.model = "claude-sonnet-4-5-20250929"
, Messages.messages = [message]
, Messages.max_tokens = 1024
, Messages.output_format = Just (Messages.jsonSchemaFormat outputSchema)
}
let Messages.MessageResponse{ Messages.content = responseContent, Messages.stop_reason = stopReason } = response
Text.IO.putStrLn $ "Stop reason: " <> Text.pack (show stopReason)
-- The response text should be valid JSON matching our schema
case toList responseContent of
[Messages.ContentBlock_Text{ Messages.text = jsonText }] -> do
Text.IO.putStrLn $ "\nRaw JSON response:\n" <> jsonText
-- Parse and display the structured data
case Aeson.eitherDecodeStrict (Text.Encoding.encodeUtf8 jsonText) of
Left err -> Text.IO.putStrLn $ "Parse error: " <> Text.pack err
Right ContactInfo{ name, email, company, interested_in } -> do
Text.IO.putStrLn "\nParsed contact info:"
Text.IO.putStrLn $ " Name: " <> name
Text.IO.putStrLn $ " Email: " <> email
Text.IO.putStrLn $ " Company: " <> maybe "(not specified)" id company
Text.IO.putStrLn $ " Interested in: " <> interested_in
_ -> Text.IO.putStrLn "Unexpected response format"
-- | Example using strict: true for validated tool parameters
strictToolUseExample :: (Messages.CreateMessage -> IO Messages.MessageResponse) -> IO ()
strictToolUseExample createMessage = do
-- Define a tool with strict mode enabled
-- This guarantees the tool input will match the schema exactly
let flightSearchTool = Tool.strictFunctionTool
"search_flights"
(Just "Search for available flights between two cities")
$ Aeson.object
[ "type" .= ("object" :: Text)
, "properties" .= Aeson.object
[ "origin" .= Aeson.object
[ "type" .= ("string" :: Text)
, "description" .= ("Origin airport code (e.g., SFO)" :: Text)
]
, "destination" .= Aeson.object
[ "type" .= ("string" :: Text)
, "description" .= ("Destination airport code (e.g., JFK)" :: Text)
]
, "date" .= Aeson.object
[ "type" .= ("string" :: Text)
, "format" .= ("date" :: Text)
, "description" .= ("Travel date in YYYY-MM-DD format" :: Text)
]
, "passengers" .= Aeson.object
[ "type" .= ("integer" :: Text)
, "enum" .= ([1, 2, 3, 4, 5, 6, 7, 8, 9, 10] :: [Int])
, "description" .= ("Number of passengers (1-10)" :: Text)
]
]
, "required" .= (["origin", "destination", "date", "passengers"] :: [Text])
, "additionalProperties" .= False
]
let message = Messages.Message
{ Messages.role = Messages.User
, Messages.content = [Messages.textContent "Find me flights from San Francisco to New York for 2 people on January 15th, 2025"]
, Messages.cache_control = Nothing
}
Text.IO.putStrLn "Asking Claude to search for flights with strict tool validation..."
response <- createMessage Messages._CreateMessage
{ Messages.model = "claude-sonnet-4-5-20250929"
, Messages.messages = [message]
, Messages.max_tokens = 1024
, Messages.tools = Just [Tool.inlineTool flightSearchTool]
, Messages.tool_choice = Just Tool.toolChoiceAny -- Force tool use
}
let Messages.MessageResponse{ Messages.content = responseContent, Messages.stop_reason = stopReason } = response
Text.IO.putStrLn $ "Stop reason: " <> Text.pack (show stopReason)
-- With strict mode, we're guaranteed the tool input matches the schema
mapM_ processBlock (toList responseContent)
where
processBlock (Messages.ContentBlock_Tool_Use{ Messages.id = toolId, Messages.name = toolName, Messages.input = toolInput }) = do
Text.IO.putStrLn $ "\nTool called: " <> toolName
Text.IO.putStrLn $ "Tool ID: " <> toolId
Text.IO.putStrLn $ "Input (guaranteed to match schema):"
Text.IO.putStrLn $ " " <> Text.Encoding.decodeUtf8 (LBS.toStrict (Aeson.encode toolInput))
-- In strict mode, we can safely extract fields without error handling
case toolInput of
Aeson.Object obj -> do
let origin = lookupString "origin" obj
let destination = lookupString "destination" obj
let date = lookupString "date" obj
let passengers = lookupInt "passengers" obj
Text.IO.putStrLn "\nParsed parameters:"
Text.IO.putStrLn $ " Origin: " <> origin
Text.IO.putStrLn $ " Destination: " <> destination
Text.IO.putStrLn $ " Date: " <> date
Text.IO.putStrLn $ " Passengers: " <> Text.pack (show passengers)
_ -> pure ()
processBlock (Messages.ContentBlock_Text{ Messages.text = t }) =
Text.IO.putStrLn $ "Text: " <> t
processBlock _ = pure ()
lookupString key obj = case KeyMap.lookup key obj of
Just (Aeson.String s) -> s
_ -> "(missing)"
lookupInt key obj = case KeyMap.lookup key obj of
Just (Aeson.Number n) -> round n :: Int
_ -> 0