packages feed

langchain-hs-0.0.1.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
-}
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 Network.HTTP.Simple
import Text.HTML.Scalpel

-- | 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
  , pageHeadings :: [Text]
  , pageLinks :: [(Text, Text)] -- (Link text, URL)
  , pageText :: 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 = Text

  toolName _ = "web_scraper"

  toolDescription _ =
    "Scrapes content from a webpage. Provide a valid URL, and it will extract the title,"
      <> "headings, links, and text content."

  runTool _ url = do
    result <- fetchAndScrape url
    case result of
      Left err -> pure $ "Error scraping webpage: " <> T.pack (show err)
      Right info -> pure $ T.pack (show 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
      let scraped = scrapeStringLike htmlContent scrapeWebPageInfo
      case scraped of
        Nothing -> pure $ Left "Failed to parse HTML content"
        Just info -> pure $ Right info

-- | Define the Scalpel scraper for extracting webpage information
scrapeWebPageInfo :: Scraper Text WebPageInfo
scrapeWebPageInfo = do
  title <- scrapeTitle
  headings <- scrapeHeadings
  links <- scrapeLinks
  t <- scrapeText
  return $ WebPageInfo title headings links t

-- | Scrape the page title
scrapeTitle :: Scraper Text (Maybe Text)
scrapeTitle = fmap listToMaybe $ texts "title"

-- | Scrape all headings (h1-h6)
scrapeHeadings :: Scraper Text [Text]
scrapeHeadings = do
  h1s <- texts "h1"
  h2s <- texts "h2"
  h3s <- texts "h3"
  h4s <- texts "h4"
  h5s <- texts "h5"
  h6s <- texts "h6"
  return $ concat [h1s, h2s, h3s, h4s, h5s, h6s]

-- | Scrape all links with their URLs
scrapeLinks :: Scraper Text [(Text, Text)]
scrapeLinks = chroots "a" $ do
  linkText <- text "a"
  linkHref <- attr "href" "a"
  return (linkText, linkHref)

-- | Scrape main text content (from p, div, span elements)
scrapeText :: Scraper Text Text
scrapeText = do
  paragraphs <- texts "p"
  divs <- texts "div"
  spans <- texts "span"
  listElems <- texts "li"
  return $ T.intercalate "\n\n" $ filter (not . T.null) $ concat [paragraphs, divs, spans, listElems]