packages feed

langchain-hs-0.0.3.0: src/Langchain/Tool/DuckDuckGo.hs

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

{- |
Module      : Langchain.Tool.DuckDuckGo
Description : Tool for extracting DuckDuckGo search content
Copyright   : (c) 2025 Tushar Adhatrao
License     : MIT
Maintainer  : Tushar Adhatrao <tusharadhatrao@gmail.com>
Stability   : experimental

Please note: DuckDuckGo Tool only returns result if the search term has a abstract card
-}
module Langchain.Tool.DuckDuckGo (DuckDuckGo (..)) where

import Control.Exception (SomeException, catch)
import Data.Aeson
import Data.Maybe
import Data.Text (Text)
import qualified Data.Text as T
import GHC.Generics (Generic)
import Langchain.Tool.Core
import Network.HTTP.Simple

-- | Icon data within related topics
newtype Icon = Icon
  { iconURL :: Maybe Text
  }
  deriving (Show, Eq, Generic)

instance FromJSON Icon where
  parseJSON = withObject "Icon" $ \v ->
    Icon
      <$> v .:? "URL"

-- | A single related topic
data RelatedTopic = RelatedTopic
  { topicFirstURL :: Maybe Text
  , topicIcon :: Maybe Icon
  , topicResult :: Maybe Text
  , topicText :: Maybe Text
  , topicName :: Maybe Text
  , topicTopics :: Maybe [RelatedTopic]
  }
  deriving (Show, Eq, Generic)

instance FromJSON RelatedTopic where
  parseJSON = withObject "RelatedTopic" $ \v ->
    RelatedTopic
      <$> v .:? "FirstURL"
      <*> v .:? "Icon"
      <*> v .:? "Result"
      <*> v .:? "Text"
      <*> v .:? "Name"
      <*> v .:? "Topics"

-- | Meta information about the source
data MetaDeveloper = MetaDeveloper
  { devName :: Text
  , devURL :: Text
  }
  deriving (Show, Eq, Generic)

instance FromJSON MetaDeveloper where
  parseJSON = withObject "MetaDeveloper" $ \v ->
    MetaDeveloper
      <$> v .: "name"
      <*> v .: "url"

-- | Source options within meta information
data MetaSrcOptions = MetaSrcOptions
  { isMediaWiki :: Maybe Int
  , isWikipedia :: Maybe Int
  , language :: Maybe Text
  }
  deriving (Show, Eq, Generic)

instance FromJSON MetaSrcOptions where
  parseJSON = withObject "MetaSrcOptions" $ \v ->
    MetaSrcOptions
      <$> v .:? "is_mediawiki"
      <*> v .:? "is_wikipedia"
      <*> v .:? "language"

-- | Meta information about the response
data Meta = Meta
  { metaDescription :: Maybe Text
  , metaDeveloper :: Maybe [MetaDeveloper]
  , metaName :: Maybe Text
  , metaPerlModule :: Maybe Text
  , metaSrcDomain :: Maybe Text
  , metaSrcName :: Maybe Text
  , metaSrcOptions :: Maybe MetaSrcOptions
  }
  deriving (Show, Eq, Generic)

instance FromJSON Meta where
  parseJSON = withObject "Meta" $ \v ->
    Meta
      <$> v .:? "description"
      <*> v .:? "developer"
      <*> v .:? "name"
      <*> v .:? "perl_module"
      <*> v .:? "src_domain"
      <*> v .:? "src_name"
      <*> v .:? "src_options"

-- | DuckDuckGo API response
data DuckDuckGoResponse = DuckDuckGoResponse
  { abstract :: Text
  , abstractSource :: Text
  , abstractText :: Text
  , abstractURL :: Text
  , answer :: Text
  , answerType :: Text
  , definition :: Text
  , definitionSource :: Text
  , definitionURL :: Text
  , entity :: Text
  , heading :: Text
  , image :: Text
  , imageHeight :: Int
  , imageIsLogo :: Int
  , imageWidth :: Int
  , infobox :: Text
  , redirect :: Text
  , relatedTopics :: [RelatedTopic]
  , results :: [Value]
  , resultType :: Text -- Called "Type" in the API
  , meta :: Maybe Meta
  }
  deriving (Show, Eq, Generic)

