packages feed

langchain-hs-0.0.5.0: src/Langchain/Tool/Shell.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}

{- |
Module      : Langchain.Tool.Shell
Description : Shell command execution tool
Copyright   : (c) 2025-2026 Tushar Adhatrao
License     : MIT
Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>
Stability   : experimental

Provides shell command execution capabilities for agents via System.Process.
-}
module Langchain.Tool.Shell
  ( shellTool
  ) where

import Control.Exception (SomeException, try)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Aeson (Value (..), object, (.=))
import Data.Aeson.Types (parseEither, (.:?))
import Data.Text (Text)
import qualified Data.Text as T
import System.Exit (ExitCode (..))
import System.Process (readProcessWithExitCode)

import Langchain.Core.Error (toolError)
import Langchain.Core.Tool (Tool (..), createTool)

-- | Tool that executes a shell command via @sh -c@ and returns its output
shellTool :: MonadIO m => Tool m
shellTool =
  createTool
    "shell_command"
    "Execute a shell command line (e.g. bash/sh) and return stdout and stderr output."
    ( object
        [ "type" .= ("object" :: Text)
        , "properties"
            .= object
              [ "command"
                  .= object
                    [ "type" .= ("string" :: Text)
                    , "description" .= ("The shell command line to execute" :: Text)
                    ]
              ]
        , "required" .= (["command"] :: [Text])
        ]
    )
    ( \case
        Object o -> case parseEither (.:? "command") o of
          Right (Just cmd) -> do
            eRes <- liftIO $ try (readProcessWithExitCode "sh" ["-c", T.unpack cmd] "")
            case eRes of
              Left err ->
                pure $ Left $ toolError (T.pack $ show (err :: SomeException)) (Just "shell_command") Nothing
              Right (ExitSuccess, stdoutStr, stderrStr) ->
                let out = T.strip (T.pack stdoutStr)
                    err = T.strip (T.pack stderrStr)
                 in if T.null out
                      then if T.null err then pure $ Right "Command completed with no output." else pure $ Right err
                      else pure $ Right out
              Right (ExitFailure code, stdoutStr, stderrStr) ->
                let combined = T.strip (T.pack (stdoutStr <> "\n" <> stderrStr))
                 in pure $
                      Right $
                        "Command exited with code "
                          <> T.pack (show code)
                          <> (if T.null combined then "" else ": " <> combined)
          _ -> pure $ Left $ toolError "Missing 'command' parameter" (Just "shell_command") Nothing
        _ -> pure $ Left $ toolError "Invalid arguments object" (Just "shell_command") Nothing
    )