Building an intelligent agent with LangGraph

Trevor Thayer
Indicium Engineering

--

Imagine an intelligent agent that effortlessly juggles complex tasks, executes functions, and interacts with various APIs. With LangGraph, this is not just a possibility but a reality.

In this tutorial, we will break down the core concepts of LangGraph, explain the interactions between the code components, and highlight why this approach is effective.

Our project will focus on creating an agent to fetch live weather updates based on user input.

What are LangChain and LangGraph?

LangChain is a powerful library that helps you build intelligent agents leveraging language models like GPT. It provides tools and frameworks to interact with various APIs, execute functions, and manage complex tasks.

LangGraph complements LangChain by enabling you to create workflows or state machines. Think of it as a graph to organize and manage the flow of operations within your agent.

Imagine your agent’s tasks as nodes (points) on a graph, and the connections between these tasks as edges (lines).

LangGraph ensures that each step in your agent’s process is well-defined and follows a clear path, making it easier to build, debug, and maintain complex systems.

Defining the Agent’s State

The first step in creating a LangGraph agent is defining its state.

What is a state?

The state holds all the necessary information during the agent’s operation, including the input, chat history, outcomes, and intermediate steps.

Think of the state as a snapshot of your agent’s brain, capturing everything it needs to know and remember to function correctly.

  • Input: The current input string the agent is processing.
  • Chat History: A list of previous messages, providing context for the ongoing interaction.
  • Agent Outcome: The result of the agent’s action, indicating what the agent has done or will do next.
  • Intermediate Steps: A log of actions and observations, helping in debugging and understanding the agent’s decision-making process.

Here’s an example of how to define the agent’s state in code:

from typing import TypedDict, Annotated, List, Union
from langchain_core.agents import AgentAction, AgentFinish
from langchain_core.messages import BaseMessage
import operator

class AgentState(TypedDict):
input: str # The input string
chat_history: list[BaseMessage] # The list of previous messages in the conversation
agent_outcome: Union[AgentAction, AgentFinish, None] # The outcome of a given call to the agent
intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add] # List of actions and corresponding observations

Why is the Agent’s State so important?

Maintaining Context:

  • The state allows the agent to maintain the context of the conversation. By keeping track of the chat history, the agent can provide relevant and coherent responses based on previous interactions.

Decision Making:

  • With the current input and the history of the conversation, the agent can make informed decisions about the next action to take. This ensures that the agent’s responses are logical and appropriate for the ongoing dialogue.

Outcome Management:

  • The state helps in managing the outcomes of the agent’s actions. Whether the agent is executing a specific function (AgentAction) or completing a task (AgentFinish), the state records these outcomes, allowing the agent to proceed with the next steps seamlessly.

Debugging and Understanding:

  • By logging intermediate steps, the state provides a detailed record of the agent’s actions and observations. This is crucial for debugging and understanding how the agent arrived at a particular decision, making it easier to identify and fix issues.

LangGraph uses this state to organize and manage the flow of operations. Each action taken by the agent is part of a larger workflow, ensuring that all steps are executed in the correct order and that the system can handle complex sequences of tasks.

Running the Agent and Executing Tools

After defining the agent’s state, the next steps are to set up the agent and create functions to run it and execute tools. These functions will form the nodes of our workflow, allowing the agent to operate efficiently.

Define the Agent

Tools: We specify the tools our agent will use. Here, TavilySearchResults is used to fetch live weather data, but can be configured fetch realtime online information.

Language Model (LLM): We use ChatOpenAI with the gpt-3.5-turbo-1106 model and enable streaming for real-time responses.

Prompt: A predefined prompt is pulled from a hub to set the context or instructions for the agent, guiding its behavior and responses.

Agent Runnable: We create the agent using create_openai_functions_agent, passing in the LLM, tools, and prompt.

from langchain import hub
from langchain.agents import create_openai_functions_agent
from langchain_openai.chat_models import ChatOpenAI
from langchain_community.tools.tavily_search import TavilySearchResults