instance FromJSON DuckDuckGoResponse where
  parseJSON = withObject "DuckDuckGoResponse" $ \v ->
    DuckDuckGoResponse
      <$> v .: "Abstract"
      <*> v .: "AbstractSource"
      <*> v .: "AbstractText"
      <*> v .: "AbstractURL"
      <*> v .: "Answer"
      <*> v .: "AnswerType"
      <*> v .: "Definition"
      <*> v .: "DefinitionSource"
      <*> v .: "DefinitionURL"
      <*> v .: "Entity"
      <*> v .: "Heading"
      <*> v .: "Image"
      <*> v .: "ImageHeight"
      <*> v .: "ImageIsLogo"
      <*> v .: "ImageWidth"
      <*> v .: "Infobox"
      <*> v .: "Redirect"
      <*> v .: "RelatedTopics"
      <*> v .: "Results"
      <*> v .: "Type"
      <*> v .:? "meta"

{-
-- | Error type for DuckDuckGo API calls
data DuckDuckGoError
  = NetworkError Text
  | ParseError Text
  | OtherError Text
  deriving (Show, Eq, Generic)

instance ToJSON DuckDuckGoError where
  toJSON (NetworkError msg) = object ["type" .= ("network" :: Text), "message" .= msg]
  toJSON (ParseError msg) = object ["type" .= ("parse" :: Text), "message" .= msg]
  toJSON (OtherError msg) = object ["type" .= ("other" :: Text), "message" .= msg]
  -}

-- | Query parameter for DuckDuckGo search
newtype DuckDuckGoQuery = DuckDuckGoQuery
  { query :: Text
  }
  deriving (Show, Eq, Generic)

instance ToJSON DuckDuckGoQuery where
  toJSON q = object ["query" .= query q]

-- | The DuckDuckGo tool data type
data DuckDuckGo = DuckDuckGo
  deriving (Show, Eq)

-- | Tool instance for DuckDuckGo
instance Tool DuckDuckGo where
  type Input DuckDuckGo = Text
  type Output DuckDuckGo = Text

  toolName _ = "duckduckgo"

  toolDescription _ =
    "Performs web searches using DuckDuckGo and returns structured information about results"

  runTool _ queryData = do
    let searchTerm = T.replace " " "+" (T.strip queryData)
    let urlString =
          "https://duckduckgo.com/?q="
            <> T.unpack searchTerm
            <> "&format=json"
    eResult <-
      ( do
          request <- parseRequest urlString
          response <- httpLbs request
          let body = getResponseBody response
          case eitherDecode body of
            Left err -> pure $ Left $ T.pack $ show err
            Right ddgResponse_ -> pure $ Right ddgResponse_
      )
        `catch` \e -> pure $ Left $ T.pack $ show (e :: SomeException)
    case eResult of
      Left err -> pure err
      Right r -> pure $ ddgToText r

-- | Converts a DuckDuckGoResponse into a concise textual summary suitable for LLM input.
ddgToText :: DuckDuckGoResponse -> Text
ddgToText resp =
  T.intercalate "\n\n" $
    catMaybes
      [ Just ("# " <> heading resp)
      , abstractSection resp
      , answerSection resp
      , definitionSection resp
      , relatedTopicsSection (relatedTopics resp)
      ]

abstractSection :: DuckDuckGoResponse -> Maybe Text
abstractSection resp = do
  abst <- if T.null (abstract resp) then Nothing else Just (abstract resp)
  url <- if T.null (abstractURL resp) then Nothing else Just (abstractURL resp)
  Just $ "Abstract: " <> abst <> "\nSource: " <> url

answerSection :: DuckDuckGoResponse -> Maybe Text
answerSection resp =
  if T.null (answer resp)
    then Nothing
    else Just ("Answer: " <> answer resp)

definitionSection :: DuckDuckGoResponse -> Maybe Text
definitionSection resp = do
  def <- if T.null (definition resp) then Nothing else Just (definition resp)
  url <-
    if T.null (definitionURL resp)
      then
        Nothing
      else Just (definitionURL resp)
  Just $ "Definition: " <> def <> "\nSource: " <> url

relatedTopicsSection :: [RelatedTopic] -> Maybe Text
relatedTopicsSection rts =
  let processed = concatMap processRelatedTopic rts
   in if null processed then Nothing else Just (T.unlines processed)

processRelatedTopic :: RelatedTopic -> [Text]
processRelatedTopic rt =
  case (topicName rt, topicTopics rt) of
    -- Handle categorized group
    (Just name, Just subtopics) ->
      ("*" <> name <> "*") : concatMap processRelatedTopic subtopics
    -- Handle individual topic
    _ ->
      case (topicText rt, topicFirstURL rt) of
        (Just text, Just url) -> ["- [" <> text <> "](" <> url <> ")"]
        _ -> []