In the last lesson, you built an agent from scratch. Now let's implement the agent using the LangGraph. And along the way, introduce some of its components and features. All right. Let's get coding. All right, so let's break down what exactly we did in the last lesson. We had this user message that came in. We then had the system prompt. And this was this very long one. We then called an LLM along with that. This output something like what's right there, a thought and an action. Based on that, we then made a decision. We would either return or we would call a tool, and we put this all in a big loop. This query function. And we had these two tools that we would call "calculate an average dog weight." And if we called the tool we would get back an observation. And then we'd loop back in, to the prompt and would put that observation as a new message. So let's break this down into LangChain components. First, let's talk about prompts. So prompt templates allow reusable prompts. And what this basically means is that we can create something like this, right here, a little string with some formatted variables that we can replace. And so those formatted variables can come from user content. So we can have this prompt template that we format in different ways depending on the user content. If you want to see a bunch of examples of these prompt templates, you can actually see them in the LangChain hub. So here is one that's very similar to the agent prompt that we just used. So you can see here it says "answer the following question as best you can." You have access to the following tools. And here we don't hardcode the tools themselves. But we have this little variable. And the same with tool names down here. And then we have the input here. And this is the user question. And then the agent scratchpad. And these are all the actions it takes. And the observations. If you want to see more prompts you can go to the hub here. And you can see a bunch of prompts that people in the community have contributed. The next component in LangChain that we have is tools. So here's a Tavily tool. And this is the search that we will be using from here on out. You can see that we import it from the LangChain community package which contains hundreds of other tools. Maybe the biggest part of this application, the most code that we wrote, was this function that would loop. And that's represented by all these arrows, here. And that's one way to think about LangGraph. LangGraph helps you describe and orchestrate that control flow. Specifically, it allows you to create cyclic graphs, which is exactly what we have here. It also comes with built-in persistence. And this is really nice for having multiple conversations at the same time, or remembering previous iterations and actions. This persistence also enables really cool human in the loop features. If you look at these diagrams here, and these are all diagrams of agents from academic papers, these are all represented as graphs. And that realization was what led us to create LangGraph, an extension of LangChain specifically aimed at agent and multi-agent flows. Crucially, it allows for really controlled flows. So in these diagrams there's really specific arrows leading from one box or state to the next. We've seen that this controllability is crucial for creating agents that can perform well. Three of the core concepts of LangGraph are nodes, edges, and conditional edges. Nodes are agents or functions. Edges connect these nodes, and then conditional edges are used when you need to make decisions about which node you should go to next. So let's take a look at an example of a LangGraph you can create that is equivalent to the function that we wrote in the lesson before. We can have an agent node. This is the LLM. We can then have a conditional edge which takes the result of that LLM call and decides what to do next. One of those edges can be an action edge which calls a function node, and that automatically loops back to the agent node. There's an entry point, which is where you start, and then there's the end node, which is the other action available to take after the agent. One of the most important things to understand when working with LangGraph is the state that is tracked over time. This is often called the agent state. This is accessible at all parts of the graph at each node in each edge. It is local to the graph and it can be stored in persistence layer, meaning that you can resume with that state at any point in time, later on. Looking at two examples, we first have a simple state where we just have this list of messages. Taking a closer look, we can see that this message variable is a sequence of base message. Base Message is a LangChain type. It's annotated with operator.add. This means that when the state is updated with new messages, it doesn't override the existing messages, but rather it adds them to that state. Let's take a look at a more complex state. Here we have input, chat history, agent outcome, and intermediate steps. They all have their own different type. Three of them are not annotated in any way. This means that when a new update is pushed to that variable, it overrides the existing value there. However, intermediate steps is annotated. It's annotated with operators.add, which means that when a new update is pushed there, it adds to it. This makes sense because intermediate steps is what is tracked as the agent action and agent observation throughout the graph as it executes. And so we want to continuously add to that as more and more actions are taken. Getting really specific about the previous agent that we created and how that would map to a LangGraph object. We can see that we will have one node. We'll call this call OpenAI. And this will call the LLM. We'll then have a conditional edge which will check for the existence of an action to take. And so we'll call this 'exists action'. And then we'll have another node which actually executes that action. We'll call that 'take action'. And the state that will track is pretty simple. It's just this list of messages to which we add over time. With that high-level overview. Let's dive into the code. Let's first load in any environment variables that we're going to need to use. This is our OpenAI API key. We're now going to import a bunch of things that we need to create these tools. Agent state. And the LangGraph itself. First, let's import state graph and this end node from LangGraph. We'll see how we'll use those later on. These typing imports and operator are used to construct the agent state. We then have all these different message types. These are LangChain message types that we'll use to represent the human in AI and system messages. From LangChain OpenAI, we're going to import chat OpenAI. This is a LangChain wrapper around the OpenAI API. This is exposes a standard interface for all language models, meaning that even though we're going to use chat OpenAI for this lesson, we can actually switch it out for any of the language model providers that LangChain supports without having to change any other lines of code. Finally, we're going to import Tavily, the search engine that we're going to start using as the tool. In this example. Let's first take a look at the tool. We'll create the tool by initializing Tavily search results with max results equals two. This means that will only get back two responses from the search API. We can see that it's of this type and it's got a specific name to tavily_search_results_json. This is the name that the language model will use to call this tool. Let's now create the agent state. This is exactly what was in the presentation. It's just an annotated list of messages that we will add to over time. Let's now create our agent. As discussed, we're going to need three functions. We're going to need one function to call OpenAI. One function to check whether there is an action present and another function to take that action. These will all be different methods on this agent class. So let's create those first. And we can then see how they interact with each other. Let's now create the agent class. We're going to want this agent to be parametrized by three different things. A model to use, the tools to call, and the system message as before. Let's save the system message as an attribute. We're now going to start creating the graph. First, let's initialize state graph with the agent state. Right now this graph is pretty bare. It doesn't have any nodes or edges attached to it. We know that we're going to want to create those three functions before and use them two as nodes and one as edges. So let's sketch it out and then we'll implement those functions as methods on the class. So first we know that we're going to want to create a node called LLM which will execute the LLM. And we can do that by doing graph, add node, LLM. And then we're going to come back here. And we're going to need to pass in the function that we want to represent this node. We'll add another node, the action node. We'll do the same thing here. Now we're going to add that conditional edge. This goes after the LLM is called. It checks whether there's an action present. If there is then it goes to the action node. If there isn't then it goes to the end node and it finishes. So let's add this again. We'll have to add the specific functions later on. The first argument we pass in is the node where the edge starts. This is from LLM. The second argument we pass in is the function that'll determine where to go after that. We're going to implement this later on so we'll leave this blank. The third and final argument that we're going to pass in, is a dictionary representing how to map the response of the function to the next node to go to. So if the function returns true then we're going to go to the action node. If it returns false then we're going to go to the end node. We're now going to add a regular edge. This is going to go from the action node to the LLM node. So we can do this by calling Add edge. The first argument is the start of the edge. The second argument is the end of the edge. We're then going to set an entry point for the graph. This is at the LLM node. With that done, we can compile the graph. Graph.compile is just what we need to call after we've done all the setups and we'll turn it into a LangChain runnable. A LangChain runnable exposes a standard interface for calling and invoking this graph. Well, we'll see more about that later on. We'll save this as an attribute on the class. We'll also save the tools and the model that we passed in. For the tools, we'll create a dictionary mapping the name of the tool to the tool itself. For the model, we'll actually call bind tools on the model passing in the list of tools that we passed into the agent. What this is doing is this is letting the model know that it has these tools available to call. All right. So we've created this graph. But we still need to create three things. We need to create the function representing the LLM node. We need to create another function representing the action node. And we need to create a third function representing this conditional edge. We'll add all of these as methods on this agent class. For the LLM node, we're just going to create a function called call OpenAI. It's going to take in this state this agent state all the nodes and the edges. We'll take this. Then what we'll do is we'll get the list of messages from the state. We'll then add in the system message and we'll then call the model. We'll then return this dictionary with a list of messages. But there's only one message in it. It's the message that's returned from the model. Again, remember because we had annotated the messages attribute on the agent state with the operators.add, this isn't overwriting this. It's adding to that state. We can then go back to the graph.add node and change that to pass in this method. Let's now do the same for the action node. The action node will also take in this agent state. We'll get the last message from the list of messages. We know that if we've gotten to this state, the language model must have wanted to call some tools. That means that there will be this tool calls attribute present on the last message in the agent state. Crucially, this can actually be a list of tool calls. So, a lot of the more modern models support parallel tool or parallel function calling. What we can now do is we can loop over these tool calls. We can find the relevant tool by looking up tool name in the dictionary of tools that we created. We can then call invoke on it passing in the arguments that we have from this tool call. We can then append this as a tool message to this results list. And then we're returning these messages mapping to these results thing. Again, this is just the new messages that we need to add. What's happening under the hood is that LangGraph is adding that to the state in between iterations. Let's go back up and let's put this here, in the action node. Finally, we need to define our conditional edge. So, this is what's going to take in the result after the LLM is called. And return a boolean either true or false, representing whether we should take an action or whether we shouldn't. In order to do that, we're just going to get the last message from the state, which is the most recent call from the language model, and we're going to return length of that message dot tool calls greater than zero. So basically if there's any tool calls we're going to return "True". If there's none, then we're going to return "false". Let's set this as the conditional edge. And with that we have our agent. So let's start to use it. We create a nice little system prompt here. We initialize chat OpenAI. So this is the language model we're going to use. And for our list of tools that we're passing in, we're just passing in our one, our Tavily search tool. One of the cool things we can do is we can actually visualize this graph we just created. So if we get the graph and then called docket graph and then .draw_png, we actually get an amazing visualization of the graph itself. This is all done automatically. So let's now call this agent. So let's first call it with this: "What is the weather in SF?" Question. In order to do this, we're going to create this human message representing a user message. We're then going to put it in this list of messages. And note that we have to do this because the state that the agent expects to work with has this messages attribute, which is a list of messages. So we need to make it conform with that state. Once we have this input, we can call the agent.graph.invoke with this and we can get back a result. So we added some print statements in the agent. And so we can see that it printed out what it was calling and it was calling Tavily search, results, json. And it was calling it with current weather In SF, San Francisco. And then it prints out back to the model. This means we're going back to the model. If we look at result. If we look at result we can see that it's a list of messages. So result, is the final state that the agent ended up in. If we just want to get the final message in this list of messages, we can go into the messages attribute. Get the last one and look at the content attribute. And we can see that the current weather in San Francisco is partly cloudy, blah blah blah. Let's now try this with a more complicated question. So "What is the weather in SF and LA?" We can see that it first calls Tavily search results json. With the current weather in San Francisco. Then before even going back to the model, it calls Tavily search results json again, with current weather in Los Angeles. This is an example of parallel function or tool calling. Finally, it goes back to the model and after that, it has all the information, it needs. So it responds. And if we look at the final message. We can see that we get back a response for both San Francisco and Los Angeles. Let's try an even more complicated question. "Who won the Super Bowl in 2024?" "What is the GDP of that state?" We can see first, it calls Tavily with the query 2024 Super Bowl winner and then goes back to the model. It then calls Tavily again, this time with the query "GDP of Missouri 2023." Then goes back to the model. And finally, it's done. And if we look at the final response, we can see that we get. "The Kansas City Chiefs won the Super Bowl in 2024 for the GDP of Missouri, where the Kansas City Chiefs are based, was 400 something billion. And the third quarter of 2023." So the difference here is that in order to make the second query, it actually had to know the result of the first query. So it calls one tool, then goes back to the model in order to enable it to call the second tool. This is a little bit different from parallel function calling. And it's not happening in parallel. It's happening sequentially. And the reason it's happening sequentially, is it actually needs the result of the first query, in order to make the second query at all. With this, we've seen how we can take that raw LLM and raw Python example with some fake tools and turn it into a real agent that can answer complex questions. Under the hood, it's using the Tavily search API. In the next lesson, we'll learn a lot more about that.