1. Introduction

In the previous article, we talked about AI agents and developed a basic calculator agent using LangChain and OpenAI. Purpose of starting with a very basic agent was to demonstrate the LangChain’s core components and explore how OpenAI’s large language models (LLMs) can be used to create intelligent systems.

Building on the same foundation, let us explore more sophisticated concepts and delve deeper into the LangChain framework. We’ll expand our understanding of the fundamentals and basic concepts of agentic frameworks, and will create more sophisticated set of tools.

2. Fundamentals of Agentic Frameworks

At its core, an agentic framework is a system that allows AI agents to interact with their environment, make decisions, and perform actions to achieve specific goals. Key components of agentic frameworks include:

  • Agents: The AI entities that perceive, decide, and act.
  • Environment: The context in which agents operate.
  • Tools: Functionalities that agents can use to perform actions.
  • Memory: Mechanisms for storing and retrieving information.
  • Decision-making processes: Methods for choosing actions based on perceptions and goals.

3. Types of Agents

LangChain offers several types of agents, along with different dimensions:

  • Tool Calling: This agent is helpful when a model capable of calling external tools is used.
  • OpenAI Tools: Similar to the above, but supporting Open AI Models and tools.
  • OpenAI Functions: This legacy type is used with Open AI models supporting function calls.
  • XML: This is useful using LLM Models, which efficiently process XML inputs/output like anthropic.
  • Structured Chat: This is best suited when supporting tools with multiple inputs are needed.
  • JSON Chat: This is useful when using LLM Models capable of handling JSON inputs.
  • ReAct: When simpler models are used, this is the best fit.
  • Self Ask With Search: This Agent supports only one tool, which isalso a search tool. Best suited when you have simple Q 7 A use case.

Word of Caution: As stated earlier, Gen AI and especially Agenting platforms are changing rapidly. You need to be very careful with the version of LangChain. The above list is supported in V0.1, and there might be some changes in different v0.1.x.

4. Decision-Making Process Of Agentic Platforms

The following steps are taken by LangChain agents during the decision-making process:

  • Perception: The agent receives input (e.g., a user query).
  • Thought: The agent considers the input and its available tools.
  • Action: The agent decides on an action (e.g., using a specific tool).
  • Observation: The agent observes the result of its action.
  • Repeat: The agent repeats this process until it reaches a final answer or conclusion.

This process is part of what’s known as the “ReAct” (Reason+Act) framework. ReAct is crucial for enabling agents to reason through tasks and decide on the best actions to take based on the situation.

5. LangChain: A Closer Look

LangChain provides a robust architecture for building AI applications. Let’s dive deeper into its core concepts:

  • Models:At the heart of LangChain are the language models. These can be:
    · Large Language Models (LLMs): Models that take a string prompt as input and return a string completion as output.
    · Chat Models: Models that take a list of chat messages as input and return a chat message as output.
  • Prompts: Prompts are the inputs to models. LangChain provides several utilities for working with prompts:
    · PromptTemplates: For creating reproducible prompts with dynamic inputs.
    · Example Selectors: For choosing relevant examples to include in prompts.
    · Output Parsers: For structuring model outputs.
  • Chains: Chains are sequences of calls to components like models, prompts, or even other chains. They allow you to combine multiple steps into a single coherent workflow.
  • Agents: Agents use language models to determine which actions to take and in what order. They can use tools and manage multi-step tasks.
  • Tools: Tools are functions that agents can use to interact with the world or perform specific tasks.
  • Memory: Memory components allow chains and agents to retain information across multiple calls.
  • Indexes: Indexes are data structures used to organize documents or other data for efficient retrieval. They’re crucial for tasks like question-answering over specific document sets.

6. LangChain Libraries & Building Blocks

Let us understand the critical libraries from LangChain that can implement these concepts.

langchain.tools & chain
In this example, we will create a web search tool that uses the Serper API to search the Internet. We will also use a summarizer tool and set these tools as a list, which the agent will use further.

The below code also usage summarizer chain:

# Other imports
from langchain.utilities import GoogleSerperAPIWrapper
from langchain.chains.summarize import load_summarize_chain
from langchain.text_splitter import CharacterTextSplitter
from langchain.docstore.document import Document

# Tools definition and other supporting methods
# Custom text splitter
class CustomTextSplitter(CharacterTextSplitter):
    def __init__(self, chunk_size=1000, chunk_overlap=20, **kwargs):
        self.chunk_size = chunk_size
        self.chunk_overlap = chunk_overlap
        super().__init__(chunk_size=chunk_size, chunk_overlap=chunk_overlap, **kwargs)

    def split_text(self, text: str) -> List[str]:
        # Simple word-based splitting
        words = text.split()
        chunks = []
        current_chunk = []
        current_chunk_length = 0
        for word in words:
            if current_chunk_length + len(word) > self.chunk_size:
                chunks.append(" ".join(current_chunk))
                current_chunk = []
                current_chunk_length = 0
            current_chunk.append(word)
            current_chunk_length += len(word) + 1  # +1 for space
        if current_chunk:
            chunks.append(" ".join(current_chunk))
        return chunks