tools = [TavilySearchResults(max_results=1)]
llm = ChatOpenAI(model="gpt-3.5-turbo-1106", streaming=True)
prompt = hub.pull("hwchase17/openai-functions-agent")
agent_runnable = create_openai_functions_agent(llm, tools, prompt)

Run the Agent

The run_agent function handles decision-making, leveraging language models to process and respond to inputs. In our project, this function will process the user's query about the weather.

def run_agent(data):
agent_outcome = agent_runnable.invoke(data)
return {"agent_outcome": agent_outcome}

Execute Tools

The execute_tools function ensures the agent can execute specific actions using predefined tools, logging the results for further reference. It will fetch the weather data and record the actions taken.

from langgraph.prebuilt.tool_executor import ToolExecutor

tool_executor = ToolExecutor(tools)

def execute_tools(data):
agent_action = data["agent_outcome"]
output = tool_executor.invoke(agent_action)
return {"intermediate_steps": [(agent_action, str(output))]}

Managing Conditional Logic

In LangGraph, managing conditional logic is similar to setting up decision points in a flowchart. These decision points guide the agent on what to do next based on the current state or the outcome of an action.

Let’s imagine you are on a journey with checkpoints. At each checkpoint, you decide whether to continue or stop based on certain conditions.

Here’s a function that acts as a decision point:

def should_continue(data):
if isinstance(data["agent_outcome"], AgentFinish):
return "end"
else:
return "continue"

In this function, data represents the information gathered at each checkpoint. The data["agent_outcome"] is the result of the agent's action.

If the outcome is AgentFinish, it means the task is complete, and the journey should stop by returning "end". Otherwise, it returns "continue", indicating that the journey should proceed to the next step.

Creating the Workflow

Creating a workflow in LangGraph is like drawing a flowchart that visualizes the sequence of steps your agent will take to fetch weather updates. Let’s build this step-by-step.

First, we define the flowchart nodes, each representing a step or action the agent will perform. We then set an entry point, which is where the process begins. Next, we add decision points that determine the next step based on the outcome of the current step. Finally, we compile the flowchart, putting everything together into a working process.

Here’s how you can do this in code:

from langgraph.graph import END, StateGraph

workflow = StateGraph(AgentState)

workflow.add_node("agent", run_agent)
workflow.add_node("action", execute_tools)

workflow.set_entry_point("agent")

workflow.add_conditional_edges(
"agent",
should_continue,
{
"continue": "action",
"end": END,
},
)

workflow.add_edge("action", "agent")

app = workflow.compile()

In this code, we first create a StateGraph instance to define our workflow. We then add nodes named "agent" and "action", which run the run_agent and execute_tools functions, respectively. Setting the entry point to "agent" establishes where the workflow starts.

We add conditional edges to determine the flow based on the outcomes of the agent’s actions. The function should_continue decides whether to continue to the "action" node or to end the workflow.

Finally, we add an edge to loop back from the "action" node to the "agent" node, allowing the process to repeat as necessary.

Running the application

Finally, we run the application and stream the outputs.

inputs = {"input": "what is the weather in sf", "chat_history": []}
for s in app.stream(inputs):
print(list(s.values())[0])
print("----")

How It Fits Together

By defining these functions, we enable our agent to:

  • Perform Complex Tasks: The run_agent function handles decision-making, leveraging language models to process and respond to inputs.
  • Interact with APIs: The execute_tools function ensures the agent can execute specific actions using predefined tools, logging the results for further reference.

This structured approach allows for efficient and maintainable code, with each component of the agent’s operation clearly defined and managed.

By integrating LangChain and LangGraph, we create a robust framework where the agent’s state and actions are seamlessly orchestrated, providing a reliable and scalable solution for building intelligent agents with automated workflows.

Supporting code on GitHub

You can find the supporting complete code in the GitHub repository. This demonstrates the processes outlined above for creating an intelligent agent with LangGraph. This includes:

  • The langgraph_agent.py script
  • A requirements.txt file
  • A README.md file to guide you through this process

--

--