never executed always true always false
    1 module PureClaw.Tools.Image
    2   ( -- * Tool registration
    3     imageTool
    4     -- * Helpers (exported for testing)
    5   , detectMediaType
    6   , maxImageSize
    7   ) where
    8 
    9 import Control.Exception
   10 import Data.Aeson
   11 import Data.Aeson.Types
   12 import Data.ByteString qualified as BS
   13 import Data.ByteString.Base64 qualified as B64
   14 import Data.Text (Text)
   15 import Data.Char qualified
   16 import Data.Text qualified as T
   17 import System.FilePath
   18 
   19 import PureClaw.Core.Types
   20 import PureClaw.Handles.File
   21 import PureClaw.Providers.Class
   22 import PureClaw.Security.Path
   23 
   24 -- | Maximum image size in bytes (20 MB).
   25 maxImageSize :: Int
   26 maxImageSize = 20 * 1024 * 1024
   27 
   28 -- | Create an image tool for vision model integration.
   29 -- Reads an image file, base64-encodes it, and returns it as rich
   30 -- content that the provider can process visually.
   31 imageTool :: WorkspaceRoot -> FileHandle -> (ToolDefinition, Value -> IO ([ToolResultPart], Bool))
   32 imageTool root fh = (def, handler)
   33   where
   34     def = ToolDefinition
   35       { _td_name        = "image"
   36       , _td_description = "Read an image file for visual analysis. Supports PNG, JPEG, GIF, and WebP."
   37       , _td_inputSchema = object
   38           [ "type" .= ("object" :: Text)
   39           , "properties" .= object
   40               [ "path" .= object
   41                   [ "type" .= ("string" :: Text)
   42                   , "description" .= ("The image file path relative to the workspace root" :: Text)
   43                   ]
   44               ]
   45           , "required" .= (["path"] :: [Text])
   46           ]
   47       }
   48 
   49     handler input =
   50       case parseEither parseInput input of
   51         Left err -> pure ([TRPText (T.pack err)], True)
   52         Right path -> do
   53           pathResult <- mkSafePath root (T.unpack path)
   54           case pathResult of
   55             Left pe -> pure ([TRPText (T.pack (show pe))], True)
   56             Right sp -> do
   57               let ext = map toLowerChar (takeExtension (getSafePath sp))
   58               case detectMediaType ext of
   59                 Nothing -> pure ([TRPText ("Unsupported image format: " <> T.pack ext)], True)
   60                 Just mediaType -> do
   61                   result <- try @SomeException (_fh_readFile fh sp)
   62                   case result of
   63                     Left e -> pure ([TRPText (T.pack (show e))], True)
   64                     Right bs
   65                       | BS.length bs > maxImageSize ->
   66                           pure ([TRPText ("Image too large: " <> T.pack (show (BS.length bs)) <> " bytes (max " <> T.pack (show maxImageSize) <> ")")], True)
   67                       | otherwise ->
   68                           let b64 = B64.encode bs
   69                           in pure ([TRPImage mediaType b64, TRPText ("Image: " <> path <> " (" <> mediaType <> ", " <> T.pack (show (BS.length bs)) <> " bytes)")], False)
   70 
   71     parseInput :: Value -> Parser Text
   72     parseInput = withObject "ImageInput" $ \o -> o .: "path"
   73 
   74 -- | Detect media type from file extension.
   75 detectMediaType :: String -> Maybe Text
   76 detectMediaType ".png"  = Just "image/png"
   77 detectMediaType ".jpg"  = Just "image/jpeg"
   78 detectMediaType ".jpeg" = Just "image/jpeg"
   79 detectMediaType ".gif"  = Just "image/gif"
   80 detectMediaType ".webp" = Just "image/webp"
   81 detectMediaType _       = Nothing
   82 
   83 -- | Lowercase a character (ASCII only).
   84 toLowerChar :: Char -> Char
   85 toLowerChar c
   86   | Data.Char.isAsciiUpper c = toEnum (fromEnum c + 32)
   87   | otherwise = c