packages feed

langchain-hs-0.0.2.0: src/Langchain/Tool/WebScraper.hs

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}

{- |
Module      : Langchain.Tool.WebScraper
Description : Tool for scrapping text content from URL
Copyright   : (c) 2025 Tushar Adhatrao
License     : MIT
Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>
Stability   : experimental

WebScraper is a tool that scrapes text content from a given URL.
It fetches the HTML content of the page, extracts the body text, removes scripts, and strips class/id/style attributes from the HTML tags.
It is designed to be used with the Langchain framework for building language models and applications.
-}
module Langchain.Tool.WebScraper (WebScraper (..), WebPageInfo (..), fetchAndScrape) where

import Control.Exception (SomeException, try)
import Data.Aeson (ToJSON)
import qualified Data.ByteString.Lazy as LBS
import Data.Maybe (listToMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import GHC.Generics (Generic)
import Langchain.Tool.Core
import Langchain.Tool.Utils
import Network.HTTP.Simple
import qualified Text.HTML.TagSoup as TS

-- | Represents a web scraper tool that extracts content from web pages
data WebScraper = WebScraper
  deriving (Show)

-- | Stores the extracted webpage information
data WebPageInfo = WebPageInfo
  { pageTitle :: Maybe Text
  , pageContent :: Text
  }
  deriving (Show, Generic)

-- Make WebPageInfo serializable to JSON
instance ToJSON WebPageInfo

-- | Input type for the WebScraper - just a URL
type ScraperInput = Text

-- | Implement the Tool typeclass for WebScraper
instance Tool WebScraper where
  type Input WebScraper = ScraperInput
  type Output WebScraper = (Either String Text)

  toolName _ = "web_scraper"

  toolDescription _ =
    "Scrapes content from a webpage. Provide a valid URL, and it will extract only the textual body content "
      <> "with scripts removed and without class/id/style attributes."

  runTool _ url = do
    result <- fetchAndScrape url
    case result of
      Left err -> pure $ Left $ "Error scraping webpage: " <> err
      Right info -> pure $ Right $ pageContent info

-- | Fetch HTML content from a URL and extract webpage information
fetchAndScrape :: Text -> IO (Either String WebPageInfo)
fetchAndScrape url = do
  request_ <- parseRequest (T.unpack url)
  eResp <- try $ httpLBS request_ :: IO (Either SomeException (Response LBS.ByteString))
  case eResp of
    Left err -> pure $ Left (show err)
    Right r -> do
      let rBody = (getResponseBody r)
      let htmlContent = TE.decodeUtf8 $ LBS.toStrict rBody

      -- Clean and extract the content
      let tags = TS.parseTags htmlContent
      let title = extractTitle tags
      let cleanedContent = cleanBodyContent tags

      pure $ Right $ WebPageInfo title cleanedContent

-- | Extract the title from parsed HTML tags
extractTitle :: [TS.Tag Text] -> Maybe Text
extractTitle tags =
  let titleTags = TS.partitions (TS.isTagOpenName "title") tags
   in if null titleTags
        then Nothing
        else case listToMaybe titleTags of
          Nothing -> Nothing
          Just r -> Just $ T.strip $ TS.innerText r