packages feed

langchain-hs-0.0.3.0: src/Langchain/Error.hs

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

{- |
Module      : Langchain.Error
Description : Central error handling for langchain-hs
Copyright   : (c) 2025 Tushar Adhatrao
License     : MIT
Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>
Stability   : experimental

This module provides a comprehensive error handling system for langchain-hs,
replacing the previous `Either String` pattern with a structured, type-safe
approach that follows industry best practices.

The error system includes:

* Structured error types with context and metadata
* Error severity levels and categories
* Utility functions for error construction and handling
* Integration with existing langchain-hs components
* Support for error chaining and context preservation

Example usage:

@
import Langchain.Error

-- Creating errors
let err = llmError "Model timeout" (Just "gpt-4") Nothing

-- Error handling with context
result <- someOperation
case result of
  Left err -> do
    logError err
    handleError err
  Right value -> processValue value

-- Error chaining
chainError "Failed to process document" originalError
@
-}
module Langchain.Error
  ( -- * Error Types
    LangchainError (..)
  , ErrorSeverity (..)
  , ErrorCategory (..)
  , ErrorContext (..)

    -- * Error Construction
  , llmError
  , llmErrorWithContext
  , agentError
  , agentErrorWithContext
  , memoryError
  , memoryErrorWithContext
  , toolError
  , toolErrorWithContext
  , vectorStoreError
  , vectorStoreErrorWithContext
  , documentLoaderError
  , documentLoaderErrorWithContext
  , embeddingError
  , embeddingErrorWithContext
  , runnableError
  , runnableErrorWithContext
  , parsingError
  , parsingErrorWithContext
  , networkError
  , networkErrorWithContext
  , configurationError
  , configurationErrorWithContext
  , validationError
  , validationErrorWithContext
  , internalError
  , internalErrorWithContext

    -- * Error Utilities
  , chainError
  , addContext
  , withErrorContext
  , mapError
  , fromString
  , toString
  , toText
  , logError
  , isRetryable
  , getSeverity
  , getCategory
  , fromStringError
  , fromException
  , liftStringError
  , simpleError
  , catchToLangchainError
  , withContext
  , withContextIO

    -- * Type Aliases
  , LangchainResult
  , LangchainIO

    -- * Re-exports for convenience
  , module Control.Exception
  ) where

import Control.Exception (Exception, SomeException, displayException, try)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Aeson (FromJSON, ToJSON)
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Time (UTCTime, getCurrentTime)
import GHC.Generics (Generic)
import System.IO (hPutStrLn, stderr)

-- | Severity levels for errors, following industry standards
data ErrorSeverity
  = -- | System-breaking errors that require immediate attention
    Critical
  | -- | Errors that prevent core functionality
    High
  | -- | Errors that degrade functionality but allow continuation
    Medium
  | -- | Minor errors or warnings
    Low
  | -- | Informational messages
    Info
  deriving (Eq, Ord, Show, Generic, ToJSON, FromJSON)

-- | Categories of errors for better organization and handling
data ErrorCategory
  = -- | Language model related errors
    LLMError
  | -- | Agent execution errors
    AgentError
  | -- | Memory management errors
    MemoryError
  | -- | Tool execution errors
    ToolError
  | -- | Vector store operation errors
    VectorStoreError
  | -- | Document loading errors
    DocumentLoaderError
  | -- | Embedding generation errors
    EmbeddingError
  | -- | Runnable execution errors
    RunnableError
  | -- | Data parsing and validation errors
    ParsingError
  | -- | Network and HTTP errors
    NetworkError
  | -- | Configuration and setup errors
    ConfigurationError
  | -- | Input validation errors
    ValidationError
  | -- | Internal system errors
    InternalError
  deriving (Eq, Show, Generic, ToJSON, FromJSON)

-- | Additional context information for errors
data ErrorContext = ErrorContext
  { contextComponent :: Maybe Text
  -- ^ Component where error occurred
  , contextOperation :: Maybe Text
  -- ^ Operation being performed
  , contextInput :: Maybe Text
  -- ^ Input that caused the error
  , contextMetadata :: [(Text, Text)]
  -- ^ Additional metadata
  , contextTimestamp :: UTCTime
  -- ^ When the error occurred
  }
  deriving (Eq, Show, Generic, ToJSON, FromJSON)

-- | The central error type for langchain-hs
data LangchainError = LangchainError
  { errorMessage :: Text
  -- ^ Human-readable error message
  , errorSeverity :: ErrorSeverity
  -- ^ Severity level
  , errorCategory :: ErrorCategory
  -- ^ Error category
  , errorContext :: Maybe ErrorContext
  -- ^ Additional context
  , errorCause :: Maybe LangchainError
  -- ^ Chained/nested error
  , errorCode :: Maybe Text
  -- ^ Optional error code
  }
  deriving (Eq, Show, Generic, ToJSON, FromJSON)

