A Guide to Building Custom Tools in Langchain

A Guide to Building Custom Tools in Langchain

In this blog, we will learn to build personalized custom tools for agents. In the previous blog, I discussed building a Langchain agent with tools, this blog is a continuation of the previous one. In case you have missed the previous blog you can find it here.

Having a custom tool gives us the flexibility to fine-tune the task as per our choice. Today we will build an Agent who will research a topic, write a summary on the topic, and save it to a file. The Agent will use custom tools to perform the 3 tasks:

  1. Search the topic using Google APIs.

  2. Scrape website content and summarise it according to the user's question.

  3. Store the summarised result in the file.

Building a Custom Google Search Tool

For performing a Google search on a user topic we will be using Custom Search JSON API. For using this API we would need an API key from Google API service and an API key from Custom Search JSON API.

In our custom tool, we will pass a user query as an argument and that query will be used in the Google search. Google will respond to us with the search result which will contain the search items. Search items are JSON object that contains information like page titles, page links, metadata, etc. Following is the screenshot of the Google Search response:

From the API response, we will extract the page URLs, below is the code of our Custom Google Search Tool.

import json
import requests

GOOGLE_API_KEY = "<Your Google API Key>"
GOOGLE_CSE_ID = "<Your Custom Google Search Key>"

def get_research_urls(query):
    search_url = build_googlesearch_url(query, GOOGLE_CSE_ID, 10, GOOGLE_API_KEY)
    search_response = google_search_response(search_url)
    research_urls = []
    for item in search_response['items']:
        research_urls.append(item['link']) 
    return research_urls

def build_googlesearch_url(q, cx, num, key):
    q = q.replace(" ", "+")
    base_url = "https://customsearch.googleapis.com/customsearch/v1"
    url_params = f"?q={q}&cx={cx}&num={num}&key={key}&alt=json"
    full_url = base_url + url_params
    return full_url

def google_search_response(url):
    try:
        response = requests.get(url)
        response.raise_for_status()  # Raise an exception for 4xx and 5xx status codes
        json_data = response.json()
        return json_data
    except requests.exceptions.RequestException as e:
        print(f"Error fetching data: {e}")
        return None
    except ValueError as ve:
        print(f"Error parsing JSON data: {ve}")
        return None

We have defined 3 functions here, the main function is get_research_urls which internally calls build_googlesearch_url and google_search_response. As the name suggests, the build_googlesearch_url function will create the Google search URL. The google_search_response function will call the search endpoint and fetch the response. And finally, the get_research_urls function will extract the URLs from the Google search response.

Custom Scraper and Summarizer Tool

Post getting the list of URLs from the search results the next step is to scrape those websites and summarise them. For scraping the website we are using Selenium. For extracting data from the raw HTML response we have used BeautifulSoup.

Our next task is to summarise the page content to extract the user-relevant content by using OpenAI. The text size in the website can be huge and might not be possible to summarise the entire text in one go as there is a limitation from the OpenAI end to receive only 4000 tokens in the request.

To overcome this obstacle we are using map reduce documents chain. Map-reduce is a data processing technique that involves dividing the data into smaller chunks. It starts by invoking the Large Language Model (LLM) with an initial prompt for each data chunk. After processing the initial prompts, the LLM is invoked again, but this time with a modified prompt that incorporates the responses from the first round of prompts.

Let's have a look at our Scraper and Summarizer Tool code:

from langchain import OpenAI
from langchain.chains.summarize import load_summarize_chain
from langchain.docstore.document import Document
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.prompts import PromptTemplate
from langchain.document_loaders import SeleniumURLLoader
from bs4 import BeautifulSoup
from pydantic import BaseModel, Field
from langchain.tools import BaseTool
from typing import Type


def scrape_content(url, question):
    urls = [url]
    loader = SeleniumURLLoader(urls=urls)
    data = loader.load()
    if data is not None and len(data) > 0:
        soup = BeautifulSoup(data[0].page_content, "html.parser")
        text = soup.get_text()
        if len(text) > 1000:
            summary = summarise_content(text, question)
            return summary
        else:
            return text
    return ''


def summarise_content(content, ques):
    llm = ChatOpenAI(temperature=0)
    text_splitter = RecursiveCharacterTextSplitter(
        separators=["\n\n", "\n"], chunk_size=3500, chunk_overlap=300)
    docs = text_splitter.create_documents([content])

    reduce_template_string = """I want you to act as a text summarizer to help me create a concise summary 
        of the text I provide. The summary can be up to 10 sentences in length, expressing the key points and 
        concepts written in the original text without adding your interpretations. The summary should be focused on
        answering the given question and the other details which are not relevant to the given question can be ignored.
        My first request is to summarize this text – 
        {text}

        Question: {question}
        Answer:
    """
    reduce_template = PromptTemplate(
        template=reduce_template_string, input_variables=["text", "question"])
    chain = load_summarize_chain(
        llm=llm,
        chain_type='map_reduce',
        map_prompt=reduce_template,
        combine_prompt=reduce_template,
        verbose=True
    )
    return chain.run(input_documents=docs, question=ques)


class ScraperInput(BaseModel):
    """Inputs for scrape_content function"""
    url: str = Field(description="The url or link of the website to be scraped")
    question: str = Field(description="The question or the query that users give to the agent")


