packages feed

claude-1.2.0: examples/claude-programmatic-tool-calling-example/Main.hs

{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns        #-}
{-# LANGUAGE OverloadedLists       #-}
{-# LANGUAGE OverloadedStrings     #-}

-- | Example demonstrating Programmatic Tool Calling (PTC) with Claude
--
-- This example shows how to use code execution to have Claude programmatically
-- call tools. Instead of Claude making individual tool calls that you handle
-- one by one, Claude writes and executes code that calls multiple tools and
-- aggregates the results.
--
-- Requirements:
-- * anthropic-beta: advanced-tool-use-2025-11-20 header
-- * code_execution_20250825 tool in the tools array
-- * Tools must have allowed_callers = ["code_execution_20250825"]
module Main where

import Data.Foldable (toList)
import Data.Maybe (mapMaybe)
import Data.Text (Text)
import Prelude hiding (id)

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.Types as Aeson.Types
import qualified Data.Text as Text
import qualified Data.Text.IO as Text.IO
import qualified Data.Vector as Vector
import qualified System.Environment as Environment

-- | Define a query_database tool that Claude can call programmatically
queryDatabaseTool :: Tool.Tool
queryDatabaseTool = Tool.functionTool
    "query_database"
    (Just "Query a regional database for sales data. Returns sales figures for the specified region.")
    $ Aeson.object
        [ "properties" Aeson..= Aeson.object
            [ "region" Aeson..= Aeson.object
                [ "type" Aeson..= ("string" :: Text)
                , "description" Aeson..= ("The region to query: west, east, or central" :: Text)
                , "enum" Aeson..= (["west", "east", "central"] :: [Text])
                ]
            ]
        , "required" Aeson..= (["region"] :: [Text])
        ]

-- | Fake database query - returns sales data for a region
queryDatabase :: Text -> Text
queryDatabase region = case region of
    "west"    -> "{\"region\": \"west\", \"sales\": 125000, \"units\": 450}"
    "east"    -> "{\"region\": \"east\", \"sales\": 198000, \"units\": 720}"
    "central" -> "{\"region\": \"central\", \"sales\": 87000, \"units\": 310}"
    _         -> "{\"error\": \"Unknown region\"}"

main :: IO ()
main = do
    key <- Text.pack <$> Environment.getEnv "ANTHROPIC_KEY"
    env <- V1.getClientEnv "https://api.anthropic.com"

    -- Create methods with beta header for PTC
    let options = V1.defaultClientOptions
            { V1.apiKey = key
            , V1.anthropicBeta = Just "advanced-tool-use-2025-11-20"
            }
    let V1.Methods{ V1.createMessage } = V1.makeMethodsWith env options

    -- Tools: code execution + query_database with allowed_callers
    let tools =
            [ Tool.codeExecutionTool
            , Tool.allowCallers Tool.allowedCallersCodeExecution (Tool.inlineTool queryDatabaseTool)
            ]

    -- Initial message asking for aggregated data from multiple regions
    let initialMessage = Messages.Message
            { Messages.role = Messages.User
            , Messages.content =
                [ Messages.textContent
                    "Query the database for all three regions (west, east, central) and calculate the total sales and units across all regions. Use code execution to make the queries and aggregate the results."
                ]
            , Messages.cache_control = Nothing
            }

    Text.IO.putStrLn "=== Programmatic Tool Calling Example ==="
    Text.IO.putStrLn "Sending request with code execution tool..."
    Text.IO.putStrLn ""

    -- Loop until we get a final response
    loop createMessage tools [initialMessage] Nothing 0

  where
    maxIterations :: Int
    maxIterations = 10

    loop _ _ _ _ iteration | iteration >= maxIterations = do
        Text.IO.putStrLn $ "Max iterations (" <> Text.pack (show maxIterations) <> ") reached"

    loop createMessage tools messages containerId iteration = do
        Text.IO.putStrLn $ "--- Turn " <> Text.pack (show (iteration + 1)) <> " ---"

        response <- createMessage Messages._CreateMessage
            { Messages.model = "claude-sonnet-4-5-20250929"
            , Messages.messages = Vector.fromList messages
            , Messages.max_tokens = 4096
            , Messages.tools = Just tools
            , Messages.container = containerId
            }

        let Messages.MessageResponse
                { Messages.stop_reason = stopReason
                , Messages.content = responseContent
                , Messages.container = containerInfo
                } = response

        -- Extract container ID for reuse
        let newContainerId = case containerInfo of
                Just Messages.ContainerInfo{ Messages.id = cid } -> Just cid
                Nothing -> containerId

        Text.IO.putStrLn $ "Stop reason: " <> Text.pack (show stopReason)
        when (containerId /= newContainerId) $
            Text.IO.putStrLn $ "Container ID: " <> maybe "none" (\x -> x) newContainerId

        -- Print response content
        mapM_ printContentBlock (toList responseContent)

        case stopReason of
            Just Messages.End_Turn -> do
                Text.IO.putStrLn ""
                Text.IO.putStrLn "=== Final Response ==="
                mapM_ printFinalContent (toList responseContent)

            Just Messages.Tool_Use -> do
                -- Find all tool_use blocks and process them
                let toolUseBlocks =
                        [ (toolId, toolName, toolInput, toolCaller)
                        | Messages.ContentBlock_Tool_Use
                            { Messages.id = toolId
                            , Messages.name = toolName
                            , Messages.input = toolInput
                            , Messages.caller = toolCaller
                            } <- toList responseContent
                        ]

                Text.IO.putStrLn ""
                Text.IO.putStrLn $ "Processing " <> Text.pack (show (length toolUseBlocks)) <> " tool call(s)..."

                -- Process each tool call
                toolResults <- mapM processToolCall toolUseBlocks

                -- Build assistant replay message
                let assistantContent = mapMaybe Messages.contentBlockToContent (toList responseContent)
                let assistantMessage = Messages.Message
                        { Messages.role = Messages.Assistant
                        , Messages.content = Vector.fromList assistantContent
                        , Messages.cache_control = Nothing
                        }

                -- Build user message with tool results only (required for PTC)
                let userMessage = Messages.Message
                        { Messages.role = Messages.User
                        , Messages.content = Vector.fromList toolResults
                        , Messages.cache_control = Nothing
                        }

                Text.IO.putStrLn ""
                loop createMessage tools
                    (messages <> [assistantMessage, userMessage])
                    newContainerId
                    (iteration + 1)

            _ -> do
                Text.IO.putStrLn $ "Unexpected stop reason: " <> Text.pack (show stopReason)

    when :: Bool -> IO () -> IO ()
    when True action = action
    when False _ = pure ()

-- | Process a single tool call
processToolCall :: (Text, Text, Aeson.Value, Maybe Messages.ToolCaller) -> IO Messages.Content
processToolCall (toolId, toolName, toolInput, caller) = do
    -- Show caller info
    case caller of
        Just Messages.ToolCaller_Direct ->
            Text.IO.putStrLn $ "  [Direct call] " <> toolName
        Just (Messages.ToolCaller_CodeExecution{ Messages.tool_id = tid }) ->
            Text.IO.putStrLn $ "  [Code execution call via " <> tid <> "] " <> toolName
        Just (Messages.ToolCaller_Unknown _) ->
            Text.IO.putStrLn $ "  [Unknown caller] " <> toolName
        Nothing ->
            Text.IO.putStrLn $ "  [No caller info] " <> toolName

    case toolName of
        "query_database" -> do
            let parseRegion = Aeson.withObject "input" (\o -> o Aeson..: "region")
            case Aeson.Types.parseMaybe parseRegion toolInput of
                Just region -> do
                    let result = queryDatabase region
                    Text.IO.putStrLn $ "    Region: " <> region <> " -> " <> result
                    pure Messages.Content_Tool_Result
                        { Messages.tool_use_id = toolId
                        , Messages.content = Just result
                        , Messages.is_error = Nothing
                        }
                Nothing -> do
                    Text.IO.putStrLn "    Error: missing region parameter"
                    pure Messages.Content_Tool_Result
                        { Messages.tool_use_id = toolId
                        , Messages.content = Just "Error: missing region parameter"
                        , Messages.is_error = Just True
                        }
        _ -> do
            Text.IO.putStrLn $ "    Unknown tool: " <> toolName
            pure Messages.Content_Tool_Result
                { Messages.tool_use_id = toolId
                , Messages.content = Just $ "Unknown tool: " <> toolName
                , Messages.is_error = Just True
                }

-- | Print a content block summary
printContentBlock :: Messages.ContentBlock -> IO ()
printContentBlock (Messages.ContentBlock_Text{ Messages.text = t }) =
    Text.IO.putStrLn $ "  [Text] " <> Text.take 80 t <> if Text.length t > 80 then "..." else ""
printContentBlock (Messages.ContentBlock_Tool_Use{ Messages.name = n, Messages.caller = c }) =
    Text.IO.putStrLn $ "  [Tool use] " <> n <> callerInfo c
  where
    callerInfo Nothing = ""
    callerInfo (Just Messages.ToolCaller_Direct) = " (direct)"
    callerInfo (Just Messages.ToolCaller_CodeExecution{}) = " (programmatic)"
    callerInfo (Just Messages.ToolCaller_Unknown{}) = " (unknown caller)"
printContentBlock (Messages.ContentBlock_Server_Tool_Use{ Messages.name = n }) =
    Text.IO.putStrLn $ "  [Server tool use] " <> n
printContentBlock (Messages.ContentBlock_Tool_Search_Tool_Result{ Messages.tool_use_id = tid }) =
    Text.IO.putStrLn $ "  [Tool search result] " <> tid
printContentBlock (Messages.ContentBlock_Code_Execution_Tool_Result{ Messages.tool_use_id = tid, Messages.code_execution_content = c }) = do
    Text.IO.putStrLn $ "  [Code execution result] " <> tid
    case c of
        Messages.CodeExecutionResultContent result -> do
            let Messages.CodeExecutionResult{ Messages.stdout = out, Messages.stderr = err, Messages.return_code = rc } = result
            Text.IO.putStrLn $ "    return_code: " <> Text.pack (show rc)
            unless (Text.null out) $
                Text.IO.putStrLn $ "    stdout: " <> Text.take 200 out <> if Text.length out > 200 then "..." else ""
            unless (Text.null err) $
                Text.IO.putStrLn $ "    stderr: " <> Text.take 200 err <> if Text.length err > 200 then "..." else ""
        Messages.CodeExecutionToolResultContent_Unknown _ ->
            Text.IO.putStrLn "    (unknown content type)"
  where
    unless False action = action
    unless True _ = pure ()
printContentBlock (Messages.ContentBlock_Thinking{}) =
    Text.IO.putStrLn "  [Thinking]"
printContentBlock (Messages.ContentBlock_Redacted_Thinking{}) =
    Text.IO.putStrLn "  [Redacted thinking]"
printContentBlock (Messages.ContentBlock_Unknown{ Messages.type_ = t }) =
    Text.IO.putStrLn $ "  [Unknown] " <> t

-- | Print final text content
printFinalContent :: Messages.ContentBlock -> IO ()
printFinalContent (Messages.ContentBlock_Text{ Messages.text = t }) = Text.IO.putStrLn t
printFinalContent _ = pure ()