instance Exception LangchainError where
  displayException LangchainError {..} =
    T.unpack $
      T.unlines $
        filter
          (not . T.null)
          [ "["
              <> T.pack (show errorSeverity)
              <> "] "
              <> T.pack (show errorCategory)
              <> ": "
              <> errorMessage
          , maybe "" ("Error Code: " <>) errorCode
          , maybe "" formatContext errorContext
          , maybe "" (\cause -> "Caused by: " <> T.pack (show cause)) errorCause
          ]
    where
      formatContext ErrorContext {..} =
        T.unlines $
          filter
            (not . T.null)
            [ maybe "" ("Component: " <>) contextComponent
            , maybe "" ("Operation: " <>) contextOperation
            , maybe "" ("Input: " <>) contextInput
            , if null contextMetadata then "" else "Metadata: " <> T.pack (show contextMetadata)
            , "Timestamp: " <> T.pack (show contextTimestamp)
            ]

-- | Type alias for results that can fail with LangchainError
type LangchainResult a = Either LangchainError a

-- | Type alias for IO operations that can fail with LangchainError
type LangchainIO a = IO (LangchainResult a)

-- | Create an LLM-related error
llmError :: Text -> Maybe Text -> Maybe Text -> LangchainError
llmError msg _model _operation =
  LangchainError
    { errorMessage = msg
    , errorSeverity = High
    , errorCategory = LLMError
    , errorContext = Nothing
    , errorCause = Nothing
    , errorCode = Nothing
    }

-- | Create an LLM error with context
llmErrorWithContext ::
  Text ->
  Maybe Text ->
  Maybe Text ->
  ErrorContext ->
  LangchainError
llmErrorWithContext msg model operation ctx =
  (llmError msg model operation)
    { errorContext =
        Just ctx {contextComponent = model, contextOperation = operation}
    }

-- | Create an agent-related error
agentError :: Text -> Maybe Text -> Maybe Text -> LangchainError
agentError msg _agentType _operation =
  LangchainError
    { errorMessage = msg
    , errorSeverity = High
    , errorCategory = AgentError
    , errorContext = Nothing
    , errorCause = Nothing
    , errorCode = Nothing
    }

-- | Create an agent error with context
agentErrorWithContext :: Text -> Maybe Text -> Maybe Text -> ErrorContext -> LangchainError
agentErrorWithContext msg agentType operation ctx =
  (agentError msg agentType operation)
    { errorContext = Just ctx {contextComponent = agentType, contextOperation = operation}
    }

-- | Create a memory-related error
memoryError :: Text -> Maybe Text -> Maybe Text -> LangchainError
memoryError msg _memoryType _operation =
  LangchainError
    { errorMessage = msg
    , errorSeverity = Medium
    , errorCategory = MemoryError
    , errorContext = Nothing
    , errorCause = Nothing
    , errorCode = Nothing
    }

-- | Create a memory error with context
memoryErrorWithContext :: Text -> Maybe Text -> Maybe Text -> ErrorContext -> LangchainError
memoryErrorWithContext msg memoryType operation ctx =
  (memoryError msg memoryType operation)
    { errorContext = Just ctx {contextComponent = memoryType, contextOperation = operation}
    }

-- | Create a tool-related error
toolError :: Text -> Maybe Text -> Maybe Text -> LangchainError
toolError msg _toolName _operation =
  LangchainError
    { errorMessage = msg
    , errorSeverity = High
    , errorCategory = ToolError
    , errorContext = Nothing
    , errorCause = Nothing
    , errorCode = Nothing
    }

-- | Create a tool error with context
toolErrorWithContext :: Text -> Maybe Text -> Maybe Text -> ErrorContext -> LangchainError
toolErrorWithContext msg toolName operation ctx =
  (toolError msg toolName operation)
    { errorContext = Just ctx {contextComponent = toolName, contextOperation = operation}
    }

-- | Create a vector store error
vectorStoreError :: Text -> Maybe Text -> Maybe Text -> LangchainError
vectorStoreError msg _storeType _operation =
  LangchainError
    { errorMessage = msg
    , errorSeverity = High
    , errorCategory = VectorStoreError
    , errorContext = Nothing
    , errorCause = Nothing
    , errorCode = Nothing
    }

-- | Create a vector store error with context
vectorStoreErrorWithContext :: Text -> Maybe Text -> Maybe Text -> ErrorContext -> LangchainError
vectorStoreErrorWithContext msg storeType operation ctx =
  (vectorStoreError msg storeType operation)
    { errorContext = Just ctx {contextComponent = storeType, contextOperation = operation}
    }

