packages feed

claude-1.3.1: src/Claude/V1/Tool.hs

-- | Tool types for Claude API
--
-- This module provides types and utilities for defining tools that Claude can use.
--
-- Example usage:
--
-- @
-- import Claude.V1.Tool
--
-- -- Define a simple tool
-- weatherTool :: Tool
-- weatherTool = functionTool \"get_weather\"
--     (Just \"Get the current weather for a location\")
--     (Aeson.object
--         [ \"type\" .= (\"object\" :: Text)
--         , \"properties\" .= Aeson.object
--             [ \"location\" .= Aeson.object
--                 [ \"type\" .= (\"string\" :: Text)
--                 , \"description\" .= (\"City and state, e.g. San Francisco, CA\" :: Text)
--                 ]
--             ]
--         , \"required\" .= ([\"location\"] :: [Text])
--         ])
-- @
module Claude.V1.Tool
    ( -- * Types
      Tool(..)
    , ToolChoice(..)
    , InputSchema(..)
      -- * Tool definition (heterogeneous tools array)
    , ToolDefinition(..)
    , ToolSearchTool(..)
    , ToolSearchToolType(..)
      -- * Tool constructors
    , functionTool
    , strictFunctionTool
    , simpleInputSchema
      -- * ToolDefinition constructors
    , inlineTool
    , deferredTool
    , toolSearchRegex
    , toolSearchBm25
      -- * Code execution tool (PTC)
    , codeExecutionTool
    , allowedCallersCodeExecution
    , allowCallers
      -- * ToolChoice constructors
    , toolChoiceAuto
    , toolChoiceAny
    , toolChoiceTool
      -- * Helpers for processing tool calls
    , isToolUse
    , getToolUseBlocks
    , makeToolResult
    , makeToolResultError
    ) where

import Claude.Prelude

import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Key as Key
import qualified Data.Aeson.KeyMap as KeyMap
import qualified Data.Vector as Vector

-- | Tool input schema (JSON Schema)
--
-- The schema follows JSON Schema format. At minimum, specify @type_@ as \"object\".
-- For strict mode (structured outputs), set @additionalProperties = Just False@.
data InputSchema = InputSchema
    { type_ :: Text
    , properties :: Maybe Value
    , required :: Maybe (Vector Text)
    , additionalProperties :: Maybe Bool
    -- ^ For strict mode, must be @Just False@
    } deriving stock (Eq, Generic, Show)

instance FromJSON InputSchema where
    parseJSON = Aeson.withObject "InputSchema" $ \o -> do
        type_ <- o Aeson..: "type"
        properties <- o Aeson..:? "properties"
        required <- o Aeson..:? "required"
        additionalProperties <- o Aeson..:? "additionalProperties"
        pure InputSchema{ type_, properties, required, additionalProperties }

instance ToJSON InputSchema where
    toJSON InputSchema{ type_, properties, required, additionalProperties } =
        Aeson.object $ filter ((/= Aeson.Null) . snd)
            [ "type" Aeson..= type_
            , "properties" Aeson..= properties
            , "required" Aeson..= required
            , "additionalProperties" Aeson..= additionalProperties
            ]

-- | Create a simple input schema with properties and required fields
simpleInputSchema
    :: Value           -- ^ Properties object
    -> Vector Text     -- ^ Required field names
    -> InputSchema
simpleInputSchema props reqs = InputSchema
    { type_ = "object"
    , properties = Just props
    , required = Just reqs
    , additionalProperties = Nothing
    }

-- | A tool that can be used by Claude
--
-- Tools allow Claude to call external functions. When Claude decides to use a tool,
-- it will return a @tool_use@ content block with the tool name and input arguments.
--
-- Set @strict = Just True@ to enable strict schema validation (structured outputs).
data Tool = Tool
    { name :: Text
    , description :: Maybe Text
    , input_schema :: InputSchema
    , strict :: Maybe Bool
    -- ^ Enable strict schema validation for tool inputs
    } deriving stock (Eq, Generic, Show)

instance FromJSON Tool where
    parseJSON = genericParseJSON aesonOptions

instance ToJSON Tool where
    toJSON = genericToJSON aesonOptions

-- | Create a function tool with a name, description, and JSON schema for parameters
--
-- This is the primary way to define tools for Claude.
-- To enable strict schema validation, use 'strictFunctionTool' instead.
functionTool
    :: Text           -- ^ Tool name (must match [a-zA-Z0-9_-]+)
    -> Maybe Text     -- ^ Description of what the tool does
    -> Value          -- ^ JSON Schema for the input parameters
    -> Tool
functionTool toolName toolDescription schema = Tool
    { name = toolName
    , description = toolDescription
    , input_schema = InputSchema
        { type_ = "object"
        , properties = case schema of
            Aeson.Object o -> case lookupKey "properties" o of
                Just props -> Just props
                Nothing -> Just schema
            _ -> Just schema
        , required = case schema of
            Aeson.Object o -> case lookupKey "required" o of
                Just (Aeson.Array arr) -> Just (Vector.mapMaybe getString arr)
                _ -> Nothing
            _ -> Nothing
        , additionalProperties = Nothing
        }
    , strict = Nothing
    }
  where
    getString (Aeson.String s) = Just s
    getString _ = Nothing
    lookupKey k obj = KeyMap.lookup (Key.fromText k) obj

-- | Create a function tool with strict schema validation enabled
--
-- This is like 'functionTool' but enables strict mode for structured outputs.
-- Automatically sets @additionalProperties = False@ as required by the API.
strictFunctionTool
    :: Text           -- ^ Tool name (must match [a-zA-Z0-9_-]+)
    -> Maybe Text     -- ^ Description of what the tool does
    -> Value          -- ^ JSON Schema for the input parameters
    -> Tool
strictFunctionTool toolName toolDescription schema =
    let tool = functionTool toolName toolDescription schema
        inputSchema = input_schema tool
    in tool
        { strict = Just True
        , input_schema = inputSchema{ additionalProperties = Just False }
        }

-- | Controls which tool the model should use
data ToolChoice
    = ToolChoice_Auto
    -- ^ Let Claude decide whether to use tools
    | ToolChoice_Any
    -- ^ Force Claude to use one of the provided tools
    | ToolChoice_Tool { name :: Text }
    -- ^ Force Claude to use a specific tool
    deriving stock (Generic, Show)

instance FromJSON ToolChoice where
    parseJSON (Aeson.Object o) = do
        t <- o Aeson..: "type"
        case (t :: Text) of
            "auto" -> pure ToolChoice_Auto
            "any" -> pure ToolChoice_Any
            "tool" -> ToolChoice_Tool <$> o Aeson..: "name"
            _ -> fail "Unknown tool choice type"
    parseJSON _ = fail "Invalid tool choice"

instance ToJSON ToolChoice where
    toJSON ToolChoice_Auto = Aeson.object ["type" Aeson..= ("auto" :: Text)]
    toJSON ToolChoice_Any = Aeson.object ["type" Aeson..= ("any" :: Text)]
    toJSON (ToolChoice_Tool n) = Aeson.object
        [ "type" Aeson..= ("tool" :: Text)
        , "name" Aeson..= n
        ]

-- | Convenience: auto tool choice (let Claude decide)
toolChoiceAuto :: ToolChoice
toolChoiceAuto = ToolChoice_Auto

-- | Convenience: any tool choice (force tool use)
toolChoiceAny :: ToolChoice
toolChoiceAny = ToolChoice_Any

-- | Convenience: specific tool choice
toolChoiceTool :: Text -> ToolChoice
toolChoiceTool = ToolChoice_Tool

-- | Tool search tool type variants (for server-side tool search)
data ToolSearchToolType
    = ToolSearchTool_Regex_20251119
    | ToolSearchTool_Bm25_20251119
    deriving stock (Eq, Show)

instance FromJSON ToolSearchToolType where
    parseJSON = Aeson.withText "ToolSearchToolType" $ \t -> case t of
        "tool_search_tool_regex_20251119" -> pure ToolSearchTool_Regex_20251119
        "tool_search_tool_bm25_20251119" -> pure ToolSearchTool_Bm25_20251119
        _ -> fail $ "Unknown tool search tool type: " <> show t

instance ToJSON ToolSearchToolType where
    toJSON ToolSearchTool_Regex_20251119 = Aeson.String "tool_search_tool_regex_20251119"
    toJSON ToolSearchTool_Bm25_20251119 = Aeson.String "tool_search_tool_bm25_20251119"

-- | Tool search tool configuration
--
-- Used to enable server-side tool search, which allows Claude to efficiently
-- search through large numbers of tools using regex or BM25 matching.
data ToolSearchTool = ToolSearchTool
    { name :: Text
    , type_ :: ToolSearchToolType
    } deriving stock (Eq, Show)

instance FromJSON ToolSearchTool where
    parseJSON = Aeson.withObject "ToolSearchTool" $ \o -> do
        name <- o Aeson..: "name"
        type_ <- o Aeson..: "type"
        pure ToolSearchTool{ name, type_ }

instance ToJSON ToolSearchTool where
    toJSON ToolSearchTool{ name, type_ } = Aeson.object
        [ "name" Aeson..= name
        , "type" Aeson..= type_
        ]

-- | A tool definition for the @tools@ array
--
-- The @tools@ array in Claude API requests is heterogeneous:
--
-- * Function tools: regular tools with name, description, and input schema
-- * Tool search tools: server-side tool search configuration
-- * Code execution tool: for programmatic tool calling (PTC)
--
-- Use 'inlineTool' or 'deferredTool' to wrap a 'Tool', or 'toolSearchRegex'/'toolSearchBm25'
-- to add tool search capability. Use 'codeExecutionTool' for PTC.
data ToolDefinition
    = ToolDef_Function
        { tool :: Tool
        , defer_loading :: Maybe Bool
        , allowed_callers :: Maybe (Vector Text)
        }
    | ToolDef_SearchTool ToolSearchTool
    | ToolDef_CodeExecutionTool
        { name :: Text
        , type_ :: Text
        }
    deriving stock (Eq, Show)

instance FromJSON ToolDefinition where
    parseJSON = Aeson.withObject "ToolDefinition" $ \o -> do
        -- Check if this is a tool search tool or code execution tool by looking for "type" field
        mType <- o Aeson..:? "type"
        case mType of
            Just t | isToolSearchType t -> do
                searchTool <- Aeson.parseJSON (Aeson.Object o)
                pure (ToolDef_SearchTool searchTool)
            Just t | isCodeExecutionType t -> do
                name <- o Aeson..: "name"
                pure ToolDef_CodeExecutionTool{ name, type_ = t }
            _ -> do
                -- Parse as function tool
                tool <- Aeson.parseJSON (Aeson.Object o)
                defer_loading <- o Aeson..:? "defer_loading"
                allowed_callers <- o Aeson..:? "allowed_callers"
                pure ToolDef_Function{ tool, defer_loading, allowed_callers }
      where
        isToolSearchType :: Text -> Bool
        isToolSearchType t = t == "tool_search_tool_regex_20251119"
                          || t == "tool_search_tool_bm25_20251119"
        isCodeExecutionType :: Text -> Bool
        isCodeExecutionType t = t == "code_execution_20250825"

instance ToJSON ToolDefinition where
    toJSON (ToolDef_Function Tool{ name, description, input_schema, strict } defer_loading allowed_callers) =
        Aeson.Object (baseMap <> optionalFields)
      where
        baseObj = Aeson.object $
            [ "name" Aeson..= name
            , "input_schema" Aeson..= input_schema
            ] <> maybe [] (\d -> ["description" Aeson..= d]) description
        baseMap = case baseObj of
            Aeson.Object m -> m
            _ -> KeyMap.empty
        optionalFields = KeyMap.fromList $
            maybe [] (\dl -> [("defer_loading", Aeson.toJSON dl)]) defer_loading <>
            maybe [] (\ac -> [("allowed_callers", Aeson.toJSON ac)]) allowed_callers <>
            maybe [] (\s -> [("strict", Aeson.toJSON s)]) strict
    toJSON (ToolDef_SearchTool searchTool) = Aeson.toJSON searchTool
    toJSON (ToolDef_CodeExecutionTool name type_) = Aeson.object
        [ "name" Aeson..= name
        , "type" Aeson..= type_
        ]

-- | Wrap a tool for inline (non-deferred) loading
inlineTool :: Tool -> ToolDefinition
inlineTool t = ToolDef_Function{ tool = t, defer_loading = Nothing, allowed_callers = Nothing }

-- | Wrap a tool for deferred loading (used with tool search)
deferredTool :: Tool -> ToolDefinition
deferredTool t = ToolDef_Function{ tool = t, defer_loading = Just True, allowed_callers = Nothing }

-- | Code execution tool for programmatic tool calling (PTC)
--
-- When included in the tools array, Claude can write and execute code
-- to call other tools programmatically.
codeExecutionTool :: ToolDefinition
codeExecutionTool = ToolDef_CodeExecutionTool
    { name = "code_execution"
    , type_ = "code_execution_20250825"
    }

-- | Allowed callers for code execution (PTC)
--
-- Use with 'allowCallers' to mark a function tool as callable by code execution.
allowedCallersCodeExecution :: Vector Text
allowedCallersCodeExecution = ["code_execution_20250825"]

-- | Set allowed_callers on a function tool definition
--
-- Only affects 'ToolDef_Function'; other tool types are returned unchanged.
--
-- Example:
--
-- @
-- allowCallers allowedCallersCodeExecution (inlineTool myTool)
-- @
allowCallers :: Vector Text -> ToolDefinition -> ToolDefinition
allowCallers callers (ToolDef_Function t dl _) = ToolDef_Function t dl (Just callers)
allowCallers _ td = td

-- | Tool search using regex matching
toolSearchRegex :: ToolDefinition
toolSearchRegex = ToolDef_SearchTool ToolSearchTool
    { name = "tool_search_tool_regex"
    , type_ = ToolSearchTool_Regex_20251119
    }

-- | Tool search using BM25 matching
toolSearchBm25 :: ToolDefinition
toolSearchBm25 = ToolDef_SearchTool ToolSearchTool
    { name = "tool_search_tool_bm25"
    , type_ = ToolSearchTool_Bm25_20251119
    }

-- | Content block types (duplicated here for helper functions)
-- These mirror the types in Messages but are needed for the helper functions.

-- | Check if a content block is a tool use block
isToolUse :: Value -> Bool
isToolUse (Aeson.Object o) = case KeyMap.lookup (Key.fromText "type") o of
    Just (Aeson.String "tool_use") -> True
    _ -> False
isToolUse _ = False

-- | Extract tool use blocks from a response's content array
--
-- Returns a list of (id, name, input) tuples for each tool_use block
getToolUseBlocks :: Vector Value -> [(Text, Text, Value)]
getToolUseBlocks content = Vector.toList $ Vector.mapMaybe extractToolUse content
  where
    extractToolUse (Aeson.Object o) = do
        Aeson.String "tool_use" <- KeyMap.lookup (Key.fromText "type") o
        Aeson.String toolId <- KeyMap.lookup (Key.fromText "id") o
        Aeson.String toolName <- KeyMap.lookup (Key.fromText "name") o
        toolInput <- KeyMap.lookup (Key.fromText "input") o
        pure (toolId, toolName, toolInput)
    extractToolUse _ = Nothing

-- | Create a tool result content block for a successful tool call
makeToolResult
    :: Text    -- ^ tool_use_id from the tool_use block
    -> Text    -- ^ Result content (typically JSON encoded)
    -> Value
makeToolResult toolUseId resultContent = Aeson.object
    [ "type" Aeson..= ("tool_result" :: Text)
    , "tool_use_id" Aeson..= toolUseId
    , "content" Aeson..= resultContent
    ]

-- | Create a tool result content block for a failed tool call
makeToolResultError
    :: Text    -- ^ tool_use_id from the tool_use block
    -> Text    -- ^ Error message
    -> Value
makeToolResultError toolUseId errorMsg = Aeson.object
    [ "type" Aeson..= ("tool_result" :: Text)
    , "tool_use_id" Aeson..= toolUseId
    , "content" Aeson..= errorMsg
    , "is_error" Aeson..= True
    ]