# Set up the LLM
llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0)

# Set up Serper for web search
search = GoogleSerperAPIWrapper()

# Set up text splitter for summarization
text_splitter = CustomTextSplitter(chunk_size=1000, chunk_overlap=20)

# Set up summarization chain
summarize_chain = load_summarize_chain(llm, chain_type="map_reduce")

def web_search(query):
    return search.run(query)

def summarize(text):
    docs = [Document(page_content=chunk) for chunk in text_splitter.split_text(text)]
    summary = summarize_chain.invoke(docs)
    return summary["output_text"]

tools = [
    Tool(
        name="Web Search",
        func=web_search,
        description="Useful for searching the web for current information on a topic."
    ),
    Tool(
        name="Summarizer",
        func=summarize,
        description="Useful for summarizing long pieces of text."
    )
]

langchain.agents
This part we already touched upon in the previous part of the series:

# Other imports
from langchain.agents import AgentExecutor, create_react_agent, Tool

# Other setup 
# Construct the ReAct agent
agent = create_react_agent(llm, tools, prompt) 

langchain.memory
Although our current example doesn’t explicitly use memory, it’s crucial for many agent applications.


7. Plugging the Pieces Together
This covers the complete code where all the previous snippets has been fully integrated. As usual we are having an API endpoint to query the agent.

import os
from dotenv import load_dotenv
from flask import Flask, request, jsonify
from langchain.agents import initialize_agent, Tool
from langchain.agents import AgentType

from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
from langchain.text_splitter import CharacterTextSplitter
from langchain.schema import Document
import requests
import json

from langchain.chat_models import ChatOpenAI

# Load environment variables
load_dotenv()

app = Flask(__name__)
SERPER_API_KEY = os.getenv('SERPER_API_KEY')
OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')

# Custom text splitter for summarization
def custom_text_splitter(text, chunk_size=1000, chunk_overlap=20):
    splitter = CharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap)
    return splitter.split_text(text)

def summarize(text):
    chunks = custom_text_splitter(text)
    docs = [Document(page_content=chunk) for chunk in chunks]
    
    prompt_template = """Write a concise summary of the following text:
    "{text}"
    CONCISE SUMMARY:"""
    prompt = PromptTemplate(template=prompt_template, input_variables=["text"])
    
    llm = ChatOpenAI(temperature=0.7)
    summarize_chain = LLMChain(llm=llm, prompt=prompt)
    
    summaries = []
    for doc in docs:
        summary = summarize_chain.run(doc.page_content)
        summaries.append(summary)
    
    return " ".join(summaries)


def web_search(query: str) -> str:
    url = "https://google.serper.dev/search"
    payload = json.dumps({"q": query})
    headers = {
        'X-API-KEY': SERPER_API_KEY,
        'Content-Type': 'application/json'
    }
    response = requests.request("POST", url, headers=headers, data=payload)
    
    if response.status_code == 200:
        results = response.json()
        formatted_results = []
        for item in results.get('organic', [])[:3]:
            title = item.get('title', 'No title')
            snippet = item.get('snippet', 'No snippet')
            link = item.get('link', 'No link')
            formatted_results.append(f"Title: {title}\nSnippet: {snippet}\nLink: {link}\n")
        return "\n".join(formatted_results)
    else:
        return f"Error in web search: {response.status_code} - {response.text}"
    
tools = [
    Tool(
        name="Web Search",
        func=web_search,
        description="Search the web for current information.",
    ),
    Tool(
        name="Summarizer",
        func=summarize,
        description="Summarize long pieces of text.",
    )
]


# Set up the LLM
llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0)

# Initialize the agent
agent = initialize_agent(
    tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True
)

@app.route("/query", methods=["POST"])
def query_agent():
    data = request.json
    if "question" not in data:
        return jsonify({"error": "No question provided"}), 400
    
    question = data["question"]
    try:
        response = agent.run(question)
        return jsonify({"response": response})
    except Exception as e:
        return jsonify({"error": str(e)}), 500

if __name__ == "__main__":
    app.run(debug=True)

Run the application:

This will start the Flask server on http://localhost:5000

To query the agent, send a POST request to the /query endpoint:

curl -X POST -H “Content-Type: application/json” -d ‘{“input”: “What are the latest developments in AI?”}’ http://localhost:5000/query

8. Conclusion and Next Steps

In this article, we’ve explored more concepts around the agentic framework and created a complex agent using LangChain and OpenAI. We’ve seen how to create flexible, extensible systems that can use multiple tools. We’ve introduced the React framework, setting the stage for a deeper exploration in our next blog.

Stay tuned for Part 3 of our series, where we’ll explore how the ReAct framework and Chain of Thought reasoning enable agents to tackle even more complex problems.

Additional Notes
Before you run the code, get the API Key from https://serper.dev/ . Keep your API Keys in the environment file. Don’t forget to install the dependencies using requirements.txt. The complete working code can be found in my GitHub repo.

Happy Learning!!

One thought on “Building Intelligent Agents with LangChain and OpenAI — Part 2: Diving Deeper into LangChain Agentic Concepts

Leave a comment