-- | Create a document loader error
documentLoaderError :: Text -> Maybe Text -> Maybe Text -> LangchainError
documentLoaderError msg _loaderType _operation =
  LangchainError
    { errorMessage = msg
    , errorSeverity = Medium
    , errorCategory = DocumentLoaderError
    , errorContext = Nothing
    , errorCause = Nothing
    , errorCode = Nothing
    }

-- | Create a document loader error with context
documentLoaderErrorWithContext :: Text -> Maybe Text -> Maybe Text -> ErrorContext -> LangchainError
documentLoaderErrorWithContext msg loaderType operation ctx =
  (documentLoaderError msg loaderType operation)
    { errorContext = Just ctx {contextComponent = loaderType, contextOperation = operation}
    }

-- | Create an embedding error
embeddingError :: Text -> Maybe Text -> Maybe Text -> LangchainError
embeddingError msg _embeddingType _operation =
  LangchainError
    { errorMessage = msg
    , errorSeverity = High
    , errorCategory = EmbeddingError
    , errorContext = Nothing
    , errorCause = Nothing
    , errorCode = Nothing
    }

-- | Create an embedding error with context
embeddingErrorWithContext :: Text -> Maybe Text -> Maybe Text -> ErrorContext -> LangchainError
embeddingErrorWithContext msg embeddingType operation ctx =
  (embeddingError msg embeddingType operation)
    { errorContext = Just ctx {contextComponent = embeddingType, contextOperation = operation}
    }

-- | Create a runnable error
runnableError :: Text -> Maybe Text -> Maybe Text -> LangchainError
runnableError msg _runnableType _operation =
  LangchainError
    { errorMessage = msg
    , errorSeverity = High
    , errorCategory = RunnableError
    , errorContext = Nothing
    , errorCause = Nothing
    , errorCode = Nothing
    }

-- | Create a runnable error with context
runnableErrorWithContext :: Text -> Maybe Text -> Maybe Text -> ErrorContext -> LangchainError
runnableErrorWithContext msg runnableType operation ctx =
  (runnableError msg runnableType operation)
    { errorContext = Just ctx {contextComponent = runnableType, contextOperation = operation}
    }

-- | Create a parsing error
parsingError :: Text -> Maybe Text -> Maybe Text -> LangchainError
parsingError msg _parserType _input =
  LangchainError
    { errorMessage = msg <> fromMaybe "" _parserType
    , errorSeverity = Medium
    , errorCategory = ParsingError
    , errorContext = Nothing
    , errorCause = Nothing
    , errorCode = _input
    }

-- | Create a parsing error with context
parsingErrorWithContext :: Text -> Maybe Text -> Maybe Text -> ErrorContext -> LangchainError
parsingErrorWithContext msg parserType input ctx =
  (parsingError msg parserType input)
    { errorContext = Just ctx {contextComponent = parserType, contextInput = input}
    }

-- | Create a network error
networkError :: Text -> Maybe Text -> Maybe Text -> LangchainError
networkError msg _endpoint _operation =
  LangchainError
    { errorMessage = msg
    , errorSeverity = High
    , errorCategory = NetworkError
    , errorContext = Nothing
    , errorCause = Nothing
    , errorCode = Nothing
    }

-- | Create a network error with context
networkErrorWithContext :: Text -> Maybe Text -> Maybe Text -> ErrorContext -> LangchainError
networkErrorWithContext msg endpoint operation ctx =
  (networkError msg endpoint operation)
    { errorContext = Just ctx {contextComponent = endpoint, contextOperation = operation}
    }

-- | Create a configuration error
configurationError :: Text -> Maybe Text -> Maybe Text -> LangchainError
configurationError msg _configKey _operation =
  LangchainError
    { errorMessage = msg
    , errorSeverity = Critical
    , errorCategory = ConfigurationError
    , errorContext = Nothing
    , errorCause = Nothing
    , errorCode = Nothing
    }

-- | Create a configuration error with context
configurationErrorWithContext :: Text -> Maybe Text -> Maybe Text -> ErrorContext -> LangchainError
configurationErrorWithContext msg configKey operation ctx =
  (configurationError msg configKey operation)
    { errorContext = Just ctx {contextComponent = configKey, contextOperation = operation}
    }

-- | Create a validation error
validationError :: Text -> Maybe Text -> Maybe Text -> LangchainError
validationError msg _field _input =
  LangchainError
    { errorMessage = msg
    , errorSeverity = Medium
    , errorCategory = ValidationError
    , errorContext = Nothing
    , errorCause = Nothing
    , errorCode = Nothing
    }

-- | Create a validation error with context
validationErrorWithContext :: Text -> Maybe Text -> Maybe Text -> ErrorContext -> LangchainError
validationErrorWithContext msg field input ctx =
  (validationError msg field input)
    { errorContext = Just ctx {contextComponent = field, contextInput = input}
    }

