LangGraph Complete Course for Beginners – Complex AI Agents with Python
Disclaimer: The transcript on this page is for the YouTube video titled "LangGraph Complete Course for Beginners – Complex AI Agents with Python" from "freeCodeCamp.org". All rights to the original content belong to their respective owners. This transcript is provided for educational, research, and informational purposes only. This website is not affiliated with or endorsed by the original content creators or platforms.
Watch the original video here: https://www.youtube.com/watch?v=jGg_1h0qzaM
Welcome to this video course on LangGraph, the powerful Python library for building advanced conversational AI workflows. In this course, Vava will teach you how to design, implement, and manage complex dialogue systems using a graph-based approach. By the end, you'll be equipped to build robust, scalable, conversational applications that leverage the full potential of large language models.
Hey guys, my name is Vava and I'm a robotics and AI student. In this course, we're going to be learning all about the fundamentals of LangGraph. Now, I assume you've heard of LangGraph before, hence why you clicked on this course. But I'm also going to assume you have never coded in LangGraph before. Now, because of this assumption, I have explained every single thing in as much detail as I possibly can. Now, also this might mean that I might be going slow at times. So if you want, you can always speed me up.
Now what are we going to be learning in this course? Well to start, we're going to be building a lot of graphs, a lot of AI agents. We're going to be learning a lot about the theory and I've also provided exercises throughout the course in which all of the answers will be provided on the GitHub. With that being said, if you're ready to start on this journey with me, let's go to our first section then.
All right people. So welcome to the first section of this course. Now in this section, we'll be covering something called as type annotations. Now admittedly, this is going to be a completely theoretical section, but it will be short and brief. I promise. The reason I've kept this specific section in the course is because when we do eventually go on to code our AI agents, our graphs in LangGraph, these will start popping up everywhere. And I don't really want you to look start coding without ever having seen these before or really not knowing what these actually are. So that's why I've kept it here. But I promise this will be short and brief. Cool. Okay.
Let's begin with dictionaries. Now dictionaries are a data structure, yes, but there's a reason I've kept it here. So let's see how a dictionary is described in Python. You should already know this. So in this case, I've described a very simple dictionary called movie. and it has two keys, the name and the year. And it has two values, Avengers: Endgame and 2019. Now, dictionaries are awesome, don't get me wrong. They allow for efficient data retrieval based on their unique keys. They're flexible and easy to implement, but there's a potential problem with them.
See, it's a challenge to ensure that the data is a particular structure. And this could be a huge problem in larger projects. So to put things in simple words, it doesn't really check if the data is the correct type, data type or structure, and that could be the source of a lot of logical errors in your project. And if your project is really, really large, then this could be quite a headache to identify, right? Cuz it's quite a small detail.
So what is the solution for this? Well, it's something called a TypedDict. Now, here is an example on how you create a TypedDict in Python. And I just want to emphasize that this type annotation is used extensively in LangGraph. This will be used to define states. Now don't worry, we haven't covered states yet. We will cover that in the next section. But just be mindful that this is quite important.
So a TypedDict is quite easy to implement. You implement it as a class. In this case, I've implemented the same example I showed you in the previous section where I described the movie is the same exact keys and values. So, it still has the name and the year. But notice in this class, I have defined the actual data type of what that key should be. So, for example, the name is a string and the year is an integer, right? And to initialize a dictionary, I have done the exact same thing to have Avengers: Endgame and 2019.
So now there are two main benefits of using a TypedDict. It's type safety, because we've explicitly defined what should be in this data structure and so this will really reduce the runtime errors. And obviously the readability is enhanced as well and this will make debugging easier if something goes wrong within this TypedDict. Cool.
So we've covered TypedDict now. Now we move on to another type of annotation which is Union. Now you might have seen these future these later type annotations before if you've coded in Python but again, I'm just giving you a high-level overview what these are. So Union, take a look at this example. So I've created a very simple function which takes in a value and it squares it. Now in this case, the input x could be either an integer or float and Union basically says that whatever value you have can be these data types only. So in this case, x can only be integer or float. So if I pass in five or 1.234, this would be completely fine. It would square the number and everything. But if I passed in a string like "I am a string," it would completely fail.
Now admittedly, yes, this function is quite easy. If I passed in "I'm a string," it would have failed anyway. But in more complicated applications, hopefully you can see how this actually is useful. In fact, the makers of LangChain and LangGraph used Union quite extensively throughout making the actual library. So again, it's flexible and it's easy to code and it allows for type safety because it can provide hints to help catch incorrect usage.
Now something similar to Union is another type annotation, which is Optional. Now Optional is quite similar and in this case, I've described another function nice_message. So you pass in a name. If you pass in a name it will say, "Hi there, name." So for example, let the name be Bob. If I pass in Bob to this function it would say, "Hi there, Bob." But what if I don't pass in anything? Now if I don't pass anything, Optional, because I've used Optional, says that the name parameter could either be a string or a None value. Now if I pass in nothing it will go in this if statement and say, "Hey random person." But this is also important to emphasize that it cannot be anything else. It can't be an integer or a boolean or a float or anything like that. It has to be either a string or a None value because that's what I've defined here. Cool.
Now comes another type annotation called Any. And Any is really the easiest one to understand. It literally means this value could be anything. It could be any data structure. So in this case, I've created a simple um function called print_value where it takes in something and it prints that. And for example I passed in this string and it prints it and anything and everything is allowed. Cool.
One last type annotation I promise, and it's the lambda function. So lambda functions are quite useful. For example, in this I'll give you two examples now. So the first example is this really simple example. Now we've already, I already created a square function before, right? Where it takes in a value, it takes in a number and it squares it. So for example, if I passed in square(10), it would give me 100. Quite an easy example.
Now let me give you a second example. This. So if you've come from a LeetCode background, then you've probably seen, you've either used lambda before and you've definitely used map before because it's quite efficient. So for example, if I pass in 1, 2, 3, 4, what this piece of code is saying is that it squares each number in nums. So this map function maps each value and performs this function to it. So x * x. So 1, 4, 9, 16 and then converts that back into a list. Now lambda functions really are just a shortcut to writing small functions and they make everything quite efficient. Now obviously this could have been done in one line as well but for example this a beginner programmer could have might have used a for loop but a more advanced programmer could have used this and this is obviously much more efficient. Right?
So hopefully you can start to see how powerful these type annotations are and these will be coming up. So again, no need to memorize this. Just need to have a high-level overview what they are. Okay, cool. So now I'll see you in the next section. See you there.
All right, perfect. So let's continue on. In this section, we will look at the different elements in LangGraph. So let's begin with our first element, one of the most fundamental elements in all of LangGraph, the State. So what is a State? Well, it's a shared data structure that holds the current information or context of the entire application. In simpler terms, it is like the application's memory where it keeps track of the variables, the data that nodes can access and modify as they execute.
Now, don't worry if you don't understand what a node is yet. That is what we will be talking in the next slide about. But as a good analogy, think of the whiteboard in a meeting room analogy. Now imagine you're in a meeting room and there are different participants as well, and every time you come up with something new or you want to record some new information or update some information you write it on the whiteboard. In this case the whiteboard acts as your state and the participants act as a node. So the state shows us the updated content/information of your entire application. Hopefully that made a bit of sense.
So let's move on to the Node, another fundamental element in LangGraph. So these are just individual functions or operations that perform specific tasks within the graph. So each of these node receives an input, which is often just the current state of your application. It processes it and then produces an output or an updated state.
So here's a good analogy of this. The assembly line station analogy. Now look at this image. Each of these station does one specific job. It could be attaching a part. It could be painting it. It could be inspecting the quality and so on and so on. The point is each of these stations represent a node because they do one specific task.
So how do you actually connect these different nodes together? Well, before we go into that, I think it's important we understand the most important element of them all, the Graph. It is so important that it's even in the name LangGraph. So, the graph is just the overarching structure and it maps out how different tasks, aka nodes, are connected and executed. So it visually represents the workflow, showing the sequence and the conditional parts between various operations. Now a graph is quite self-explanatory but you can think of it as a road map. On a road map you can see it display the different routes connecting cities with the different intersections offering choices on which path to take next.
Now, here's a great image of what a graph is, and these are the individual nodes, but you'll see they're connected somehow. So, how are these connected? That brings us to the next element, Edges. So, edges are just the connection between nodes and these determine the flow of execution. So, they tell us or tell the application which node should be executed next after the current one completes its task.
A really good analogy of this is imagining a train track. So this is the train track and think of it as an edge and think of it as connecting two stations, one here and one here, which represent nodes together in a specific direction. Now the train which will go on the train track, that acts as your state. So the state gets updated from one station to another.
But there is another type of an edge and it's called a Conditional Edge. So this is still not very complicated. It's quite simple to understand. These are just specialized connections that decide the next node to be executed based on the specific condition or logic applied to the current state. Now a really good analogy for this is the traffic light analogy. So green could mean to go one way, red could mean to stop. yellow could mean to slow down. The point I'm trying to make here is that the condition, in this case the light color, it decides the next step. If you want to think even more simply, you could think about an if-else statement.
So that being said, we move on to the next element, the Start Point. So the start point or the node, the start node is a virtual entry point in LangGraph and this marks where the workflow begins. Now it's important to note that it doesn't perform any operations itself but it serves as the designated starting position for the graph's execution. Now in terms of analogy it is quite simple to understand but if you really want, think of it as the starting line of a race.
Now if you have a start point, well you need an end point as well and that's where the End element comes in. So the end nodes just signifies the conclusion of the workflow in LangGraph. So when the application reaches this node, the graph's execution completely stops and it indicates that all intended processes have been completed. And again, a good analogy for this is just the finish line in a race. So nothing too hard yet.
But now let's look at Tools. So tools are specialized functions or utilities that nodes can utilize to perform specific tasks. For example, it could be fetching data from an API. They basically enhance the capabilities of these nodes by providing additional functionalities. Now, one common question could be, well, what's the difference between a tool and a node? The node is just the part of the graph structure. Whereas the tools, these guys are functionalities used within the nodes. Now, a really good analogy for this is just tools in a toolbox. So imagine a hammer for the nails, a screwdriver for the screws, etc. The point is each tool has a distinct purpose. Again, don't worry. You will understand the differentiation between tools and nodes in a lot more detail later when we code this, but this is just for a general overview.
Now, another question you could be asking is, is there a middleman between a tool and a node? Short answer is yes. That's where Tool Node comes in. So a Tool Node is just a special kind of a node whose main job is to run a tool. So for example, a Tool Node could be a node where its only job is to use a tool and that tool's job is to fetch some data from an API. So it connects the tool's output back into the state so other nodes can use that information.
So think about this analogy going back to the assembly line. In this case, imagine the operator as the Tool Node and it controls the machine which is the tool and then sends all of these results back into this assembly line.
Now if we progress further, let's look at the StateGraph. So this is quite an important element as well. This will be one of the first elements you actually interact with and its main purpose is to build and compile the graph structure. So it's quite important. It manages the nodes, the edges, the overall state and it makes sure that the workflow operates in a unified way and all of the data flows correctly between components. So again it's quite an important element. You can think about it as a blueprint of a building. So just as a blueprint outlines the design and the connections within a building, the StateGraph does exactly that, but it just defines the structure and the flow of your workflow or application.
Now here's where the Runnable comes in. Now some of you will be coming from a LangChain background and Runnable is quite common there and it's quite similar in LangGraph as well. A Runnable in LangGraph is just the standardized executable component that performs a specific task within an AI workflow. It basically acts as a fundamental building block allowing for us to create these modular systems.
Now a question you could have right now is, well, what's the difference between a Runnable and a node? Short answer is a Runnable can represent various operations, whereas a node in LangGraph typically receives a state, performs an action on them, and then updates the state. Now don't worry if you didn't 100% get that. When we go into the coding section you will get it a lot better. But a good analogy is a Lego brick. So just as how Lego bricks can be snapped together to build these complicated structures, Runnables can be combined to create sophisticated AI workflows.
So now let's move on to the different types of messages. Now again, if you come from a LangChain background, you'll be quite familiar with these. If you haven't, don't worry. We will look at the five most common message types in LangGraph. So to start off, there's the HumanMessage, which represents the input from a user. The AIMessage, which represents responses generated by AI models. The SystemMessage, which is used to provide instructions or context to the model. ToolMessage, which is similar to the FunctionMessage but specific to tool usage. And the FunctionMessage represents the tool of a function call. If you've used an API like a large language model API before, such as OpenAI's API, a lot of these will be quite familiar, especially the SystemMessage, the AIMessage, and the HumanMessage.
And that concludes this section. So, I will see you in the next section.
Awesome. So, now this is quite exciting. We're actually about to start coding in LangGraph for the very first time. Now that we've covered all the theory, admittedly the boring section, we're now actually going to code up some graphs. And we're about to code up our very first graph in this subsection.
But um for this overall section, I have a slight confession to make, which is we're not going to be building any AI agents in this section. Why? Because I thought that one, we haven't really even seen how to actually code in LangGraph, and combining all of these LLMs, APIs and tools and all of that stuff which comes with it, combining them together would be quite messy and it could be quite confusing at times. Especially the fact that we have never coded in LangGraph before. Again, like I said at the beginning of the course, this course is supposed to be beginner friendly, detailed and comprehensive, and we're going to go in steps like little by little. So hopefully you understand.
But don't worry, we will be coding AI agents soon. We're just going to be building a couple of graphs right now, understand LangGraph better, the syntax better, and how to actually code up graphs and get confident with it. And then we will actually build AI agents. Okay cool.
So what is the graph which we're going to be building together in this section? I call it the hello world graph, mainly because it's the most basic form of graph we can actually code in LangGraph. So the objectives are these. So we're going to be understanding and defining the agent state structure, and don't worry, you'll understand what that is in a few minutes. And we're going to be creating simple node functions, nodes like we discussed in the previous section, and we're going to be processing them and updating the state. We're going to be building the first ever basic LangGraph structure and we will understand how to compile it, invoke it, process it, everything. And really the main goal of this section is to really understand how data flows through a single node in LangGraph.
Now just to give you a bit of a heads up as to what we'll actually be covering, what we're going to be building I should say, is this graph. Again, like I said, this is the most basic form of graph you can build in LangGraph. It has a start point and an end point and this node sandwiched in between them. All right, cool. So hopefully you've understood what the objectives are. It's quite basic and yeah, I'll see you at the code.
Okay, cool. Now let's actually code this very first graph. So I've imported three main things here. The dict, the TypedDict and the StateGraph. The dict and TypedDict is obviously dictionary and TypedDict, but um and StateGraph. These three are elements which we covered in the previous section. So I would highly recommend you going back there if these are completely unfamiliar. But again you don't need to memorize what these are.
Okay. But just to refresh your memory, I've written in the comment here what the StateGraph is. So think of the StateGraph as a framework that helps you design and manage the flow of the tasks in your application. Um again that might sound a bit complicated but it's not. Once we actually start coding you will it'll make more sense.
So now the first thing we're going to do after importing everything is create the state of our agent and let's call it AgentState. And just to refresh your memory again what the state is. Think of the state as a shared data structure. And this keeps track of all of the information as the application runs. All right cool.
So now let's build the agent state. And the way we do this in LangGraph is through a class. So let's build class AgentState and in these parenthesis, the state needs to be in the form of a TypedDict. So that's why we specify TypedDict here. Now let's keep this very, very fundamental and basic. Let's just pass in one input. Let's call it something like message and obviously we put colon and we specify the data type of that attribute. Now obviously the data type of message will be string, right, so that's why we specify str. Again this is just normal Python.
So once we've done that we are now going to be coding our very first node, again another very fundamental element in LangGraph. So how do we actually define a node? It's quite simple. It's just a normal standard python function and this is how you do it. So let's say, let's first try to find the objective. Let's say we are trying to, let's a greeting message, a simple greeting message. So we'll write def greeting_node and we need to pass in an input and pass what the output type should be.
Now the input type of a node needs to be the state and the output type also has to be the state because remember the state keeps track of all of the information in your application, right? So obviously you need to pass that as an input and you need to pass out the or return the updated state. So here's how you do it. You pass in state and what is the state of our application? Well, it's the AgentState which we defined earlier, right? And the output is going to be AgentState cuz we need to output the updated state. And our updated state will again just be the AgentState once we've done all of the um all of the mechanics we do in this function, the actions we perform in this function.
All right. Okay. So now we need to do something very, very important and it gets annoying sometimes but um it's really a key habit which I want you to form and it is docstrings. Now docstrings in LangGraph is quite important. Why? Because docstrings is what will tell your AI agents, when we actually build the AI agents, your LLMs what that function actually does, what that function's actions are, what it performs. So in this case, uh by the way to create a docstring is just three quotation marks, three pairs of quotation marks. Uh let's call the docstring in this case, let's just write "simple node that adds a greeting message to the state." Perfect.
So now how do we actually refer to this message? Well again this is just normal Python code. So we will pass in state and we will type in message. Now this specific part allows us to actually update the state or the message part of the state. And let's say let's come up with something like "Hey " plus state['message']. Um we can also add something like ", how is your day going?" something basic.
Now what's the last thing which I need to do in this function? Think about it. Okay. So now remember in a few moments ago I said we have to return the state or the updated state. Well the updated state we've already done, we've just manipulated the state here. So all we have to do is just simply return the state. Cool. And yeah that runs without any errors.
Okay. Now let's actually build the graph, which is again obviously very important. So how do we build the graph? Remember here I said StateGraph is a framework that helps us design and manage the flow of tasks as a graph. Well that's exactly what we're about to do now. So hopefully it clicks now.
So to create a graph in LangGraph, you use the StateGraph attribute and you pass in your state. You can see the state schema which VS Code has asked for what the description of what the parameters are. So our state schema in this case is just the AgentState which we defined, right? So we pass an AgentState. I will actually also write here our state schema so you can physically see what it is. Okay. And let's store this in a variable called graph or something.
Okay. Now here comes a very important method. How do we actually add a node to this graph? Cuz this graph is completely like nothing right now. So to add a node we use the inbuilt function graph.add_node and it requires two main parameters. Now what VS Code is suggesting is a God, I don't even know what that all of all of that is, right? It's very confusing. So to put things simply, you require really two parameters: the name of your node and what action it will perform.
So let's go with the name. The name could be absolutely anything sensible of course. Um let's call something like "greeter." Cool. And you can see VS Code has also asked us to um input an action. Now what's the action going to be? Well, the action will just be whatever your node will actually perform. And what action or mechanics will this node actually perform? Well, all of that is defined by this function, right? The greeting_node function. So we simply just put that, the name of the greeting node function here, and that's it. We've successfully added the greeting node to our function, to our graph, and it will be named as "greeter."
So remember this diagram. In this diagram, there is supposed to be a start and an end point. We've done the node which is sandwiched in between these but we haven't really added the start and the end point yet. So, how do we do that? Well, there's actually multiple ways to do that. In this subsection, in this graph, I'm going to teach you one way. Further down the line, I'll teach you another way. So, but they're both, they're quite easy.
So, you simply just call the inbuilt function set_entry_point and as the parameter is just one parameter which is the key. Now, the key is the name of your node which you want the start node to connect to. Again, visualize it. The start, the start point is here and the node is here. Obviously you need to reference a node for it to create like an edge, right? So we simply pass "greeter" and similarly graph.set_finish_point. We will again pass "greeter" here as well. Why? Because imagine again the node is here and your finish point is here and you need to connect some sort of connection between these two, right? And that's why we use uh "greeter" in this case.
Don't worry, you will solidify this once you complete the exercises and as we go down building more graphs. All right. And one last thing which we need to do is actually compile this graph. So graph.compile using the inbuilt uh graph using the inbuilt compile function. And let's just store this in a variable. Cool.
So that run without any errors. But just a word of caution here. Just because the graph compiles without any error doesn't mean it will successfully run. I mean, God knows once we build like more complicated graphs, there could be so many logical errors. So, that's just an important thing to know. So, don't get too happy once it compiles cuz there might be logical errors. Trust me, I know. Okay.
So, I want to write some code which will actually help you visualize this. And you can use the IPython library. So, you can use this, this piece of code here. This code is awfully familiar with the first ever graph I showed you, right? I'll put a picture somewhere here for you to compare. The only difference is really the name of the node which we've set. In this case, it's greeter. Why is it greeter? Because that's the name we gave to this node, right? Cool.
So that's looks pretty good. Let's actually run this. So to run, you use the inbuilt method invoke. Um so let's pass in the message as something like "Bob" or something and let's actually store this result in a variable. Okay. Now how can we actually specify, how can we actually get the value of result? So result, we need to actually reference a certain attribute. Now the only attribute we have in the entire graph is message, right? So we simply just put message and perfect, you we get the final answer which is "Hey Bob, how's your day going?"
Now why is it like this? Because this is exactly how we set our, how we set our function to be, what action it performs. It says "Hey," then concatenates the input message, in this case it's just the name, and it says ", how's your day going?". Now I could have changed this to absolutely anything else, right? What goes here like these functions are almost endless, but that's the whole flow of how everything works. So hopefully you understood how to build this very first hello world graph. It's quite simple. But um don't worry if you didn't fully 100% understand this. I'm now going to show you what exercise you need to complete uh to be able to solidify this. All right. All right. I'll see you at the exercise.
Okay. So time for your very first exercise. So the exercise for this graph is quite similar to what we just did, but I want you to create a personalized compliment agent. So you should pass in your name as like something like Bob or something and then output something like, "Bob, you're doing an amazing job learning LangGraph." And to give you a hint as to what you need to do again, you again have to concatenate the state, not replace it. All right, it's very similar to what we just did and it's quite basic. You should be able to do this, but um this is really just to get your hands dirty. All right. Okay. Once you've completed this exercise, join me when we build the second graph. I'll see you there.
Okay. So now we're about to build our second graph as you can see here. And it's again quite similar to the first graph we built, except now we're going to be able to pass multiple inputs as you can see here. So again, what are the objectives which you will be learning in this? Well, we're going to build a more complicated agent state. Uh, and we're going to be creating a processing node that performs operations on list data. So now we're about to see how we can really work with different data types apart from just string. And we're going to set up the entire graph that processes and outputs these and computes these results. And we're going to be able to invoke the graph with the structured inputs and retrieve the outputs. But the main goal which I want you to be able to learn in this specific subsection is really how to handle multiple inputs. All right. Okay. Let's code this.
Okay. So now let's actually code the second graph up, the second application up. So again I've just imported the same things again, the TypedDict and the StateGraph. And I've also imported the list this time. But list is just a simple data structure which you should know already. So if you remember from the previous graph we made, we are supposed to uh implement the state schema first, right? So how do we do that? Again, we use the class AgentState, TypedDict.
Okay, before I continue, just a heads up, I could have named the state schema anything I want. I could have named it something arbitrary completely like a bottle for example. In this case I've just said AgentState because one, that's how I learned it. It's like a habit for me now. But it also really tells you what it actually is. It's the state of your agent, right? So that's why I've just kept it like that. But again, just a heads up, you could have named this whatever you want. Cool.
Okay. So now let's, if you remember the main goal for this graph, for this uh building this graph was to be able to handle and process multiple different inputs, right? So how do we actually assign and I really do that? Well, the answer is in the state which is here's what uh which is what we're about to do now. So you really, cuz remember this is just a TypedDict. So you basically have multiple keys now you uh create that. So let's say something like values, list of integers. So let's say one of our input is a list of integers and let's also pass in a name which will obviously be in a string and let's have the result in a string. Something completely random. But now you can see we're now operating on two different types of data structures, a list of integers and a string. And we're handling three different uh different uh inputs: values, name, result. Okay cool.
So let's run this. Perfect. So now let's actually build our node because in again in this uh graph we're just going to have a single node to keep things easy. Remember step-by-step. So let's call, let's write def process_values and again, what was, what needs to be here? Yeah. So we need to pass in the state and we need to return the updated state. So how do we do that? Well, we write state: AgentState and we pass out the AgentState. Cool.
Now, again, building healthy habits. I know it's annoying. We have to write the docstring. So, let's just write something like this function processes, handles multiple different value in multiple different inputs. Cool. Again, I'm not being super specific here because one, I don't want to spend too long on writing doctrines and everything, and two, there's no AI or LLM here, right? So that's why it doesn't really matter. I'm just doing this to build healthy habits.
Okay, so now let's do something like whatever values we pass, the list of integers, let's sum them up. And let's also concatenate the name as well and store it in the result. Sound cool? Okay, so how do we do that? We pass in state['result'] cuz that's what we are, the action we're performing is on result, the attribute result. And let's say something like, "Hi there" and then we refer to the name, um cool, "and your sum is equal to" and let's just use the inbuilt Python function sum and we pass state['values']. Cool and lastly we obviously return the state. Okay, perfect. And that's that done.
Okay, so now we actually create the graph. Again, this is going to be very, very similar to what we did in the previous section because again there's just a node, there's a start point and an endpoint. So like last time, we use the StateGraph to initialize a graph and we pass in our state schema. So AgentState and let's store this in the variable graph. Okay.
Uh let's add our node. So graph.add_node and again remember it requires two parameters. It requires the name and the action. So in this case the name will be let's call it "processor" for example. Again this could be anything you want, and your action will be performed by this function, right, process_values. So we can just add that.
Okay. Now I've already told you how to, how to initialize a start point and an end point and this is just given by that code. So you attach your entry point to your node. In this case it's just one node, which is the processor node, and again same goes with finish and you compile it using graph.compile. Perfect.
So take a moment now. How do you think this graph will look like? That again like I said very, very similar on how the graph actually looks like but the only difference now is the name of the uh node which we've kept this as "processor."
Okay. So now let's actually test this. Let's actually invoke this graph. So how do we do that? Well, we use the invoke function. Now here's another important part which is quite a common mistake, especially like I have done this many times. Make sure to store your compiled graph in a variable cuz if you invoke the graph i.e. if you write something like graph.invoke that won't make sense cuz you haven't compiled the graph. That's why you need to invoke using app. That's why I've also done app here. If I did graph.get_graph, oh, it's completely messed up. Right? It says StateGraph object has no attribute because your graph hasn't been compiled yet. That's why when I do app.get_graph, the process works. Cool.
So now let's again store this in uh let's store something like answers is equal to app.invoke. Cool. Let's pass in some values. Let's say something like values and let's have a list of integers. 1, 2, 3, 4. Again, I'm just trying to prove a point. I'm not trying to make a very complicated um graph yet. And let's pass the name as something like Steve something. Okay. Uh cool. And let's print, let's print answers. Let's see what happens.
Perfect. So now you can see your values is 1, 2, 3, 4. Your name is Steve. And your result is, "Hi there Steve. Your sum is equal to 10." Again, why? because that's exactly what we uh asked the node, the action to perform. "Hi there, your name" which in this case is Steve. "Your sum is equal to the sum of the values" and 1 + 2 + 3 + 4 is 10. Right? And that's how you get this answer.
Now what if I wanted to just access result? I didn't want any of this other nonsense. Well to do that you can again just specify result and you will get it in a more clean manner. Cool. Okay.
Now I want to try one more thing just to build your understanding a bit more. Uh let's put some print statements here. So let's have a print(state) here. Then we perform the action and then we print the state here. This is really just to show you how the state gets updated and it should be easy, like interpretable cuz this is quite a basic piece of code. Again, print state before the action and print state after. So there cool.
And here you go. So values is equal to 1, 2, 3, 4, name is equal to Steve and these are the inputs we passed. Now notice I didn't pass results as an input as well. I could have uh done that but LangGraph automatically sets that as like a a None value in this case if you don't pass an input.
Now here's where you need to be cautious. If I had actually used state['result'] here as well to uh update state['result'] like I used state['result'] to update either itself or something else then you would run into a problem because your state['result'] has been initialized as None because you didn't pass it as an input. So be mindful of that. But in this case it worked because we're only assigning state['result']. We're not using it to assign something. It's getting assigned. Cool.
And you can see after the action has been performed, uh your operation has been performed and the thing has been concatenated. You can see result is here and that was exactly what we were getting before we cleaned this up. Cool. So hopefully you understood that. Again it should have been quite intuitive and interpretable but um to solidify your understanding even more complete the exercise. So I'll see you at the exercise then.
Okay. Welcome to the exercise, your second ever exercise. And for this exercise, I want you to create a graph which passes in a single list of integers along with a name and uh an operation this time. And if the operation is a plus, you add the elements. And if a, well, times, you multiply all the elements all within the same node. So don't create an extra node yet.
So for example your input should could be Jack Sparrow, your values 1, 2, 3, 4 again and then your operation uh multiplication and your output should be in the format of, "Hi Jack Sparrow your answer is 24." So just to give you a hint as to how you would perform something like this, uh you would need an if statement in your node, so slightly more complicated but the whole concept is the same. So once you've completed this exercise I will see you in when where we build this third graph All right, see you there.
Okay, welcome to your third graph. So, what are we going to do this time? Well, enough processing multiple values and everything. Let's actually get the graph more complicated. So, that's why we're going to be building a sequential graph. So, all it, all that basically means is we're going to be creating and handling multiple nodes that can sequentially process and update different parts of the state. So we will learn how to connect nodes together in a graph through edges of course, and we're going to invoke the graph and really see how the state gets transformed as we uh progress through our graphs step by step. So again your main goal is should be to understand how to create and handle multiple nodes in LangGraph. Sounds cool. Okay I'll see you at the code.
Cool. So now we're about to code up the third graph. Uh, and we're making quite fast progress. So well done on that. So again, the imports are the same. StateGraph and TypedDict. Perfect. And like we've done in the previous two graphs, we're going to be coding the uh the state schema or the agent state first. So let's have class AgentState. And again, it needs to be in the form of a TypedDict, right? And in this case, let's have the three attributes as all strings because we've already, we already know how to handle multiple data types, right? So, let's keep it simple. name: str, age: str, and final: str.
Okay. Now, here's what we're going to build. Now, we're about to build our two node functions, uh, which are again the actions. Okay. So again you simply write first, well I'll name it first_node in this case and like I mentioned before we pass in the state and we return the updated state. Okay. So again healthy habits, docstring again. So this is the first node of our sequence. Okay. And what do we want to do in this specific node? Well, I really just want to manipulate uh the final part. So, let's say something like state['final'] is equal to state or let's have an f-string, f"Hi {state['name']}". Let's say something like " that." Cool. And we'll just return the state. Perfect.
And now again we create a new node. So state: AgentState. Return that. Perfect. And I'm just going to copy this docstring and just change it. This is the second node. Perfect. Okay. To speed things up. And in this case I also want to have state['final'] is equal to " You are {state['age']} years old." Again quite a simple example, easy to follow. That's why I've kept it as quite a basic graph. I mean it's not going to solve the world's problems or anything but it will help you understand.
There is one logical error which I've put deliberately here. I want you to try to identify it. Okay. So the logical error in this case is that once we've built our graph and everything, what would have happened is we would have said hi to whoever uh we pass in, let's say Charlie or something. So, "Hi Charlie." And we store that in the final uh attribute in the state, which is what we want.
But here's where things start to be logically incorrect. Once we finally get to our second node, again, we're updating state['final'], which you can do. You can repeat, you can interact with these attributes in any node possible in all of the nodes. And you can do it as many times as you want. But notice this part. What's happening here is we've completely replaced all of the content we had before. So remember how we had "Hi Charlie"? We've just completely replaced it with "You are age years old." But we want both of them, both of those stuff, right? So how do we get both of them?
Well, again we just concatenate them. So we can have something like state['final'] plus state file. And there we go. Logical error should be now solved, right? Cuz now we have concatenated state['final']. Uh we're essentially just like adding on to, uh we're preserving what we had before, right?
Okay. Now let's get to the fun part. How do we actually build this graph? And really it's quite similar to the previous two graphs except there is one new thing which you're about to learn. So like always we use StateGraph to start the framework. So AgentState and let's store it in graph. Again I could have had this name, the variable, into anything. I've just kept it graph cuz it makes intuitive sense.
Okay. Now we add our nodes. So we do graph.add_node. And for simplicity sake I'm just going to have the name as the same name as the uh function. Okay. So that way it'll just be easy to follow. So graph.add_node('first_node', first_node) and graph.add_node('second_node', second_node). Cool. Okay.
Now that we've added both nodes, we need to obviously add the entry point and the end point, right? So we set the entry point like this. Again, quite self-explanatory because we wanted it to connect to the first node, not the second node, right? So it should be start, first node, second node, end point.
How do we connect the first node and the second node together though? Hopefully you had an answerr for that. Uh if you remember or recall from the previous section, theory section, there was an element in LangGraph called the edge. That's exactly what we're about to do right now. We're about to use edge and that was the new thing which I was talking about a few moments ago which you're about to learn.
So how do we use it? Well you use graph.add_edge and if we can, perfect. So again it's quite simple, you use a start key and end key. So similar to entry point where, but in this case you need to pass two parameters. So the edge we want is between the first node and the second node right? Well that's exactly what we pass here. So first_node and second_node.
And like before we will just set the finish point at second_node and we will compile this. Now how will this graph look like? Take a moment to try to think of how it will look like. Like that. Start point, end point and these two nodes are sandwiched in between. But now there is a edge. It should be called a directed edge if I'm being like quite picky. But yes, a directed edge cuz the flow of data or your flow of your state updates is from the first node to your second node. Right?
Cool. So now that we've built that, let's again invoke this. So I've got this code ready here. Uh let's invoke it. Let's pass the parameter as Charlie and let's pass the age as 20. Cool. Print result. Perfect. Apart from the uh misalignment here which I can just change right now. Perfect. Okay. So now you can see it says, "Hi Charlie, you are 20 years old."
Now obviously we could have performed all of this in one single node which we have been doing in the previous subsection but the obviously the aim was to be able to create multiple nodes right and handle um the state how the state progresses. So yes, you, one important thing which you've learned is obviously how to use the add_edge method, but another concept which you have solidified here is you can change these, these keys of your state at, in at any point in time, like as long as, as however many times you want. Cuz remember here we've passed in state['final'], we implemented state['final'] here, we implemented state['final'] in the second node. If we had more nodes in the sequence. we could have done that again and again and again.
And we also learned how to like, one key logical error is sometimes a lot of people just accidentally replace their content in one of the attributes and that leads to a lot of logical errors. So always be mindful of that. And yeah, that again was quite simple, not too hard and hopefully the exercise which I'm about to give you solidifies this. Cool. So I will see you at the exercise then.
Awesome. So now we will move on to the exercise for this third graph. And what I want you to do is really build on top of what we just covered. Instead of two nodes, I want you to build three nodes. Again, in a sequence, don't need to go too fancy yet. We will, again, three nodes in a sequence. And we will have, you will need to accept the user's name, their age, and a list of their skills.
So the first node will be specifically for personalizing the name field with a greeting. The second node will be describing the user's age. The third node will be listing all of the user's skills in a formatted string. And then you'll need to combine this and uh store it in a result field and output that. And this should be a combined message. And the format I would like you to output is something like this. So let's say the name was Linda. And let's say, "Linda welcome to the system. You are 31 years old and you have skills in Python, machine learning and LangGraph." Okay.
And just as a hint for this exercise, I would, you'll need to use the add_edge method twice. So this will really solidify your understanding on how to build graphs in general. All right, cool. So once you've done that, again, answers will be on GitHub for all of the exercises. Once you have uh cross referenced and checked that you've done it right, I will see you in the next section where we build our fourth graph. All right, see you there.
Welcome, welcome, welcome. Okay, I'm particularly excited for uh teaching you this graph, graph 4. Why? Because we're about to learn how to build a conditional graph. So for the very first time, we're about to implement conditional logic. And obviously we've done it in a previous exercise before but that was within a single node. This is how to implement conditional logic in the overall graph structure. And so we will be implementing conditional logic to route the uh flow of data to different nodes. We will be using the start and the end nodes to manage entry and exit points. We will be designing again using multiple nodes to perform different operations such as addition and subtraction. And we will be able to create a router node to handle decision-making and control the graph flow. So the main goal is really to use this inbuilt function which uh allows you to create conditional edges in LangGraph. All right, exciting stuff. I'll see you at the code.
Okay, so let's actually code this up now and you'll see the imports are slightly modified this time. Again, TypedDict and StateGraph is there. But now I've also imported START and END point. Again, if you remember a few subsections ago, I told you there are multiple ways to be able to initialize the start and the end point. And this is another way you could. Arguably, this is the easier way, but um whatever. I don't really have a preference, but I'll teach you both ways regardless.
Okay, let's import these. Successful. Okay. like standard procedure we will design, we will um code up the uh the schema, the state schema. So class AgentState and let's again TypedDict. In this case uh uh I want to be able to pass in two numbers and pass in an operation. So a plus operation and a minus operation, one of those two operations. Now obviously I could have handled all of this within one single node, but that's not the point here. I've kept it deliberately very, very simple. So the main concept which you learn is how to uh implement conditional logic. Okay.
So let's code the different uh keys which we require. So number_one will be an integer. operation will be in the string, a plus or a minus. Uh number_two will be an integer and final_number will be an integer. The final_number will be the result of either adding or subtracting the two numbers. Easy enough. We've done this multiple times now.
Okay. Now, here's where things get interesting. Now, just a heads up. Initially, this won't make sense. But when we look at it from a bird's eye view and we look back at all the code in this subsection again, uh everything will start to click. So again, it won't make sense initially, but it will once we look at it. Uh again, don't worry. All right.
So let's create our first node function. Let's call it adder. And it's again still a node. And we input the state schema. And we return the updated state schema and docstring again. But uh this time I'm just going to copy it from here. Uh it's tells exactly what it does. This node adds the two numbers. Uh and easy enough, we just do state['final_number'] = state['number_one'] + state['number_two']. Okay. And we just return the state. Quite simple, right?
And just like what we did with the addition, we need a node for subtraction as well. So def subtractor. Now uh I already implemented it to uh don't, to not waste time but this node subtracts the two numbers. It's very similar to the previous uh node function. Uh it just subtracts these two numbers. Again, yes, you could be saying what if number one is uh smaller than number two, it'll give you a negative result. It that doesn't matter. The main aim again was to implement the conditional logic not the inner workings of each node. Okay.
Okay. Now we built another type of node. Uh and we initialize it the same way but this time let's call this node decide_next_node. Let's actually give it a name which actually says what it does. Right. So again we use state: AgentState and we pass like this. Perfect. Okay. Now the docstring will be something like so. So this node will select the next phase of the graph or well next node of the graph I should say. Okay.
Now we use an if statement and before I code something let's just try to map how this will work. This specific node will be at the start of our uh graph. So we will have the start node. We will have this, this specific node we'll call it the router. And this router because it routes uh the next to the next node depending on what the state schema is at that point. So we will have the, I will put an image up right now so you kind of get what I'm trying to say, but we essentially will have the router decide whether we add the two numbers and subtract the two numbers, and obviously this will be decided with the operation uh attribute, right, which you should see from here.
Okay, let's code this up now. So this is not the hard part. If state['operation'] is equal to equal to plus... Okay, if state['operation'] is equal to equal to plus, we need to do a certain thing to pass it to the next node. Okay, now here's, well your first guess could be, okay, well, we guess, I guess just call this function, right? Not exactly. Not in LangGraph. You actually return, uh return the edge.
Now, we haven't described the edge yet, right? But for now, I will just say the edge's name is addition_operation. So, addition_operation. Similarly, if it's subtraction, we will do this like so. So just to reiterate, we will see what the um value is at the operation in the state schema. If it's a plus, we call, we will return the edge addition_operation and if it's a subtraction we will use the subtraction_operation edge.
Again, we haven't described or defined these two edges yet. That's what I was saying earlier. When we look at it from the bird's eye view later on in a few moments once we've built everything it will make much more sense. So stick with me for now. Okay. And runs perfect.
Now we build the graph. And now here's the exciting part. So we again like normal standard procedure we use StateGraph to create the graph framework. So graph is equal to that. And let's add these nodes to our graph. So graph.add_node and let's say router. Okay. And again, we will pass this decide_next_node. Perfect. Okay. Now, I have another confession to make. Lots of conventions. I know this won't work. I know I haven't built the rest of the graph yet, but this eventually will not work. And there is a subtle reason why this won't work. You know, it's mainly in this line: add_node router decide_next_node. The problem is with decide_next_node because, oh, you can see that the docstring appears once we press the decide_next_node.
But the reason this won't work is look closely at these three functions. What are we doing in these two functions that we're not doing in this? I'll give you a moment to try to analyze this. Okay. So, it doesn't matter if, don't worry if you didn't get that. The correct answer is we are returning this updated state in this one and this one. But in this node, we're not. We're just returning the edge. Subtle difference, I know, but that's how LangGraph works, and you will see why they do it like that right now.
So how do we deal with this? Now, I obviously could have built this graph and then I would have shown you the error, but then things would have just gotten messy. That's why from the get-go I have told you why this wouldn't work. So now that you know why this won't work, how do you fix this? Simple. You use this code lambda state. Now, if you have used lambda functions before, this is quite easy to understand. If you haven't, don't worry. All this is saying is your input state will be your output state. That's it. In even more simpler words, think of this as a pass-through function.
So, what it's saying is your input state will be passed, your state will be inputted and your output will be the exact same state. Now, why is it the exact same state? Because you're not changing the state at all. You're comparing stuff here, but you're not assigning anything. There's a difference between comparison and assignment. Right? Again, even in this one, you're just comparing to see whether the operation is a minus, but no assignment's been made at all. In fact, there's been no changes to this state whatsoever. That's why we can use this as a pass-through function. Now, hopefully that made sense.
Okay, let's continue. Again, we will get a lot more practice. Don't worry, this is the first time you're seeing this. Okay, so now we will add the edge. And this is just the normal edge we did last time. So, we will need the start key. And now here's how you initialize differently. Remember how we used to do set_entry_point and set_finish_point? We don't do that anymore. We use START the keyword because that's what we imported. Make sure to import it if you do it this way. You use START and END.
So your start will be a start point and what do you want the start to be connected to? Well, we want it to be connected to the router. If I put this in quotation marks, perfect. Now, why not add node or subtract node? Well, think again. Refer back to that diagram which I'll show right here. We, if we connected the start point to the add node or the subtract node, well then what's the point of the router in the first place, right? The whole point was the router decides what the inputs are and then from there it branches off to the correct node. So that's why the router needs to be the first node we connect our start point to. Cool.
Okay. Perfect. Now we add the, we now implement the main, the new thing which we are going to learn in this section is graph.add_conditional_edges. So graph.add_conditional_edges. Now again, wow, looks really confusing but it's actually much more simpler than it looks like. So the first part is your source, which you can see here as well. So the source will just simply be the name of the node. And what's the name of the node which we want the conditional edge to be? It's the router node, right? So that's going to be the source part. Perfect.
Now if you look here, it's asking for a path. What's the path you would like it to do? Now before we implement the path, we obviously need to tell it what action, what action it needs to do. And that's where this node will come in, the decide_next_node part. So we pass that as the second parameter. So that's the path. And now we implement something called the path map which you should have briefly saw here. Path map.
So we've implemented the source which is the router. We've implemented the path which is your decide_next_node function. Again, don't need to worry about hashable runnable any and all of this stuff. Okay, it's, you don't need to overcomplicate it. Now it's time for the path map. Okay, so now your path map will be in a form of a dictionary. And remember how I said earlier that we had implemented addition operation and subtraction operation? These were edges. So now we're about to implement those only. So we're about to create two new edges here. Let me just write this code up for you and then it will make sense. Give me one second.
Okay, so there we go. Now what is this code actually saying? Well, this is in a format of edge and node. Now the starting point of this edge will obviously be this router node and it's telling us where it will connect to. This visualization will be, it will be, it'll be much easier to visualize when I actually show you the graph. Don't worry. But for now, addition operation and subtraction operation is the edge. And the two nodes are add node and subtract node. Right? Okay.
Lastly, we now, we're now at the point where we need to create the end point. But obviously, if you look back at this diagram which I've shown on the screen right now, you can see that we need two edges to connect to the end point, right? We need an edge from the and node and we need an edge from the subtract node. So we can add two edges like this: graph edge. We start at the add node and then we end at the endpoint. Again, similar, subtract node and endpoint. And then we just compile this. So app is equal to graph.compile(). Cool. No errors.
Okay. Now here comes the most exciting part. Again, try to visualize what this graph will actually look like. Okay. So it should look something like that. Probably slightly different to what you initially anticipated, but that's okay. We again have a start point. We have the router and we have our two nodes, add node, subtract node. And notice, remember when I said addition operation and subtraction operation are the edges' names? Well, here it is: addition operation and subtraction operation. It's telling us which direction to go into. Do we go, how do we go to add node? Well, we use the addition operation. How do we go to subtract node? Well, we go to the subtract operation. And then obviously we create these two edges, these two to connect to the endpoint.
Awesome. So, we will once again look at it from a bird's eye view. But let's actually invoke this graph to see what happens. So let's use this piece of code. So what it's saying is it's defining number one as 10, operation as minus, and number two as five. So because we've used subtraction, the final number should be 10 - 5 which is five. And we've printed the results and the answer is like such. Number one is equal to 10, operation is equal to minus, number two is equal to 5 and final number is five. Obviously the way I've invoked it is slightly different to what I have done before. Again, this is another way you can invoke. Okay, so not too hard.
But let's just go through everything one more time to solidify everything. Okay, so we imported everything. We created the state schema using AgentState and a type dictionary. Then we created our three different nodes, which is the add node, subtract node, and the decide_next_node. And this is within the decide_next_node. You can see that if the operation is a plus, it goes to the addition operation edge, which is this edge. And if it's subtraction operation, it goes to this side.
And this is how we built the graph. We added the nodes. We added the edge from the start point to the router. And then we added the conditional edge, the new thing which we've learned in this section, which is we reference router and we use the edge-node format. So the edge will be addition operation to add node, then it will be subtraction operation to subtract node. Visually speaking, it will be addition operation to add node, subtraction operation to subtract node. Now, I know this will be quite confusing at first and don't worry, it took me quite a while to understand this myself as well, but hopefully the exercise I've given you will really be able to help you understand this much better. Okay, so I will see you at the exercise then.
Awesome. So, let's actually find out what the exercise is for this graph. So, you need to make this monstrosity. Now at first glance it looks terrifying, but if you analyze it a little bit closer, all it is is what we just coded twice. So we coded this and we need to replicate it once more. So in essence, you need to actually input four numbers and two operations and you need to output their final results. For example, number one, number two, number three, number four, and the respective operation and the respective results. Right? So in this case we would have to do 10 - 5 which is 5 and 7 + 7 + 2 aka 9 and those two numbers should be outputted.
Now the reason I gave you this exercise to do is because this will really solidify your understanding about conditional edges, which will really be important for the next few, next graph and the next AI agents we make. Okay. So once you have completed it by looking and cross-referencing the answer on GitHub, I will see you in the next graph.
Okay. All right. Well done. We're almost at the end of this section and we're about to build our final graph aka graph 5. Now we've learned quite a lot about LangGraph and its internal mechanisms. And this will really help us in the next section where we finally build the AI agents you were looking for. Now in this section, in this subsection sorry, we're going to be learning an important concept. There's still one more concept we haven't learned and that's about looping. So we're going to be creating, well, a simple looping graph.
Now I kept the objectives to be quite small here. There aren't that many objectives. It's essentially implementing logic which involves looping to route the flow of data back to the nodes. And we're going to be creating a single conditional edge, which you know how to do in the previous section. Regarding the previous section, however, I know the exercise. Please do complete that exercise. That exercise will be probably the hardest exercise you would have done until this point. So, don't worry if you didn't get it. If you did, great job. You're doing really, really well. But if you didn't get it, look at the GitHub. Try to compare where you went wrong. Remember, in LangGraph, there's more than one way of building the graphs. Make sure the graphs are well built and it actually functions. And if you want an extension, try to make it even more robust than it is.
All right, but back to this now. Final graph, I promise. The main goal really is to code up the looping logic. So, with that out of the way, let's build a final code for this section. See you there.
Awesome. So, final code we have to build for this section. And here we go. So, graph 5. Now, I'm going to take a slightly different approach this time. And I'm actually going to show you the graph we want to end up building from the get-go. And there's a reason I'm going to start that from now so we get in good practice. The reason is once you finish this course and actually start either making your own AI agentic systems for someone else, for your clients or for yourself, like make your own Jarvis system or whatever, you obviously need to plan how it works, right?
You need to see, okay, what nodes do I need? What edges do I need? Does this need to be a conditional edge? Where's the start point going to go, end point going to go, etc, etc. And you can either do that via pen and paper or software like I've used, but point is you need some sort of blueprint and that's how really it works in the industry as well. You will obviously have a blueprint and then from there you will code up the graph similar to how a UI designer, for example, renders their UI designs and then sends that off to a software developer who well, develops the application forwards. Right? So that's the habit I want to start creating with you.
All right. So this is the graph I want to build in this section. So there's obviously going to be a start and end point. And this really should be mostly familiar except for this loop. So there we're going to create a simple greeting node and another node which is called the random node. So in the greeting node, I essentially want the user to have stated their name and it should output a simple "hi there, your name." And then the graph progresses to the random node, and in the random node I essentially want to generate five random numbers. Okay, now just as a heads up, yes, this graph in industry would be completely useless.
I know, but I've deliberately kept it simple again so you know the fundamentals. Like this loop could have easily been avoided and transferred into a for loop for example, right? Like I could have had a for loop within this node and ran it five times to generate the numbers. I get it. But this is again kept deliberately simple so you actually understand the concept. Okay, cool.
So the usual inputs and the only difference is this time I've also imported random, but if you have used Python before quite a lot, you would have come across this library, right? Okay, so let's start with our agent state. So class AgentState(TypedDict). And what's the first thing we're going to need? Well, let's see. We have the start point. Do we need anything, any keys? No. For greeting node, what did I say I want? I wanted the user to be able to input their name. So, we need a name attribute or a key.
And then for the random number, a random node, we need some form of a list to actually store the numbers. So, we have number: List[int]. Okay, cool. And one more thing, look at this loop. How will we actually know when to stop? We need some form of counter, right? So, counter: int. Perfect. Now, obviously, just as a heads up, when you do go on to make your AI agents and everything, you're not going to know what attributes you need right from the get-go, unless if you planned it like extremely, extremely well. But chances are you won't get it. But don't worry, iteratively, well, you'll obviously be better at speculating what attributes you need through practice. But you can obviously do iterative development as well, right?
Okay, cool. So now let's actually build these nodes. Okay. So let's start off with the greeting node. So how we normally define a function. So def greeting_node(state: AgentState). Perfect. And the docstring. But luckily for me, I've already got that here. So I don't need to do it again. I know it's boring, but habits. Now let's update the name key. So how do we do that? Well, you should know by now, state["name"] = f"Hi there, {state['name']}". Perfect. So what will this do? I input a name and it'll replace that name with a string of "Hi there, this person."
Now let's also initialize the counter variable here. Now why am I doing that? Let me just first write it and think about this. Okay. Now, obviously I'm changing, I'm setting the value. So, I will need to have passed in like a valid integer when I am passing the value, when I'm invoking the graph, right? But here's the thing. What if I pass in -2 for example? Well, as the counter value, as the initial counter value, if this line wasn't there, well, it would have just kept on incrementing until it got to five because I want to have five numbers. But if it starts at -2, well, it would end up giving me seven numbers. Now, that's not robust, right? So, this basically wipes out whatever rubbish integer the user even inputs. If they had put zero, well, okay, we replaced it to zero. If they put like -20 because they're greedy or something, then we have made sure to like set that back to zero. So, just a way to make it robust. That's all. Return state.
Okay, cool. So, now let's create our second node which is a random node. So, we can say random_node(state: AgentState). Perfect. Docstring. Again the docstrings will be useful. I promise in the next section they will. So this generates a number, random number from 0 to 10. Now this piece of code here essentially appends the randomly generated number to the number list. Okay, that's all it does. And what else do we need to do in this node? Well, we need to increment the counter value, right? So, += 1. So this will increment the value by one and then we just return the state.
Okay, cool. Now here's where we're, how we're going to implement the looping logic. Now just a warning here and please listen to this. Like in any software development program or programming language, there's more than one way of coding up an application, right? Same goes with LangGraph as well. There is multiple, multiple different ways of coding like a looping code like this graph. I'm going to be showing you one of them. I obviously can't show you all of them because it's just a time constraint, right? But obviously the more you practice, the better ways you'll, more efficient ways you'll find, right? But the way I'm going to show you is pretty efficient as well. Don't worry.
Okay. Now, you might have speculated that I'm going to create like a router node. It's close. I'm not going to create another router node. You can see in the graph, let's say the client wants this graph. The client doesn't want another router node here. So how do we go about that? Well, we could create a conditional edge. How do we do that? Okay, let's begin that. So let's write a new function, say def should_continue(state: AgentState). And let's create this docstring, "function to decide what to do next," something like that.
Okay cool. Now here's where we set our looping logic and this should look quite familiar to you now. Perfect. So let's run that. Okay. So what have I written here? Well, if the counter value is less than five because we're starting with zero, right? So 0, 1, 2, 3, 4. That'll be five values. I've also written a print statement so like we can keep track of the progress. Also whenever I'm writing the code as well when you're coding with me or doing the exercise, it's really helpful to print statements, like put in print statements everywhere. Or you could also use breakpoints as well. So you know where the code failed if it fails. Okay. So here we return the loop, a loop edge and the exit edge. So obviously we have the loop edge and this will be the exit edge. So everything is going to plan so far but, so far is the key. You never know, right?
Okay. Just as a heads up though, I want to show you this. So this is how the trajectory should follow. We start at the greeting node. Why? Because we obviously go from the start to the greeting node. And then we enter the random node. And we enter the random node and exit it five times. So 1, 2, 3, 4, 5. Why five times? Because we want five random numbers, right? By then this if statement will, well it won't work. It will fail. So we will go to the else statement and return "exit". And if we return to exit, we'll go to the end node. endpoint. Okay, so that's how the general gist is.
Okay, let's quickly make this graph. So you should know how to initialize a graph agent graph and let's just add these nodes. So we have our two nodes which are here, greeting and random, which is exactly what we wanted, right? Greeting node and random node. Perfect. Okay. And now we're going to add an edge between greeting and random. Why? Because, well I've created this edge. You see this edge, greeting node and random node? This edge, that's the edge I've created. Okay.
Now I'm going to create the conditional edges, which is done through here, and I've written some comments here as well. So there will be the source node, which is the random. So where I want the conditional edge to start from. And then the routing function, or this, I should have really written "action" here because it is the action I want to perform, the underlying mechanism or function which is going to, which we're going to determine which edge to use. And that's implemented by the should_continue function, right?
And notice how again, these two edges are the same edges here. So if the loop is the one which is outputted, then we need to go back into its random, the random node which we've generate, which we put there. And if it doesn't, we go to the end part. Okay, and then obviously we set the entry point. Okay. So again, you don't have to set the exit point here or the finish point because we've already done it using END here. Okay, perfect. And then we just compile the graph. app = graph.compile(). And okay, it compiled. That's a good sign.
But let's see if we have got our graph to be the exact same. Now I'll put the graph image here so I don't keep scrolling back and forth. But you can see we have the start point and the end point. We have the greeting and the random. And then we have our two condition edges. So we have the loop going back into the random node as we wanted and the exit which you can see. So take a moment and you can see, compare and contrast. Okay, let's continue.
Okay, now I have this code. So I've given a name, my name, a completely brand new list, and I've set counter to -1. And as you can see it enters loop one, loop two, loop three, loop four, because these are print statements we printed. It says "Hi there, v" which is my name. Number 10, 2, 1, 0, 2, 6 just randomly generated and you can see the counter value is five. Now remember what I was saying over the counter. We set the counter value to zero here to make it more robust. If we had not done that, well it would have generated six times. And now I can set this to -100. It will still obviously give me different random values, but the code is largely the same. So that's really the way which I personally use to create loops in LangGraph. It's pretty easy, right?
But obviously with practice you might even find some other ways. If you do find other ways like obviously do let me know. There's more than one way. Again, you can send me a message on LinkedIn or Instagram or whatever. But yeah, so this is finally, finally we have implemented the code for our final graph of the section. So just complete the graph 5 exercise please and yeah we should be good to go to make AI agents. So I'll see you at this code's exercise. Okay, cool.
Okay, good job on that. Now for the exercise for this last graph, you need to implement this graph on the right. So you need to implement an automatic higher or lower game. So for context, you need to set the bounds which we can guess between 1 to 20 integers of course, and the graph has to keep guessing where the max number of guesses is 7. Where if the guess is correct, it stops, but if not, we keep looping until we hit the max limit of seven. Now please note, we don't have to pass any inputs. The actual graph should automatically guess by itself. So there should be no human-in-the-loop human intervention at all.
So each time a number is guessed, the hint node aka this node, should say either higher or lower and the graph should account for this information and guess the next guess accordingly. So for example, the input should be something like the player name student. The guess should just be an empty list because we're initializing the list. Attempts should be set to zero and the lower bound and upper bound should be 1 to 20. Now the reason I've also passed these as inputs is because if you wanted to expand this to maybe 1 to 50 numbers or whatever you can. It's quite easy to do that.
So just as a hint, it will need to adjust its bounds after every guess based on the hint provided by the hint node. So once you've completed this exercise, you would have fully reinforced your understanding about loops in LangGraph. So once you've completed this, cross reference it. Cross reference the answers on GitHub. I will see you in the next section where we finally begin AI agents. See you there.
Okay people. So welcome back to this brand new section where we actually start learning about AI agents. Now we finally are upgrading our ability in LangGraph. I even upgraded my clothing sense. Not really. But this is exciting times because we actually finally build AI agents. So, we're going to build a lot of AI agents in this section. And starting off with the first agent. Well, technically it's not really an agent, but I just named it that because it sounds cool. But technically it's not though.
But let's see what we're going to actually learn in this section, in this subsection. So, we're going to build a simple bot. That's it. And these are the objectives. So we're going to define a state variable, state structure, which we're going to have a list of HumanMessage objects. And I briefly mentioned what a HumanMessage was a long time ago in the course. What it is, it's, well, it's in the name, it's a message prompt which is given by the human aka us to the AI.
We're going to initialize the GPT-4o model for this using LangChain's ChatOpenAI library. We're going to send and handle different types of messages. We're going to build and compile the graph of the agent. But the main goal really is how we can integrate LLMs into our graphs. So what is this sort of graph we actually going to end up building? Now it's very, very simple. It's going to look like this. And yes, this looks exactly like the graph we made in the first ever graph we actually ever made. But the functionality will obviously be different because now we're actually integrating LLMs. So exciting stuff, people. Okay, I will see you at the code then.
All right, coding time. So now we first code our, well we code up our very first AI agent aka the simple bot and I've already imported all the necessary libraries we'll need to not waste time. So while you're coding these up as well and copying these, I'll also briefly explain what these are so we're at the same level. Okay, so we've already imported TypedDict and List many times before, but these two we haven't, sorry these two. The langchain_core.messages import HumanMessage.
So I briefly mentioned this in the intro of this section, of the subsection, what a HumanMessage is, right? And this is the library we get it from. And similarly, we're going to be using OpenAI's LLMs. So that's why we're going to use ChatOpenAI from the langchain_openai library. The langgraph.graph. These we were familiar with and this is the .env. Now just a few points. You could have been saying, "Okay, wait, hold on. I thought we were about to do a LangGraph stuff. Why is the LangChain stuff here?"
Now you must know that LangGraph is built on top of LangChain, and LangChain already has the sophisticated libraries, right? So why not actually use them? That's how LangGraph is designed. It's designed to use the robust, sophisticated libraries which LangChain offers, right? So no, I'm not a traitor. We're still doing LangGraph stuff, but we're also using, leveraging LangChain's strengths as well. Okay, and now this .env file. Now it's okay if you haven't ever encountered a .env file before. Essentially, it's just a file used to store secret stuff like API keys or configuration values. So, it's really there for security purposes.
Now, I have my own file stored in my folder structure so that you don't see my API key, because if you do, then I would go bankrupt. So, that's why. Now, you might also be wondering why do we need an API key here? We need the API key because we're doing calls to an external LLM. If we were using our own LLM like through Ollama, then we would not have an API key, right? We would just use like the Ollama library integration with LangChain. So because we're using charges, we need an API to communicate with the LLM in their cloud servers.
Cool. So how do we actually load this? So to load our API, we just use a simple Python code load_dotenv(). All right. So now that we're at the same level, let's actually code up our AI agent. Cool. So let's define the state like we always do. So this time class AgentState(TypedDict). Perfect. Okay. Now what are the attributes we need in this section, in this state? Well, really just one, the messages part, right? So messages, but what form will it be? Well, it will be in the form of a list of HumanMessage, right? So we'll have List[HumanMessage].
Why? Because when we invoke the graph, we're inputting human messages, right? So to tell the large language model that this is a human message, i.e. this is a message from me the user, aka human, right? We need to actually mention HumanMessage that it's a HumanMessage type. Cool. Okay.
So now we actually initialize the large language model. So we just write llm = ChatOpenAI(). And now we specify what model we want. Now I'm going for GPT-4o. Now yes there's also Chat Anthropic. I think there's Chat Ollama. There's a lot of like in-built libraries which LangChain offers which is great. Personally I've used ChatOpenAI a lot. I've also used ChatAnthropic a lot as well. Personally I like ChatOpenAI because it's just really simple to use. I've also used tried, well tried to use ChatOllama before but really there's some difficulty in integrating it with Lang. So that's why I've opted for OpenAI.
And if you're worried about financial cost, don't worry, it's extremely cheap. If you want, you could also go for the GPT-4o mini model as well if that's a concern. But trust me, it's extremely cheap. Like the input tokens, output tokens is like in tens of pennies for like a thousand tokens. So really, really cheap.
Okay. So now let's actually define our node through our function. So process and we obviously define the state and then return the state like so. Perfect. Now how do we actually call the LLM? Now LangChain and the LangGraph team really like using the word "invoke". You might have noticed that to call a graph or like to make the graph run, we've used "invoke". Similarly to run the LLM, we use "invoke" as well. So we, okay, let's store the response we get in a variable. So llm.invoke(). And what do we invoke?
Well, you can see from the hints here that it requires an input of language model input. What's that basically saying is what do you want the LLM to do? Right? What's your question? Now what is our question? Well, that's in the messages. So we write state["messages"]. So what will happen here is as soon as I've written state["messages"], let's say I have written "hi" or whatever, we will pass this to the LLM through the invoke method. The LLM will then generate a response from its cloud server through our API and it'll get, it will give us back its response and then we'll store it in the response section, the response variable. Cool. And let's actually print this like so and return the state like such.
Okay, done. Now we obviously need to create the graph like such. Okay. So what is it saying? Well, it's saying that we've created a node process, which is that, where the action is the process function. We've added an edge. We've added an edge from the start to the end node, end point, and we've compiled the graph. Okay. Yeah. So let's now ask for the user input. So user_input = input(). We'll say "Enter something." And now we will invoke the agent because we need to invoke the agent of course, because we're creating a graph, right? And the graph is like an agent in this case.
Cool. Let's actually run this code now. So, python agent_bot.py and perfect. So, enter. Let's say "hi". The AI message was, "Hello, how can I assist you today?" Now, I can reassure you I did not pre-code this or hardcode this. This is the actual LLM. Let's run it one more time. Let's come up with a different message like, "Who are you?" And it'll say, "I'm an AI language model created by OpenAI called ChatGPT." So you can, this basically pretty much confirms that yes, this is GPT in the background.
Okay, but why just stick to one message, right? Why not be able to run multiple message, like asking multiple messages kind of like a chatbot, right? So this is the code which does this and I'll walk you through this what's happening here as well. So like before, we input our query and now we basically say keep iterating through and as soon as the user has said like "exit" or something then well you exit the while loop and that basically signifies that well you don't want to talk to the LLM anymore.
So let's get run this. So python agent_bot.py. Okay, let's say "hi" again. "Hello, how are you?" But now we can run it again. It's just a simple while loop. It's nothing groundbreaking. So like, "Who made you?" Okay, perfect. "What is 2 + 2?" "2 + 2 = 4." Okay. Let's say now, "Hi, I am Bob." Okay, now watch this carefully. I'm about to ask, "What did I just, well, or I should say what is my name?" "I'm sorry, but I don't have the ability to know your name or any personal information about you." Why is that?
Why didn't it know what my name is? Well, even though I clearly specified it. So, let's quickly exit. Okay, now this is important. Nowhere in the code have we actually created some sort of memory. That's why I called this subsection "Simple Bot." And that's why I kept on saying AI agent because it's not even an agent yet. It's just a simple, like the most basic LLM wrapper you can possibly have. But at least now you know how to actually integrate LLMs in your graphs, right? And it's pretty straightforward. You just embed them within your functions and then your functions obviously act as the actions in your nodes. And that's it. It's quite an easy piece of code. Like it's only what, 29 lines or 25 lines, give or take. But yeah, pretty simple. I don't think there'll be any exercise for this because well there really isn't much to do with this. So I will see you in the introduction for the second AI agent we're going to build. Okay. So I'll see you there.
Cool. Cool. Cool. Okay. So now we're going to build our second AI system. And we're going to try to fix the problems we faced in the last AI system we built. And what was the problem? Well, the problem was it doesn't remember what we had said before, right? Why? Because we were calling separate API calls. So now we're going to try to create a chatbot with some sort of memory. That's why I included the brain emoji here.
So let me walk through the objectives for this subsection. So, we're going to use different message types, in particular the HumanMessage and the AIMessage. We're going to maintain a full conversation history using both of these message types. We're going to particularly use the GPT-4 model again using the LangChain's ChatOpenAI library, and overall we're going to create a sophisticated conversation loop. So, what is the main goal of this subsection? It's really to create a form of memory for our agent. So if you're ready, let's go to the code.
All right, awesome people. So let's begin coding our simple chatbot then. Okay, so like last time, I've already imported all of the necessary libraries and it's largely the exact same except now I've added two more stuff. So the first is the AIMessage and I explained this in the introduction of this subsection why we need the AIMessage. And I've also imported the Union type annotation. Now the Union type annotation is something we covered in the first chapter. So if this is the first time you are looking at it or hearing about it, I would recommend you going to the first chapter, really understanding and watching the first two chapters. They're quite short to be honest and then coming back. Okay.
Now that being said, let's actually begin the coding then. Okay. So like always, we define the state. So class AgentState(TypedDict). Perfect. Now last time what did we define this as? Again, we're only going to have messages again, but last time we had List[HumanMessage]. Okay so that was what we had defined as our agent state previously. Now this time we also want to include the AIMessage as well. We're building a more sophisticated chatbot. So how do we do that?
Well, one way or the naive way is to really have it as messages_ai: List[AIMessage] or something like that. Something like that. And don't get me wrong, this is still a valid approach. You can still build your graph and everything like that, but I think it's a bit longer. So let me tell you another way which would actually be better. So remove this. Instead, let's use the type annotation Union like this. So, Union like so. And let's include AIMessage. Now, what has this done?
Essentially, let me first tell you about a bit about HumanMessage and AIMessage. HumanMessage and AIMessage and like all of these different structures are actually data types in LangGraph and LangChain. That's what the developers of these libraries have got them to be. And when I write Union[HumanMessage, AIMessage], then that basically allows me to store either human messages or AI messages in this key in the state, the messages. So that's what that literally means in a nutshell.
Now, one important thing which I want to mention is this. All of these AI agentic libraries like LangChain, LangGraph, Crew AI, Autogen, they're all great, but you really can make your own AI agentic system by writing just Python functions. You don't even need to use a library. Now that being said, I would still recommend using these libraries, especially LangGraph because LangGraph, well, because it's a personal favorite, no bias at all, but LangGraph really allows you to control much more than other libraries would. Obviously, not as much control as if you were writing the Python functions yourself and everything, but I think it's a good balance of how much control you have and how much unnecessary jargon you need to write. Because think about it, think about how much of this unnecessary code which you would have had to write else otherwise LangGraph and LangChain support. So that's why I highly recommend using these libraries and everything.
So again, HumanMessage and AIMessage are data types, inbuilt data types within LangGraph and LangChain. Cool. Okay. Now let's again initialize the large language model as we did last time. And again we're only, we're using GPT-4. Okay, now we're going to create our node. Again, it's going to be the exact same graph structure, by the way. So, state: AgentState, but obviously the actions we perform will be slightly different. Now, let's write a docstring. "This node will do solve the request you input," something. Docstrings aren't really needed for this AI agent or this subsection because we're not going to use them. But again, good habit.
Okay, cool. Let's invoke this. So what have I done here? This is exactly the same code which we did in the previous subsection. We invoke the LLM with the state messages. And what are the state messages? Well, it could be either a HumanMessage or an AIMessage. It's a list of those. Awesome. So now we write this piece of code. state["messages"].append(AIMessage(content=response.content)). What on earth is happening there? Okay. Let's break this down.
response.content, well, that's just extracting only the content part of the response, aka the response being the answer or the result after we make the API call from the large language model. And it only extracts the content. So it only extracts like the important stuff, right? It removes all the unnecessary jargon which comes with it like the amount of tokens you use and all that. It removes that and that gets stored in, that gets converted to to an AIMessage and that's appended to our state messages in the state. Simple. Okay. Now obviously to make it look pretty in the terminal we're going to print this and then we're going to return the state. That's it. That's how simple it was.
Okay, so now we're going to create this exact same graph. And that's why I've just copied and pasted it because it's a time waste of me rewriting the code in front of you again and again. So we can just reuse the same code because it's the exact same graph structure as the previous subsection. Cool. Okay, now we're going to now here's where it actually starts working differently. See, last time we didn't have this, the conversation_history. Really this is what's going to be our memory in this setup.
Okay, so we have now initialized conversation_history. Now again, we're going to ask the user what they want, right? What's their request? So now we use this while loop and this while loop was the exact same loop we had in the previous subsection as well. It only exits unless if the user has inputted well "exit" obviously, but now look at this. The conversation_history is appended with the HumanMessage and the HumanMessage is obviously the user input. The reason I've kept on writing content is because well that's the parameter in HumanMessage as you can see here.
Okay, cool. And we've invoked the agent. What is the agent? Well, the agent is the compiled version of the graph, the compiled graph with the entire conversation history. Now this is important. The entire conversation history is sent, not just the current HumanMessage. So this will make more sense. Don't worry, I'm trying my best to explain it right now, but obviously it will make much, much more sense as soon as I start running it. Okay? So bear with me if you didn't fully understand that. Don't worry. Let's remove that for now.
And then we replace the conversation_history completely, like wipe it with the result messages. Why? Don't worry, it's going to make sense as soon as I run it. And I think yeah, we should be able to run this now. So, let's write python memory_agent.py, which is the name of the file. Cool. Okay, let me just quickly write a "hi" just to see if the API is working. It is, perfect. Okay. "Hello. How can I assist you today?" Now I'll say like, "Hi, my name is Steve." "Hi Steve, it's great to meet you. How can I help you today?"
Okay, now remember from last time. Last time if I asked it, "Hey, who am I?", it didn't know. Do you think it will know now? Think about it. It does. "You are Steve, or at least that's the name you've chosen to share with me." And the rest is whatever. So it does know about what I have said, but just looking at this code, I guess you can try to like pick out, okay, how does it work and everything, like why everything works like that, but I think we can add print statements and everything. So let's try to add print statements now and see well, how this is actually working. So let's exit the program.
Okay. Let's add a print statement here. Let me include this. Cool. So what is this saying? So this print statement actually, kind of think of it like a snapshot of what the current state is. So as soon as it goes into a process node, just before it's about to finish by returning the state, we also print the current state as well. And this will literally just output whatever is stored in the messages attribute within our state. Okay. So let's clear. There we go. Let's run this again.
So, "Hi, nice to meet you." Something like that. Now take a look at the current state. See it outputted, "Hello, nice to meet you. How can I assist you today?" Why did it output that? Well, because in our process function, we've asked it, we formatted it in a way so it says "Hey AI," which is this part, and the response or content is this part. Okay. Now this response or content is also what was stored, remember how I said it's stored in a nice manner and was appended to our state messages. Now was it appended to the state messages? Yes. How do you know that? Because look at the HumanMessage. The HumanMessage was what I wrote, which was "hi nice to meet you." Forget the additional keyword arguments and response metadata because I didn't provide any. You don't need to worry about that. The main part is this part, the content.
And then look at the AIMessage part. The content is equals to "Hello nice to meet you. How can I assist you today?" Notice how it's the exact same thing as it was here. Okay. Now, now we're going to go one step ahead and I'm going to ask it to say "My name is Steve" again. Now, think about how will this current state change? Pause the video and try to think about this like that. Okay. So, now the second message I sent was "My name is Steve." Its response for the second message was "Hi Steve. How can I help you today?"
Now look at the current state. It still begins with "Hi, nice to meet you," which was the first ever message I inputted into this conversation, and then its respective AIMessage. And then that gets appended. Why appended? Well, because we had appended the AIMessage and appended the HumanMessage. So that's why. Okay. And you can see the HumanMessage is "My name is Steve," which is the most recent message which I put, and then the AIMessage which is "Hi Steve, how can I help you today?" Perfect. And we can just keep going and going and going. But for now, I'll exit.
Now here's the thing. This works well, relatively well, right? We've got it like as a chatbot, which is what we wanted. It has some recollection of memory or of what we are, who we are and everything. But there's two big problems here. Let's start with the first massive problem which is this. You know how I've exited the program right now. Yeah. Okay. I'm going to run the exact same program again. Now I told it that my name is Steve. "What is my name?" and it says, "I'm sorry, I don't have access to your personal data." Okay. And look at the current state, completely wiped out.
Again, that's pretty self-explanatory as to why you exited the program and that's why obviously all the data was stored in the variables, right? The state was stored in the variables, completely got wiped away. So what is the solution? Think about it. Well, obviously one potential solution would be to store it in a database, like a large database, right? Or a vector database if you're trying to do some RAG applications. For now, I'm just going to store it in a very simple text file. Why? Because it's really easy. And I've got the code as well.
And usually, honestly, I just store it in a text file when I'm prototyping. Now, yes, obviously, storing it in a vector database or a database is much more robust and sophisticated, and that is what you should do. But when you're prototyping and you want to really see, try to build it quickly and fast, I just tend to use a text file. It still works, still works great and everything. So what is the code for the text file? It's here. Okay. So what is this code saying? Well, essentially it's saying create me a text file called logging as a text file you see in write mode and a file. Write your conversation log. This is just to like make it look better and more aesthetic.
But this is the main part of the code which you should actually try and understand for every single message in the conversation history. Okay. Now note the conversation_history was this variable which we had like initialized the conversation history, stores the AI messages and the human messages. So that's where all of the information outside the graph actually is. Right? The state is locked in within the graph now and the conversation_history is just another, I guess you can say a duplicate version of the state, right? Because we've used, we've appended the exact same human messages and AI messages and kept on updating it through this line.
Cool. So what it says is that for every single message in the conversation history, by the way a conversation is the duration between my first message and the last message I sent, that entire thing is a conversation. A conversation isn't just a single API call. It's the entire length. Okay? So, just to be mindful of that. So, it first checks if it's a HumanMessage, it writes that as "You" and then extracts that content, and if it's the AIMessage, it puts it under the "AI" stuff. So, let's run this again. So, let's exit this. Clear this. Okay.
Okay, let's say, "Hi, I am Steve." Again, I'm using Steve because a Minecraft movie just came out, so that's why. Okay, now I'm going to intentionally make a spelling mistake here. "Good mornin." It should obviously, morning should have been spelled right, but I'm doing that for a reason. It gets, no problem. Let's say, "Tell me a joke." Just another random thing. "Sure. Why don't skeletons fight each other? They don't have the guts." Um, really rubbish. My point is it works. Now I'll exit the program. And now it says "Conversation saved to logging.txt."
Now look at this. Remove that. Let me remove that. Okay, perfect. So this is the conversation log. The first message I ever sent was, "Hi, I'm Steve." There, its response. Now, "Good mornin." Now, why did I spell it like wrong? Why did I do that? Because I wanted to show you that this is the actual human message, my message being stored unaltered. So whatever I pass in the state as a human message, that stays there. So it's unaltered. The AI message cannot or anything can really change the previous human messages at all. Right? So that's why. And you can see "tell me a joke." "Sure." It's, its a rubbish joke is after that. And that's the end of the logging.txt file.
So that's a really fast, efficient way, not the most robust way of course, but it is a fast, efficient way to be able to store your data outside the application if it stops. Perfect. Now, what was the second problem that I was mentioning? It's this. Look at how I initially, I say, "Hi, I'm Steve." I don't know why I printed twice there. Weird, but whatever. Look at the current state length. Okay. Then I pass in another message, it becomes longer. I pass in another message, it gets longer. It keeps getting longer and longer and longer.
That's a problem because think about it, you will use these library, you'll use these like large language models whether it's for your own AI agentic startup or your own mini Jarvis or your own projects or whatever it is. You would obviously want to minimize cost, right? But using so many tokens, using so many tokens like input tokens especially will really eat away your, the amount of money you will spend, like it will drastically increase it and that's a huge problem, right? We want to try to be conservative a bit about our money and our financial, our finances.
So what is one, what is a way to solve this? Think about that. Well, right off the bat, I can give you an easy solution to implement, which is within the code, write some code where if the number of human messages exceeds five or something like that, then you remove the first human message in your history. Why remove the first and not the latest? Well, because the latest is most likely to be more relevant, right? The first message could is most likely to be the one where the one which is well, not needed or it could have been like, it has more of a chance to be like a bit more, less of an impact to have been removed. So why did I pick five? Well, five is just a random number I thought of. You could do 10, 15, 20, 25, three, whatever. But that's a really easy solution to do.
Okay, so we learned quite a lot there. We learned how to integrate HumanMessage and AIMessage within a thing. And now we've created a somewhat of a sophisticated chatbot, right? It has a memory. It still talks to us. If we write who we are, it remembers that and everything and it works great. Obviously, it has its flaws, but for now, it's pretty good. Okay, so now we're going to build our third AI agent. And this is going to be a special type of an AI agent. The technical term for this type of agent is called a ReAct agent, and ReAct stands for Reasoning and Acting. So this is a quite a common type of AI agent which you will build.
So how does it look like? Well, it looks something like this. So it's quite simple. There's a start point and then there's an end point obviously. Then you have your agent and then we use a loop where we attach it to tools. Now, this could be one tool, two tools, a lot of tools. And it's the agent's job or the LLM in the background to be able to decide which tool to select. But not only that, it's also its job to be able to decide when there's no more tool calling left to do. And when that happens, it goes to the end part.
So that's the general gist of what a ReAct agent is. It's a very, very common and famous type of agent to make in LangGraph. And that's exactly what we're going to be building in this subsection. So what are the objectives? So to build a ReAct agent, the objectives are learn how to really create tools in LangGraph. We're going to be creating a ReAct graph. Of course, we're going to be working with different types of messages such as tool messages. See, last subsection we covered AI messages, human messages, but now we're going to look at a lot more types of messages. For example, tool messages, system messages, base messages, and we're obviously going to test our robustness of our graph. So, the main goal is to create a robust ReAct agent. So, if you're excited, I'll see you at the code.
Okay, people. So, now we're going to code up the ReAct agent. And just a heads up, this is going to be quite a long subsection. So, get ready. You can see it's going to be long because of how many imports I've done. But because I've done so many new imports, I actually want to take some time off and really explain each line so that we're all on the same page. Okay, let's go. So the first line is from typing import Annotated, Sequence, TypedDict. Now we obviously know what a TypedDict is, but we haven't come across Annotated or Sequence yet.
So these are also type annotations. And I'll start off by explaining what an Annotated is. So Annotated is a type annotation which provides additional context to your variable or your key without actually affecting the type itself, the data type itself. Now what exactly does that mean? Well, I'll give you an example. Let's say I am trying to create a TypedDict where there is an email key in it. Okay? Now, obviously, an email is a string. So if I was to create a TypedDict, I would have written email: str, right? That's how we've been doing it. But here's the thing with certain keys like email, they have to be a certain format. For example, it has to be like abc@gmail.com. But if I pass in abcd-gmail.com or something like that, that's not a valid email format anymore, but it's still a string technically.
So how do we resolve that? Well, that's where Annotated comes in. And I'll give you an example here. Let's say email is equal to Annotated. I'm not going to create the whole TypedDict to save time. But for the example itself, you first pass in what data type you want it to be. So we want email to be a string, right? That's not changing. But here in quotation marks, I provide some more additional information, additional context. And this is basically adding onto the metadata of this key or variable. For example, let's say this has to be a valid email format.
Now, obviously, I should do it in more detail, but for now, that's fine. So, how can I actually see the metadata? So, if I want to, I would write print(email.__metadata__) like that. And then I would press run. And here we go. You can see "this has to be a valid email format." That's the exact same thing which we wrote here. So that's Annotated done.
But what about Sequence? What does Sequence mean? Well, Sequence is also a type annotation. And the way I've described it is here. It basically automatically handles the state updates for sequences, such as by adding new messages to a chat history. Now, what does that mean? Well, it's really just there to avoid any list manipulation to the graph nodes. Obviously, like when we're using graphs and nodes and all of that stuff and updating the states, there's a lot of list manipulations which we'll have to do. Sequence really handles a lot of that. So, that's really what it's there for. You don't really need to worry about it too much.
Okay. Now, if we continue, we have .env from import load_dotenv. From last time, we know that this is just to store our API keys, and I've done that here. That will load the API keys. But you'll see now these three imports. We're importing some new message types here. So we're importing BaseMessage, ToolMessage, and SystemMessage.
I'll start off with the ToolMessage. So it's essentially a type of message where the data is passed back to the LLM after the tool has been called. And like the information which is passed is like the content itself, the tool call ID. That's what ToolMessage is. It's pretty self-explanatory.
Now for a SystemMessage, it's a message for providing instructions to the LLM. So like for example, if you've used LLM APIs before, you might have written, "You are a helpful assistant." That's exactly what a SystemMessage is. And don't worry, we're going to code this up as well, so you'll actually see what they are.
Now what's a BaseMessage? So in the comments, you can see that I've written "the foundational class for all message types in LangGraph." Now here's how this works. Think about the class hierarchy. So you know how you have a parent class and then you have child classes as well. Well, the BaseMessage would be the parent class, and these AIMessage, HumanMessage, ToolMessage, SystemMessage, and all these other types of messages will be like the child classes, and they will inherit all the properties of the BaseMessage because that's the parent one. I guess you can say the all-father or something.
But the AIMessage, HumanMessage, and all these child classes, obviously, they'll have their own properties, right? For example, the ToolMessage has its own content and tool call ID and all that stuff. So that's what the BaseMessage is. It's really the foundational class for all the message types in LangGraph. Cool.
Okay. So now if we continue, you can see we've imported ChatOpenAI. We've done StateGraph and END. These we've come across. We know what they are. And we've imported Tool and ToolNode, which we cover in the second chapter or second section of this course. These are different elements which we're going to be using in LangGraph.
Now what about this line: from langgraph.graph.message import add_messages? Now what does that mean? So this is a little bit different. This add_messages is a reducer function. Now if this is the first time you're hearing that, don't panic. It's not that hard.
So a reducer function is essentially just a rule that controls how updates from nodes are combined with the existing state. In simpler words, it really just tells us how to merge new data into the current state. Now here's the thing. If we didn't have some sort of a reducer function, updates would have just replaced the existing value or state entirely. And I'll give you an example for this.
So let's say I had a state where it was just "hi." I had one attribute, messages, and "hi." Now obviously I should have created the TypedDict and everything and formalized it, but just for simpler, for time-saving purposes I've done it like this. Now what if I had an update which says, "nice to meet you"? If I didn't have a reducer function, that would completely overwrite it.
In the previous graphs and agents we've made, we've appended it. But now that we're using so many different messages and calls and tool calling and whatever, we can't really always append everything, like it will get far too complicated. So that's why we need to leverage a reducer function. So if we didn't use a reducer function, it would just overwrite it completely. But if we did, like "hi, nice to meet you," it would append it. That's the key.
So in a nutshell, the reducer function really just aggregates the data in the state. This reducer function, and the reducer function which I'm talking about is add_messages. So once again, add_messages is a reducer function that will really just allow us to append everything into the state without any overwriting happening, because we want to preserve the state. Okay, cool.
So now let's actually code this ReAct agent. Okay. All right. Okay, I've cleared the screen now and let's actually begin like how we always begin with the creation of our state of our agent. So, TypedDict like such. Okay. And now we'll only have one key in this example, which is just messages. And now let's use the new type annotations we've learned. So, Sequence[BaseMessage] and reducer function add_messages.
So again, this piece of code is saying to preserve the state by actually appending it rather than overwriting. That's what this reducer function does. Okay. All right. Oh, and the Sequence[BaseMessage] is the data type, and this provides the metadata. That's why we have the Annotated keyword here. That's really it.
Okay. Now let's create our first ever tool. Now, how do we do this? Some of you who have come from LangChain might know how to do this already. We use a decorator, and we define it like this. Now, this decorator basically tells Python that this function is quite special. It does something, and well, it is special because it's a tool which we're going to use. So let's define our tool as def. Let's create a simple addition tool. Okay. So we'll say a: int, b: int. It's basically going to add two numbers.
And this is where docstrings actually come now. And I'll show you how important they are. For now, let's say, "this is an addition function that adds two numbers together." Okay. All right. And we just return a + b. Simple.
Now, how can we actually infuse these tools to our large language model? Well, first, let's create a list. Add, like such. Now, yes, at this current moment, I only have one tool, but in a few moments, we'll have multiple tools. That's why I'm adding this list for now. And let's actually create our model. So, model = ChatOpenAI(model="gpt-4o"). Again, I'm using GPT-4o because I've never had a problem with it, to be honest.
So how can we tell our GPT-4o large language model that these are the tools you can use? Well, we can use this inbuilt function called bind_tools. bind_tools, like that. And we pass in the list of tools we have. So that's tools. Pretty simple, right?
Okay. So now our large language model will have access to all of our tools. Okay. So now we need to create a node which actually acts as the agent within our graph. So how do we do that? Let me create just a simple function, like def model_call(state: AgentState) -> AgentState: Okay. Now I'm going to quickly copy this piece of code. Give me a second.
Okay. So what is this code doing? You can see that we're invoking the model, aka running the model, and this is the system message which we are asking. So we're explicitly saying to the large language model that, "You are my AI system. Please answer my query to the best of your ability." So that's what the large language model's task is to do.
Now if we want to get technical here, you could have written it in a slightly different way, and that way is through this. So remove this. We could have said system_prompt. Okay. So what's going on here? Remember how I said SystemMessage is also something which we imported. So the system message, like I said, is this line. "You are my AI system. Please answer my query to the best of your ability."
Now, either way would have worked. If I had just straight up passed this string into here, that would have worked as well. Personally, I think this way is better. Even though they achieve the exact same thing, I think this way is better because it's more readable. Okay? And you're only adding just one more import. Okay? So, I would prefer you to—I would really recommend you doing it like this so even the large language model knows that this is a system message. Okay, cool.
And this is just another way of writing the updated state. You know how we've been writing state['messages'] = something something something. Well, this is a more compact way of updating the state as well. So return {"messages": response}. So we update the messages with the response. No +=, this, this, this, or adding something. We can simply just write it with the updated state. Why? Because the add_messages, aka the reducer function, handles the appending for us. It doesn't overwrite it.
Now, if I ran this code and I built the graph and everything, would it work? No. Why wouldn't it work? Because think about it, the response, when we've invoked the model and we store it in the response variable, when we actually invoked it, we didn't actually pass in the query. How do we pass in the query? Think about it. All I passed is my system message. Where does the query go? So to be able to add the query, I actually have to add it like this: state['messages']. The query, it will be in the form of a human message. And the human message will be stored in the messages attribute, right? And now that we've passed that into our model as well, we can invoke it. And now this should work.
Okay. Okay. Okay, so now we define the conditional edge. Now why do we need the conditional edge here? I'll put the picture of the ReAct agent again here. You can see that the looping part, even like in the last one in the graph when we made the loops for the first time, you saw that it was a conditional edge which we had to use. And now that's actually going to come in play here. That's why I took so much time to build those graphs, because now the concept is coming.
So how do we define the conditional edge? def should_continue... Okay. So again, like always, we pass in the state and let's do it like this... like such... else return 'continue'. Okay. So as you might have guessed, 'end' and 'continue' will be edges which I'll define later in the graph. But what is going on here? Well essentially, when I'll pass in the query, when we've invoked the actual model, you will know that we'll create a list of tools, right? So what we're going to be doing is we're going to be getting the last message, and we're going to see if there's any more tools needed to be run. If there are, then we'll go into the 'continue' edge, aka we'll go to the tool node, and we'll select the tool, and we'll do all these actions and then come back. If there's no more tool calling left, we will just end, and we'll just exit the graph, and that'll be the case. You'll understand more what I mean when we've actually are testing and running the graph.
Okay. Now let's just define the graph. So like always, we create the graph, we initialize the graph through the StateGraph. And let's call the node our_agent. So the action will be the model_call, aka the underlying function will be this. Okay.
Now we create something called a ToolNode, which is also what we covered in the previous, in the second section or second chapter in this course. The ToolNode essentially is just a singular node which contains all of the different tools. So we only have one tool. If you see what this variable is, tools, we only have one tool, which is add. Don't worry, we'll add some more tools like subtract and multiply in a bit. But I just want to really solidify your concept of how we can add these tools and how the graph will work.
Okay. Now we obviously set our entry point and point it to the our_agent. Now let's add our conditional edge. So remember how I said there's two edges, 'continue' and 'end'. Again, 'continue' and 'end'. And if it goes to the end, we end it. If it goes to 'tools', then we go to ToolNode, which is tools. Okay. Okay. So yeah, this is pretty straightforward still, right?
Now we also need to add an edge which goes back from our tool to our agent, because that's how we're going to create a circular connection. Right? You can see that the conditional edge only provides a one-way directed edge from either the agent to the tool node or the agent to the endpoint. But we need another edge which will go back from the tool node back to the agent. And that's what this edge does.
And lastly, we need to obviously compile it. So we'll just say app = graph.compile(). Perfect. That's it. Now I've just created a a new helper function here which, this isn't part of LangGraph. I've just written this code because it will make everything, like the tool calling and everything, output in a much better way. So you'll see what I mean in just one second.
Okay. So now we actually can begin. So let's say the input is something like this. Let's say I want to add 3 + 4. Okay, simple. And this line of code basically streams the data. That's all it does. So, let's actually run this. Clear. And let's do it. Okay. So, remember we wrote add 3+4. Wow, look at that. So, we've added 3+4. It calls the tool and it even knows what tool to pick: add. And it gives us the result. The ToolMessage, as you can see, is 7, and the AIMessage, the final AIMessage is, "The sum of 3 and 4 is 7." That's it.
Let's try something harder now. Let's write add 34 + 21. So if we run this, you can see 55, because 34 + 21 is 55. And you can also see again all the tool calls and everything that's done, right?
I want to show you one, two more things actually before we add some more tools, which is this. If I remove this docstring here by commenting it out for now, let's clear and let's run that again. Error. Why? Because the function must have a docstring if a description is not provided. The docstring is necessary. That's why I included it, otherwise the graph won't work. Remember, it tells the LLM what that tool is for.
Okay. So now that we have that there, let's try this as well. add... uh 3+4. Again, this time I want both of them to be executed. So clear now. Do you think this will work? Let's see. add 34 + 21. add 3+4. Perfect. Brilliant. Okay. So you can see the result of adding 34 + 21 is 55. The result of adding 3 + 4 is 7. You can see how the tool was called twice this time. And that's the power of the loop which we created. Remember, we created the conditional edge, and then we also created that directed edge back from the tool node to the agent.
Let's try to make it even, give more complicated stuff. Let's say add add 12 + 12 something. So let me clear this. Clear. Let's see what happens. Wow, look at this. If I press enter... sorry, I messed up there. But you can see the results of the addition as well as 34 + 21 is 55, 7, 24. And you can also see that I called the tool—the sorry, the AI called the tool three times.
Now these tool calls is also an indication that the LLM didn't use its own like inbuilt information which it was trained on to come up with an answer. Right? Remember an LLM doesn't know how to do math. It just guesses the next output like through probability. But through this, we were able to actually add the two numbers.
So an important concept here is the LLM actually decides what should be passed as the arguments to each tool. So 3 + 4, like if I said add 3 + 4, it will actually create—it will actually input the numbers 3 and 4, and then this tool will handle the information, return it, and it will go back to the agent, and then the AI agent will decide what's the answer and everything. So that's how it works. Awesome.
Okay. Now let's make this even more complicated. Let's add some more tools. Let's add subtract and multiply. Okay. And the only change we have to do is instead of this one line, we just now include subtract and multiply as well. That's it. Otherwise, this line, this code largely stays the same. Now let's actually run this same command and see if it gets confused with the different tools it has access to. Now let's see.
Okay. You can see again that 55, 7, 24. Okay, it didn't get confused. Perfect. Let's now give it a different command. Let's say something like this. One second. "Add 40 + 12 and then multiply the result by 6." So now it has to make use of two different tools. Let's see if it gets that. Okay. Wow. Brilliant. So it first used the add tool and then used the multiplication tool. And you can see all the queries or all the tool calls and everything, and the final answer is 312. So 52 * 6, yes, it is 312. Okay. Wow, that works like brilliantly.
So now that we know that this is robust, what about if I add this? Let's say, "also tell me a joke please." What do you think will happen? Do you think this will break? Let's see. If I play this and run this. Let's see. Wow. Look at this. The result of adding 40 and 12 is 52. Multiplying that by 6 is 312. And here's a joke for you. Why don't skeletons fight each other? They don't have the guts. I swear to God, it's always the same dead joke.
But you get the point. This is so robust. It can handle even queries where it doesn't even need a tool. And that, ladies and gentlemen, is the power of LangGraph. So it's so robust, even if we don't need to use a tool, it will still give us an answer. And the reason it was able to do that is once all the tool calling is done, it passes it to the agent. The agent checks again, "Oh, I need to tell the user a joke as well," and adds that to the final information and then ends it. That's the power of LangGraph.
Okay, so after all of that, we finally now know how to create a ReAct agent. Yes, it was a simple ReAct agent, but the concept's the same. You can create your own external tools from now onwards, and you can create your own graph. And that was the whole point of this course, right? For you to actually understand how we can create these, how we can use different tools, and then the rest is up to you. It's up to your imagination. Okay, perfect. So now I will see you at the next subsection.
All right, see you there. Okay, people. So we've made great progress so far. So well done on that. But now we make a fourth AI agent. And this time we'll do things again slightly differently. Well, this time we're going to be making a mini project together. So the project's name is going to be called Drafter. And you'll see why in a minute.
So picture this. Me and you are working in a company together. And our boss comes up to us and she has a problem and some orders for us. So the problem is this. Our company is not working efficiently. We spend way too much time drafting documents and this needs to be fixed. Again, a valid problem.
So, what are her orders? She says, "You need to create an AI agentic system that can speed up drafting documents, emails, etc. The AI agentic system should have human-AI collaboration, meaning the human should be able to provide continuous feedback and the AI agent should stop when the human is happy with the draft. The system should also be fast and be able to save the drafts." Okay.
So then me and you start discussing and we are going to use LangGraph, obviously, and we come up with a sketch. Now the sketch of our graph is something like this. It obviously is going to have a start and an end point, and it's going to have our agent, and the agent will have access to tools, aka the tool node.
Now this looks similar to a ReAct agent which we covered in the last subsection. But there's a reason we haven't chosen to do that. See, we realize that one of the tools is the save tool. It will save the draft, right? That was one of our requirements. But obviously when we—once we've saved it, the process should end, right? But if you remember from a ReAct agent, the tools always goes back to the AI agent, not directly to the endpoint. And we don't want that anymore. So that's why as soon as the save tool is used, because the save tool will be within tools, right, it ends. So that is the structure we have chosen to go with. So the only thing left is to actually code this graph. So let's code this together then.
Okay. So let's actually code up this drafter project then. So you can see I've already done all the imports and I've loaded up my .env file. Now all of these imports are imports which you've already encountered before. So there's no point in looking at them again. But the first thing which I'm going to do is I'm going to be defining a global variable.
Now this global variable, yes, it's a bit odd defining global variables, and there's a reason which I've done it, and this will become more apparent as I go through the code. But just as a heads up, the reason I've defined a global variable in this case is to actually pass in a state in tools. The correct way to do it in LangGraph is through something called injected state. Now injected state is beyond the scope of this course. So the workaround on that is to use a global variable. And what will happen is our tools will—whatever updates are made, we'll update the global variable, and then when we go on to save it, the save tool will use the contents in this global variable and save that into a text file. So that's why this is included.
Okay. So now let's define our agent state again. And the way that's done is the exact same way we did last time. class AgentState..., messages: Annotated[Sequence[BaseMessage], add_messages]... as the reducer function.
So now we define the tools, and there will be two tools for this. The first tool will be the update tool and the second tool will be the save tool. So let's start off with the update tool, and I will obviously use the decorator and then create def update and then we need to pass in a parameter, content.
Now just as a refresher, whatever parameters you pass or you request, who actually gets those parameters? Well, the LLM or your model in the background, that's what will automatically pass the parameters for this tool. So, in this case, the content parameter will be provided by the LLM in the background. So, you don't need to worry about that.
Okay. So, now we need the docstring obviously, and I've just created a simple docstring which just "updates the document with the provided content," because that is exactly what it does. So now we define—to interact with the global variable in Python, you obviously need to define it, you need to code it like this, and then you need to update your document_content, aka the global variable, with your current content. And then you just return again a statement to the large language model telling it that we have successfully updated it. So I've written, "Document has been updated successfully. The current content is this," which is the content which we store in the thing.
Okay. So now we define our second tool, which is the save tool. So again, same decorator we use like this. And now we request the LLM to give us a file name as well. So it should give us a suitable file name, which will be a suitable file name for the text file. And this save tool will automatically handle all the save logic. So as a docstring, I pass like this. So, "saves the current document to a text file and finishes the process." And the arguments are filename, which is the name for the text file.
Now, I've specifically mentioned that we're going to be using a text file so that the LLM knows that the file name which it needs to pass has to have a .txt in the end of it. Now, if it doesn't by any means, to make the graph, to make the entire code more robust, I've also written this if statement such that if this file name doesn't end with a .txt, just put a .txt there, just as a robustness measure.
Now again, we need to call the global variable again. So global document_content. Okay. Now this next bit of code, this is not LangGraph. This is just whatever you put in the tool. It doesn't have to be LangGraph related, right? So this piece of code is just some code which allows you to save the contents, aka the content stored in the global variable under the file name as a text file. And I've also added this exception, which is a good thing for debugging purposes, where if there's an error, it will tell me exactly what the error is and then we can fix it. Okay. So hopefully there won't be any errors.
Now we create a list of tools which again will be update and save because we only have two tools. And now we actually call the model, and how do we call the model? Like such. Now let me ask you a question. Is this it for the model definition or do we need something else? Well, there's a reason I asked that question, right? We've forgotten to do .bind_tools(). So .bind_tools(tools). So that will do.
Okay. Now we actually initialize the agent itself, or the function which will—because remember the agent will be a node in our graph. And what will be the function behind that? It will be this function which we're about to define. So let's write this as def our_agent. And again, we need to pass in the state, the AgentState, and it'll return the AgentState. And okay, so now this—not docstring, this—we need to pass in a system message to our LLM, right? Now this LLM, this system prompt will be quite large, so get ready.
Like such. So in this system prompt, I have specifically said this is a system message and the content is this: "You are Drafter, a helpful writing assistant. You're going to help the user, aka us, to update and modify documents." And I've also written some more stuff about what the update or what to do if the user wants to update. We use the update tool. We need to use the save tool to save it, and to always show the current document state after modifications and all that stuff. Cool. Okay. Oops. There we go.
And now it's time for some robustness measures. So when we're first initializing the graph, like when it's the first message we're writing, obviously we're not going to straight up say, "How would you like to change the document?" right? Because we haven't passed in a document yet. So if messages, this part, if there's nothing in it, we will have to say something like an introduction message, right? So this is how you can do that.
So we can say if not state['messages'], aka if there's nothing in the state messages, then we can say, "I'm ready to help you update a document. What would you like to create?" and then it collects the user input and passes it, stores it as a HumanMessage in this user_message variable.
Now what if I've already passed in a message or we are on the process of updating our draft or drafting it? Well, to do that, we need this else statement. And what does this say? Well it says, "What would you like to do with the document?" So this assumes that there's already stuff in the messages key in the state. How do you want to update it further? And then we also print it under this emoji in the terminal so the user can also see what they've inputted. And then this is also stored in the user_message. All right.
Okay. Now we combine all of this, all messages, aka the system prompt which was the system message, and we create a list of the state messages and the user message, the new message which we want, aka the update. And then we just invoke the model. And how do we invoke the model? You just use the model.invoke(). Okay, so pretty basic code so far. There's nothing hard or nothing extraordinary or something we haven't seen before. All of this we have seen before.
And now the rest of this function is just a print statement which I've included. This print statement is just for making things look prettier on the terminal. That's all it is. You can see the two print statements. There's the AI response which will be printed, and then there'll be the tools, whatever the tool messages are, that's also printed. So that's the whole point of it. There's nothing to really talk about here.
And then we also need to obviously return the updated state. Now remember last time I showed you that this is also a really convenient, concise way to update the states. So from now onwards, we're only going to update the states like this.
Okay. Now we create our conditional edge function, or the function behind the conditional edge, because remember, let me open this up. So the conditional edge which I'm talking about will be this, this conditional edge. So there, from tools, there will be either the choice of going back to the agent or the choice of ending it. So we need to create the underlying function behind that. So let's create that now under this.
So should_continue. We've done this many times before. It will determine if we should continue or end the conversation. And remember, continue or end the conversation. Okay, makes total sense.
Okay, so now we do this. So we get the messages, and if there's nothing in the messages, well, obviously we'll need to continue, right? It won't go to the end part. And this is just as a robustness measure, to be honest. Okay.
So now this piece of code is basically saying look at the most recent tool message, or the recent tool we've used, and we need to check if this tool has used the save tool. Now why? Remember how we have two tools? We have either the update tool or the save tool. If we use the update tool, well, we will obviously need to use the continue branch, right? But if we use the save tool, well, after you saved it, there's nothing else to do, right? You finished your draft, you finished everything, so you might as well end the program. That's why this end tool.
So for the continue, to go through the continue edge, we have to use the update tool. And to go to the end edge, you need to use the save tool. So it should make sense now, but don't worry if it doesn't. We will do some more print statements so you see the workflow. Don't worry.
And lastly, we need to obviously return continue, because by default, it's checked here that it's used the save tool. The only other tool left is the write tool, and sorry, the update tool. And the update tool means that we have to go to the continue edge, right? Okay. And that's that function done as well. So pretty easy still.
Now this next function is again, I only coded this just to make the print statements in a more readable message format when we printed on the terminal. So you will see where this comes in play when we actually start invoking the graph and seeing how our process is going. Okay, cool.
Okay. So now we actually create the graph. So how do we create the graph? We've done this many times. We will initialize it through a StateGraph. And now we will add the nodes: agent and tools. And the tools will be a ToolNode. And again, if you notice back, we had one node, two nodes, the agent node and the tools node. I'm keep—I'm like reflecting back and forth between this diagram and the code so I can show you exactly what we're coding.
Okay. So again, agent and tools node we've done. Now we will set the entry point at agent, which is the start point, aka this part, right? And now we're going to add an edge between agent and tools. Now we need to obviously create this edge because the agent needs to go to the tools, right? And then this edge, this directed edge and this conditional edge creates the loop which will allow for the human-AI collaboration. All right.
Okay. So now we add the conditional edge, and that's the conditional edge which I was talking about, the continue and the end, aka this conditional edge from tools. Okay. And now the last thing we need to do is just compile it because we finished the graph completely, right? There's nothing left. We've done the start point, we've done the end point, the end, this conditional edge is done, the node's done, and then this directed edge is done, and the start point is obviously done because we've created a directed... so you can see the entire graph we have created just like that. So again, nothing too hard. Cool.
Okay. So now we actually run the program. And to run it, I have just written this function so that everything is in a more compact way. This is just to invoke the graph. Okay. And let's do that. So that was the entire code. And this code will allow for human-AI collaboration. Now, yes, we used a global variable. And again, there is nothing wrong with using a global variable. I know some of you might frown upon it, but again, there's nothing wrong with it. If we wanted to use more complicated stuff from LangGraph like the injected state or even using something like commands and interrupts, we would have to write the code slightly differently. But because this is a beginner level course, we've just disregarded that completely and we found another way of performing human-AI collaboration. Awesome.
So let's actually run this now. So let's write python drafter.py and you should be able to see all of the things. I made my face cam slightly smaller so you can hopefully see everything. Perfect. So "You currently have an empty document. Could you let me know what you'd like to add or create in the document?" So what would you like? So let's say we are writing an email to our colleague Tom saying that we can't make it to the meeting. So let's say, "write me an email to Tom saying I cannot make it to the meeting." Let's see what it says.
So it says, "Hi Tom, I hope this message finds you well... Please let me know..." Okay, let's now give it some feedback on how we can improve. And also you can also see that it's used the update tool as well. Perfect. So, let's say, "make sure to also have specified that the meeting was supposed to be at 10:00 a.m. at some random location." Canary Wharf. Okay.
Okay. Let's see the updated thing. "Hi, Tom... meeting at 10... Canary Wharf... due to unforeseen circumstances..." Uh, let's, I don't like this "Your Name" part though. So, let's say "my name is Ved," and it will update that as you can see. Perfect. What else do we want to change? We can also say something like, "but tell him that I can make it at 12 p.m. in New York, some random location," okay, I'm making this up, but you get the plan, "the next day." So let's see. And perfect. It's updated it. "However, I am available to meet at 12:00 p.m. in New York the next day." Obviously, it's complete rubbish like the timings of the location I've written. But you can see how we can just use human-AI collaboration here.
Uh, one more thing which I don't like is this part. I don't like the fact that it's not a new line. So, I mean, I'm being a bit picky here. We can say something like, "put the 'I hope this message finds you well'..." Awesome. And as you can see, that's done as well.
So now let's say I like it. "Save it please." And what happens? You can see that it used the save tool. The tool results is "Document has been updated successfully. The current content is this" and "The document has been saved to unable_to_attend_meeting_email.txt". Now remember, we never passed in the file name at all. That was all generated by the agent itself. And to check, we need to go on "unable to attend meeting." So let's see here it is. And you can see it's the exact same email we said. So, "Subject... oops, best regards, Ved." All the exact same content. Perfect.
And we don't even need to make it so that we're drafting emails. We can even draft short stories. We can draft whatever we want. In fact, we can also pass in a previous message. So, the reason it started off like with nothing is because we passed in an empty list. But if you wanted to, we could have written something over here with a pre—with an already existing email or already existing document, and then the model, the agentic system would know that this is what the current content is. "How would you like to change that?" And that is exactly how we will be able to operate on our existing ones.
So you can see that this is quite a robust thing. If we want another example, for example, uh, let's say python drafter.py. Okay, now watch this as well. Look how robust this is. If I say something like, "write an email," it actually gives back questions. So, "Sure, what would you like the email to say?" So remember, it didn't even go through any tool here. Using LangGraph, we can really make the agents quite robust, and that's the thing which I wanted to show you. It doesn't always have to pick a tool. Its own LLM, the agent itself, because remember the agent node has an LLM in the backend. The .bind_tools() function allows it scope, like it increases the scope of it by providing some tools, but that doesn't mean it has to use those tools. If it doesn't feel the need to use the tools, it won't. And in this case, it wanted to ask us more questions about it. So it would say, "Sure, what would you like this email to say?" because to be fair, I only wrote three words. But that was what I was trying to show you. So let's just clear this now because we don't need to.
And yeah, you can see perfectly works, human-AI collaboration in LangGraph, and this is actually somewhat useful as well. Now yes, of course, you can use GPT-4, Canvas, and all of that stuff, of course, but this is how you would do it in LangGraph. All right.
So, if you would like an extension to this, what you could do is add a voice feature as well. So, maybe you could add, use OpenAI Whisper for speech-to-text conversion, or add ElevenLabs for text-to-speech conversion. And maybe you can make it voice-based, because right now I'm giving it—how am I communicating? With text mode. What about voice mode? You could also include a GUI to this. There's a lot of stuff which you can do. You can even have your own knowledge base as well and include that. So a lot of potential with this. If you want a homework for this specific project, there you go. All right. Okay. Cool.
So that's the end of this subsection. Awesome. So now let's build our fifth AI agent. And some of you might have been looking forward to this. It's Retrieval Augmented Generation, RAG. So what will the graph look like? It will look something like this. Again, start point, end point, really similar to what a ReAct agent was, right? But we have two agents in this case. We have a retriever agent and we have our main agent LLM, right? So, and it will have obviously a conditional edge, a loop, and everything. Again, we're bringing everything we've learned so far and merging them into one. And we're also going to be learning a little bit about RAG. Now, I'll assume you know what RAG is. I'm not going to go too much in detail into the nitty-gritty of it. But again, on the surface level, I will obviously explain what RAG is about and everything. Okay. So, if you're excited, let's jump to the code.
Okay. So, now you can see that I've already done all of the imports which we'll need. But you'll notice how there are these four imports which we haven't come across yet. Now, rather than explain them from the get-go, I will explain them as they come because it'll make more sense, it'll make more intuitive sense that way.
Okay. So now I'm going to be loading our .env file which contains all the API keys. And this time I'm going to be initializing our LLM differently. Well, slightly differently. It's the same LLM, but why did I say differently? Because I've passed in a new parameter called temperature. Now for those of you who do not know what temperature is, it's essentially a parameter which depicts how stochastic the model outputs, how stochastic you want the model outputs to be. So because I've set it to be zero, temperature=0 makes the model output more deterministic. Similarly, if I had set the temperature to be one, the model output would have been more stochastic.
Okay. So now we create the embedding model. And the embedding model is what's going to convert our text into vector embeddings, right? So this will be the layout for it. Now please note one important thing, which is the embedding model has to be compatible with the LLM we're using. You can use whatever LLM you want, but make sure the embedding model is compatible with it. For example, let's say we're using GPT-4o, an OpenAI model, but the embedding model we're using is from Ollama, some random model. Now, they wouldn't—most likely they're not going to be compatible. Why? Because there's so many differences between them. One potential difference could be the vector dimension. So just a rule of thumb, make sure the LLM and the embedding model is compatible.
Okay. Awesome. So now we're going to specify the PDF path. So this is the "stock_market_performance_2024.pdf". And essentially this is just a document which I created which contains a lot about the stock market performance. Okay. I can show you that right now actually. So this contains nine pages and is just a document containing some stock market details in 2024. Okay. Awesome.
So now, in case you've specified the—you have put the PDF in a wrong directory or if it can't find it, this error will pop up. So again, I've just put this for debugging purposes if you use the code which I provided on GitHub. Okay. Now this will load the PDF, and you can see PyPDFLoader is one of the imports which we made here. So again, it's in the name and the comment. It just simply loads the PDF.
Okay. And this try and except command just checks if the PDF is there. And pages = pdf_loader.load(). So this essentially says how many pages are there in the document. So you can see there's nine pages in our document. So if I run this command, if I run this, so clear python rag_agent.py, it should say nine pages. So there we go. "PDF has been loaded and has 9 pages," as expected. Right?
Okay. Now it's time for the chunking process. Now what is chunking? First look at this. There are two parameters which I've specified. chunk_size which is 1,000 and chunk_overlap which is 200. So let's break this down a bit. Going back to our document. So chunk_size was 1,000 tokens. So let's say that this was a chunk, for example. Okay, obviously that's not going to be 1,000 tokens, but just for demonstration purposes, let's assume it is. So this is saying as soon as you've reached 1,000 tokens, you create a new chunk. So let's say 1,000 tokens ended here. So this would be the start of a new chunk, like such. Okay. And you keep going and going and going until the end of the document.
But what about the second parameter? The second parameter specified overlap, and that's essentially saying, let me use it in a different color, that your chunks, consecutive chunks should have some tokens which exist in both. For example, because it was 200, the second chunk is not going to start from here. It's actually going to start something like this. They're obviously going to be the same length in terms of tokens, but they will have some tokens which will be in both chunks. So for example, this part will be in both, because that's the overlap. 200 tokens to be precise. Okay, so that was just a brief overview of what chunks are in RAG.
Okay, so that's that part done. And again, this RecursiveCharacterTextSplitter is one of the imports we did. Okay. So this text splitter chunking process, we now apply it to all of the pages, all of our nine pages in our document. Okay. And this piece of code is essentially saying this: the Chroma vector database. We're going to be using a Chroma vector database to store all of our vector embeddings, by the way. But the place where we want our Chroma vector database to be will be specified in this file path. And the collection's name will be called "stock_market". Now, you can specify it wherever you want obviously, but I've just specified it to be in the same folder.
Okay. So this is just an if statement to make sure that if this is the first time we're running this command, if we're running this file, if this collection doesn't exist, we will create the collection in the specified directory. Okay, again not too hard yet.
Now here comes a try-except block. So this is where we actually create the vector embedding, where we create the Chroma vector database. And these are just parameters which I specified. So for example, how I want the pages to be split, what embeddings to use, where to store it, and the collection name. The collection name being "stock_market", right? And if there is an error, it will throw an error, and if it's successful, it'll print on the terminal.
Okay, awesome. So now we create something called a retriever. So the retriever is quite important in RAG. It's, well, obviously the first part of RAG: Retrieval Augmented Generation. So the retriever is what actually retrieves the chunks, the most similar chunks. The search type which we're going to use is similarity. It's just the default anyway. You don't really need to know how that works, to be honest. But what you do need to know is this part.
So in this code, I have made sure that every time it goes, the amount of chunks it outputs back is five. Why? Because k here is the amount of chunks to be returned. So I've set it as five. Now, I'm pretty sure if we go to the actual documents here, the default is four. Okay. So this is just a parameter which you can set. Now you don't want it to be too high of course, or too low. So you want a good middle ground, and four or five is a good middle ground in my opinion.
Okay. So now let's create our tool. So again, we use decorator @tool. And the tool's name is going to be this retriever_tool. It will input—it will take in a query and it'll output a string. So the docstring is as follows: "This tool searches and returns the information from our document." Okay, self-explanatory. And obviously, we need to invoke it to the retriever. So whatever query we ask, for example, "What was Apple's performance in 2024?", that will be the query, and that will be passed to our retriever, which will grab all the chunks, the top five most similar chunks.
Okay. Now if we don't, if there's nothing similar which it finds, for example if I say something like, "Who's Bob the Builder?" something like that, right? Obviously, Bob the Builder is not in this document. So it will return, "I found no relevant information in the document," and this will be passed to our LLM agent. Okay. If it does find it though, what we'll do is we'll create an empty list and we will store all of the similarity, all of the chunks which it found, and then return those results through this.
Okay, still it's quite easy still. And this piece of code we've already come across. There is only one tool. So we just bind that tool to our LLM. And this code we have also done many times. It's the creation of the agent state. And again, we're using our add_messages reducer function. All of this we've covered many times, so you should be quite familiar with it.
Okay. So now we create the should_continue function, and the should_continue function is going to be the underlying function between our conditional edge, behind our conditional edge. So it will check if the last message contains any tool calls. If it does, then we proceed. If it doesn't, then we'll just end. Right.
Okay. So now we specify the system prompt. Now this system prompt is going to be quite big. So let me copy and paste it here. Now the reason it's quite big is I want to specify as much information to the LLM so that it knows what to do, right? So I've just said, "You're an intelligent AI assistant who answers questions about the document loaded into your knowledge base." You can read the rest if you would like. But I've also written this: "Please always cite the specific parts of the document you use in your answers." This is really just to make sure it's not hallucinating, right? Because as we know, hallucination is quite a big problem with LLMs. So this is just to make sure hallucinations are kept to a bare minimum.
Okay. All right. So now we create a dictionary of our tools, and we now create the underlying function which will be our LLM agent. So this function will call the LLM with the current state. And you can see it converts the messages to a list, passes the system messages, and passes it to our LLM, which is defined like this. And it will just return the messages, aka the updated state. Okay, this should be like such. Okay, awesome.
So now we create our second agent, which will be the retriever agent, which you saw on the—in the graph which I showed you in the introduction. So the retriever agent executes the tool calls from the LLM response. So what is this code actually saying? Well, all in all, this massive piece of code really just says if there is a tool, if the tool name is within the—is a proper specified tool, aka if it's retriever_tool, then actually run it. If it's not, then we will output the result as "Input incorrect tool name. Please retry and select the tool from list of available tools." It's just for checking if the tool which is decided from the LLM is valid or not. So that's all what this is doing. If it is valid, it will invoke it, and we will store the results like this and we will return that. Okay. Again, this should be AgentState, like such.
Okay. So we've created all of our two AI agents now, and now we're going to create the graph itself. So like how we've done, initialize it through StateGraph, and then we're going to add our two AI agents as nodes with their respective actions. And we are now going to add the conditional edge, so which will be—llm, which will start from llm, and the should_continue function is the function which will be the underlying function. And this is a true-false statement. And this is the edge, the set entry point. All of this we've covered many times, so again, should be quite familiar to you. And last but not least, compile the graph and store it in a rag_agent.
Okay, one last thing though. I've created this function, and this function is just a function which allows us to keep asking questions to our graph and keep receiving answers back. And if you want to exit, we can write either "exit" or "quit." And it's just a simple while loop. That's all it is. Okay. And it prints the answer.
Okay. So that's the actual code complete. Now we're going to test it and see if this is reliable or not. Okay. Okay. So, let's actually test this now by doing python rag_agent.py. Let's run this. Okay. "PDF has been loaded and has 9 pages. Created Chroma vector database vector store." So, where is this stored? Well, you can see that this is—by the way, this will all be on GitHub as well. But this is the Chroma database and its respective bin files. Okay. And we can even view it. But it'll look something like that. Okay. But because this has been created, this is a good sign that everything is working.
Okay. So, let's ask a simple question. Uh let's ask something like, "how was the S&P 500 performing in 2024?" Enter. So it's calling the retriever tool with the query. Its result then puts that complete back to the model, and the model has given us this. "In 2024, the S&P 500 delivered a total return of this, with a 23% increase..." "...late 1990s," and all of that stuff, "Magnificent Seven," and has given us the respective citations as well.
Now, how can we verify this is correct? Let's see. So, notice how if you remember this part, "the total return of approximately 25%." Well, the reason I prompted—I asked it for this is because that's exactly what over here it stated, "the benchmark roughly at 25%... 23%..." Remember this late 1990s part? That's exactly what this is saying here as well. And this was correctly defined in the first document. So this is clearly working now, right? It can't have made up this information. So that means our RAG is successfully set up.
Now I can ask as many questions as I want now. But now let's see if there is something which is not included in the RAG. So for example, we can say something like, "How did OpenAI perform in 2024?" Retriever tool called, back to the model. Okay, now look at this. The documents do not provide specific information about OpenAI's stock performance, which is true because OpenAI is not a publicly traded company. And yeah, it got that correct. So no hallucination there. So you can clearly see that this is working completely fine. And that, ladies and gentlemen, is how you create a Retrieval Augmented Generation graph in LangGraph. Okay. Awesome.
All right people. So that brings us to the end of this course, and I hope you liked it, and I hope you learned a lot about LangGraph. Now, although this course is finishing here, your journey in LangGraph is just beginning. Just think about how many cool AI projects, AI agent systems you can make now. Maybe your own J.A.R.V.I.S. as well.
Now, if you have any further questions related to the course material or just things in general or just want to say hi, you can always message me on LinkedIn. With that being said, thank you so much for watching this course and I hope to see you in another course. Take care.