class ScraperTool(BaseTool):
    name = "website_scraper_tool"
    description = "useful when you need to get data from a website url, passing both url and question to the function; DO NOT make up any url, the url should only be from the search results"
    args_schema: Type[BaseModel] = ScraperInput

    def _run(self, url: str, question: str):
        return scrape_content(url, question)

    def _arun(self, url: str):
        raise NotImplementedError("error here")

Our main function is scrape_content which internally calls summarise_content function to summarise the content. The scrape_content function currently accepts two parameters url, the URL of the webpage which needs to be scrapped. The second parameter is question, it is the user-asked query based on which the content should be summarised. The idea here is to summarise only those portions of the text that are relevant to the user's query, the remaining can be ignored as it is irrelevant to the user.

If we directly try to use the function scrape_content as our Agent tool, it will throw an exception since each tool is allowed to have only one parameter. If we have a requirement of sending two arguments to our tool then we have to take the Tool class approach. In the above code, we have defined a new class called ScraperTool and it inherits from the BaseTool class. We have defined a separate class for our function parameter called ScraperInput which inherits BaseModel, and the class contains only 2 attributes url and a question. The ScraperTool class has a run method that calls our scrape_content function. This is the format we should follow when one pass more that one arguments to our tools. We also have the attribute fields in our ScraperTool class which are name, description and args_schema.

Custom File Appender Tool

The tool is pretty straightforward, we will use this tool for writing our summarised content in the file. Following is the code for our tool:

FILE_NAME = "summary_content.txt"

def append_text_to_file(text):
    try:
        # Open the file in 'a' mode (append mode) which creates the file if it doesn't exist
        with open(FILE_NAME, 'a') as file:
            file.write(text + '\n') # Append the text to the file
        print(f"Text appended to '{FILE_NAME}' successfully.")
    except IOError as e:
        print(f"Error: {e}")

We only have one function append_text_to_file and it accepts only one parameter. It then writes the content in a file.

Using Tools in Agent

So far we have defined all our custom tools and now it's time to add them to our agent. Following is the code of our agent.

from langchain.schema import SystemMessage
from langchain.chat_models import ChatOpenAI
from langchain.agents import initialize_agent
from langchain.agents import AgentType
from langchain.tools import Tool

system_message = SystemMessage(
    content="""You are a world class researcher, who can do detailed research on any topic.

        Please make sure you complete the objective above with the following rules:
        1/ Your job is to first breakdown the topic into relevant questions for understanding the topic in detail. You should have at max only 3 questions not more than that, in case there are more than 3 questions consider only the first 3 questions and ignore the others.
        2/ You should use the search tool to get more information about the topic. You are allowed to use the search tool only 3 times in this process.    
        3/ Aggregate all the resources or urls that you can get on this topic.
        4/ Use the provide scrape tool to scrape through your aggregrated list of urls one by one.
        5/ Write the content you got from the scrape tool into a file. Use the write_to_file tool for writing to the file.

        Your task is complete once you have written all the scraped content to the file.
        """
)


tools = [
    Tool(
        name="Search",
        func=get_research_urls,
        description="useful for when you need to answer questions about current events, data. This will return list of useful urls which should be scraped to get additional information."
    ),
    ScraperTool(),
    Tool(
        name="write_to_file",
        func=append_text_to_file,
        description="useful when you need to write any text in the file."
    ),
]


agent_kwargs = {
    "system_message": system_message,
}

llm = ChatOpenAI(temperature=0, model="gpt-3.5-turbo", verbose=True)

agent = initialize_agent(
    tools,
    llm,
    agent=AgentType.OPENAI_FUNCTIONS,
    verbose=True,
    agent_kwargs=agent_kwargs,
)

In our tools array, we have defined the list of tools. The search and write to file tools have only one parameter so are able to define them directly. We have used the name, func and description attributes where we have defined the name of the tool, the function to be called by the Agent when it wants to use it, and then the description, which is read by the agent to make a judgment on when to use this tool.

We have used the initialize_agent function to initialize the agent, we are using ChatOpenAI as the LLM model and OPENAI_FUNCTIONS as the AgentType. We have written a System message prompt for our agent which are nothing but a series of instructions that the Agent is supposed to follow. We have passed the prompt to our agent in the agent_kwargs argument which is an optional argument.

Agents In Action

Till now we have defined our custom tools and Agents, now it's time to see our agent in action. Let's give our agent the task of building apps using Langchain agents.

response = agent.run("Langchain agents for building apps")

Below is the screenshot of our Agent execution logs:

The agent starts its action with the search task where it queries Google with the given query. It gets one url in response and then uses the scraper tool to scrape that page and summarise it. Once it gets the summary it invokes the file appender tool to write the summary in the file. Following is the screenshot for the same.

I trust you discovered this blog post on building a custom Langchain tool to be beneficial. I continuously produce similar content covering topics related to Langchain, LLM, and AI. If you wish to receive more articles of this nature, consider subscribing to my blog.

If you're in the Langchain space or LLM domain, let's connect on Linkedin! I'd love to stay connected and continue the conversation. Reach me at: linkedin.com/in/ritobrotoseth

Did you find this article valuable?

Support Ritobroto Seth by becoming a sponsor. Any amount is appreciated!