-- | Create an internal error
internalError :: Text -> Maybe Text -> Maybe Text -> LangchainError
internalError msg _component _operation =
  LangchainError
    { errorMessage = msg
    , errorSeverity = Critical
    , errorCategory = InternalError
    , errorContext = Nothing
    , errorCause = Nothing
    , errorCode = Nothing
    }

-- | Create an internal error with context
internalErrorWithContext :: Text -> Maybe Text -> Maybe Text -> ErrorContext -> LangchainError
internalErrorWithContext msg component operation ctx =
  (internalError msg component operation)
    { errorContext = Just ctx {contextComponent = component, contextOperation = operation}
    }

-- | Chain an error with a new message, preserving the original as the cause
chainError :: Text -> LangchainError -> LangchainError
chainError msg originalError =
  LangchainError
    { errorMessage = msg
    , errorSeverity = errorSeverity originalError
    , errorCategory = errorCategory originalError
    , errorContext = errorContext originalError
    , errorCause = Just originalError
    , errorCode = errorCode originalError
    }

-- | Add context to an existing error
addContext :: ErrorContext -> LangchainError -> LangchainError
addContext ctx err = err {errorContext = Just ctx}

-- | Execute an action with error context, automatically adding context to any errors
withErrorContext :: MonadIO m => ErrorContext -> LangchainIO a -> m (LangchainResult a)
withErrorContext ctx action = liftIO $ do
  result <- action
  case result of
    Left err -> return $ Left $ addContext ctx err
    Right val -> return $ Right val

-- | Map a function over the error in a result
mapError :: (LangchainError -> LangchainError) -> LangchainResult a -> LangchainResult a
mapError f (Left err) = Left (f err)
mapError _ (Right val) = Right val

-- | Convert a String to LangchainError
fromString :: String -> LangchainError
fromString str = internalError (T.pack str) Nothing Nothing

-- | Convert LangchainError to String
toString :: LangchainError -> String
toString = displayException

-- | Convert LangchainError to Text
toText :: LangchainError -> Text
toText = T.pack . toString

-- | Log an error to stderr (can be extended to use proper logging)
logError :: MonadIO m => LangchainError -> m ()
logError err = liftIO $ hPutStrLn stderr $ toString err

-- | Check if an error is retryable based on its category and severity
isRetryable :: LangchainError -> Bool
isRetryable LangchainError {..} = case errorCategory of
  NetworkError -> errorSeverity <= High
  LLMError -> errorSeverity <= Medium
  VectorStoreError -> errorSeverity <= Medium
  EmbeddingError -> errorSeverity <= Medium
  ToolError -> errorSeverity <= Medium
  _ -> False

-- | Get the severity of an error
getSeverity :: LangchainError -> ErrorSeverity
getSeverity = errorSeverity

-- | Get the category of an error
getCategory :: LangchainError -> ErrorCategory
getCategory = errorCategory

-- | Convert a String error to LangchainError (for backward compatibility)
fromStringError :: String -> LangchainError
fromStringError = fromString

-- | Convert an IO exception to LangchainError
fromException :: SomeException -> LangchainError
fromException ex = internalError (T.pack $ displayException ex) Nothing Nothing

-- | Lift an Either String to LangchainResult
liftStringError :: Either String a -> LangchainResult a
liftStringError (Left err) = Left (fromString err)
liftStringError (Right val) = Right val

-- | Create a simple error with just a message (uses InternalError category)
simpleError :: Text -> LangchainError
simpleError msg = internalError msg Nothing Nothing

-- | Catch IO exceptions and convert them to LangchainError
catchToLangchainError :: IO a -> IO (LangchainResult a)
catchToLangchainError action = do
  result <- try action
  case result of
    Left ex -> return $ Left $ fromException ex
    Right val -> return $ Right val

-- | Run an action and add context to any errors
withContext :: Text -> Text -> LangchainResult a -> LangchainResult a
withContext component operation result = case result of
  Left err ->
    case errorContext err of
      Just ctx ->
        Left $
          err
            { errorContext =
                Just $
                  ErrorContext
                    { contextComponent = Just component
                    , contextOperation = Just operation
                    , contextInput = Nothing
                    , contextMetadata = []
                    , contextTimestamp = contextTimestamp ctx
                    }
            }
      Nothing -> Left err
  Right val -> Right val

-- | Run an action and add context to any errors (IO version)
withContextIO :: MonadIO m => Text -> Text -> LangchainResult a -> m (LangchainResult a)
withContextIO component operation result = case result of
  Left err -> do
    now <- liftIO getCurrentTime
    return $
      Left $
        err
          { errorContext =
              Just $
                ErrorContext
                  { contextComponent = Just component
                  , contextOperation = Just operation
                  , contextInput = Nothing
                  , contextMetadata = []
                  , contextTimestamp = now
                  }
          }
  Right val -> return $ Right val