Complete one lesson every day to keep the streak going.
Su
Mo
Tu
We
Th
Fr
Sa
You earned a Free Pass!
Free Passes help protect your daily streak. Complete more lessons to earn up to 3 Free Passes.
To build a multi-agent system, you'll use Google's ADK or agent development kit, which is a framework for developing agents. In this lesson, you'll learn how to create and run one agent as well as a team of agents. All right, let's go. In this lesson, you'll familiarize yourself with Google's Agent Development Kit, and you'll use that for building agents throughout this multi-agent system. In this lesson, you'll learn how to create and run an agent using ADK. and the imports that we're doing here, of course the standard OS and then a bunch of stuff from Google ADK itself. We're going to import this Agent, which is going to be the key thing for actually describing what an agent is. For interacting with an LLM, we're going to be using the LiteLLM library. So we're going to import that here and this is a a little wrapper for letting Google ADK talk to OpenAI. And then we're also going to need some memory for the agent. For the memory, we're going to use InMemorySessionService, and then finally the agent also needs a way to run, so we're going to import a Runner. We also have some typing stuff here that's not really so important for how all this works. Let's go ahead and make sure that all that imports. Great. All the libraries are imported. So the next part then is to start defining the models that we're going to use. We're going to be using OpenAI for this course. So let's go ahead and import, we'll define what the the model is going to be itself is OpenAI's GPT-4o. Again, we're going to be using LiteLLM for actually reaching out to the OpenAI API. And we'll just going to go ahead and try a sample execution of completing a chat with that. So if you look at the chat here, you'll notice that we're just setting up a message that we're going to send to OpenAI and we're going to ask it the question, Are you ready? So if that all works successfully, we'll get a good message from OpenAI letting us know that this works. Looking at the ModelResponse, there's a lot of information here, but the key part is that the response includes a message from OpenAI saying, Yes, I'm ready! How can I assist you today? OpenAI is all set to go. We're also using Neo4J today. so we're going to import a helper library that combines Neo4J with Google ADK in a nice way. And that library is something we can take a look at to see the details of what's going on. The import that we're going to use is just from neo4j_for_adk import the graphdb library. Let's take a look at those details. Okay, now we're inside of Neo4J for ADK. And you can see it starts off pretty simply. We import some stuff from the operating system. We of course load up the environment. And then from Neo4J, the Python package, we load up the GraphDatabase, and you also get a Result class definition. Now, a bunch of these functions that are in here are just really nice for just wrapping up how you interact with Neo4J and presenting the results from Neo4J in a way that Google ADK likes to see things. One of the key things is that results should come back either as a dictionary with a status that is a success or a status that is an error. These two helper functions help do that. You can call tool_success or tool_error along with some extra arguments and actually get a nice formatted result from any kind of call. To that, we then go ahead and also integrate a helper library. results that are handled by Google ADK need to be easily persisted. So this function called here to_python basically goes through all the different kinds of results you can get back from Neo4J and makes them easily serializable in a nice format for for Google ADK. You can look at the details if you want to see what's going on here. The key part of course is this function that takes the Neo4J result, you're passing that in as just a Result. Basically, calls to the Python package. Packages all that up into a result that is easily used by Google ADK. Each of those helper functions is then also used by this upper level class called Neo4jForADK. And this is really a wrapper that combines a Neo4J driver, initializes the environment variables used there, allows access to the driver, and then has a special function here for sending queries and it'll send any kind of query you'd like to, but then uses those helper functions to actually reformatting those results into a nice Google ADK result. At the end then, because we're just going to have one driver and one connection that we're using, we're going to have a singleton of this class that we're use. This is the graph DB that we're going to actually expose as a variable and use that through all the notebooks. Okay, we took a look at Neo4J for ADK. Let's continue with the notebook. So now we've got everything set up, and we can start to define an agent itself. Okay, with our Neo4J wrapper ready, let's go ahead and give it a try. You can just send in kind of Neo4J query that you'd like to, as long as it's valid Cypher. Here the cypher that we're sending is a very simple cypher statement that doesn't do any work other than directly return a result. Here the result is a string, Neo4J is Ready! passing that back aliased as a message variable. So if we run this and then print the results, you'll see that it's been packaged up into this nice little dictionary with a status success and then the query result containing an array of all the rows returned from the database. Great, with OpenAI ready and Neo4J ready, we can start to define find our tools our tools themselves and the agent eventually. Tool definition is an important part in actually defining an agent. Without tools, an agent can do a lot of thinking, maybe have some chats, but it can't really do any interactions with the environment or with the world around it. So we're going to define a very simple tool just to kind of illustrate what kind of things an agent can do. Hello world is the classic of course. So we're going to develop the hello world version of for an agent's tool. and we're going to call it say hello. So tools can be simply defined as functions. Here the function is just to say hello. It takes a single argument, person_name as a string, and it returns a dictionary. Of course, that dictionary is the Google ADK dictionary that likes to see all results in. And it also has a couple of important parts here. There's a doc string that's here, which is always nice when you're writing code. It's also especially critical when you're developing tools for Google ADK. This doc string is what lets Google ADK understand what actually this tool does. This gets passed along to the LLM when the LLM is told, here's all the all the tools that are available and also here's what those tools are described as doing. So, this is a couple of important parts that we should go through here. You describe what the tool does. Here, it just formats a welcome message to a named person. Of course, passed in as person_name. You describe the arguments that are being passed in and also the result. The result here is consistent through all the tools because the result is a dictionary that Google ADK likes to see. But you can add a little extra detail if you want to here. For instance, if you know the kinds of errors that might come back, you could explain that here as well. Inside, the function is incredibly simple. All we're doing is using Neo4J again to actually do a hello world. We're going to call out to Neo4J, sending a query where we're still going to return a hello to you, but then we're also going to concatenate that with this person_name, which is passed in as an argument to the function. That's going to get passed in as a query parameter to this query. Whenever you see that dollar sign in a Neo4J Cypher statement, that means here's a query parameter. So, here in this array, in this dictionary that's being passed in. you'll see the person_name is being passed in as person_name. That'll get substituted in as this variable here, not as a template substitution, but as the value gets passed in. and then this whole query will get executed. So let's define that function. And of course, because this function is just a normal function, we can try to run it. So we can print saying hello to ABK and expect to see a very nice Google ADK formatted result that was successful where the career result that was sent to Neo4J has the reply, Hello to you, ABK. doing that concatenation. Fantastic. Now just a side note here about query parameters, part of why we're doing that, this is good discipline for any kind of query language that you're using. Most query languages have the ability to have query parameters. And one of the reasons you do that is to avoid injection attacks, right? So that people can't pass in values that look like SQL statements or Cypher statements or any other query language statement as a value and have that concatenated into a string. that can cause all kinds of mayhem. So instead you pass in a query parameter, and here for instance if I pass in the name and say, actually the name's going to be, hey, return some string here, this could be some malicious code instead that gets passed in as the person's name. If you just concatenated that as a string, all kinds of bad things can happen. Instead because it's a query parameter, that still just behaves as a variable. It gets passed in, it gets concatenated as a string with the other string, and the result is simply hello to the somewhat friendly malicious injection attack. and of course, it's been avoided now. We have a basic tool available, now we can define an agent that's going to use that tool. Now there are a couple of components of defining an agent, it's common across all frameworks. They'll do some variations of the same idea. Let's take a look at what's happening inside of Google ADK to define an agent. Now if you recall earlier, we had imported this Agent class from Google ADK. So we're going to make an instance of that class and it takes a bunch of parameters. So the first thing that's important is that every agent is given a name. Here we're going to call it hello_agent_v1. This is in case you have multiple versions of the agent that you want to have at the same time, or if you want to actually have different version instructions as well. Having an idea of what the version of this agent is is very helpful for debugging later on. You also pass in the model that you want to use. If you recall, we defined LLM before is being a call through LightLLM out to open AI. So we're going to use that here. and these next two are pretty critical for how the Google ADK actually orchestrates and executes agents. The first part is this description of the agent. This is a little bit like the doc string of the tool, what describes what does this agent do. This lets Google ADK and also the other agents understand what is the purpose of this agent and is there occasions when I should call this agent rather than doing something myself. What's called agent delegation. So that's for other agents to know what to do with the agent here. Then for the agent itself, you provide some instruction. And the instruction is basically the same kind of thing you would have done with uh when you're doing prompt engineering and you're defining the system prompt for uh the LLM. This is the same kind of thing that you would do there. This is a hello world agent, so it is, of course, very helpful. It's a helpful assistant that's going to chat with the user and it really only has one tool that it wants to use. So you describe that tool here so that the agent understands, not just from the tool definition, but also from your instructions that you're giving it, how to think about that tool and when to use that tool. So here if the user provides their name, you know, use the say_hello tool to give them a custom greeting. And then the final part, of course, is we have to make those tools available. The tools get passed in as an array of just the function names themselves. So we're going to pass in say_hello, and then the agent then has access to that tool. It knows what to do with that tool because we gave it instructions and other agents if there are other agents know what this agent's role is. Now that we have an agent and the agent has the tool and it has instructions about how to use that tool, now we need to run the agent. This is the final part in creating a very basic agentic system. An agent has to have an execution environment. So if you look at this diagram here, you'll see that inner box here. This is the execution environment that happens. There's a Runner class that actually manages all the event loops basically for calling out to the LLM, passing results. from the LLM to particular agents and coordinating all the agents themselves and how they execute. Each of those Runners also has access to various Services, whether that is in memory services for like doing actual memory, memory could be in memory or it could be, you could use a database like Neo4J for memory as well. that gets you access to Storage. Then there's the Execution Logic that inside of the execution loop, here are the steps of for actually running things and coordinating the agent, whether agents are running in parallel or in sequence or in a loop. This all together that are actually is the execution environment for the agent. Okay. We're going to do this by hand first and then show how we can wrap this up to a little convenience method. When you're doing your own development with Google ADK, Google ADK has really great tooling for just defining an agent and then using the tooling for actually just running that agent, providing it the full execution environment that it needs. We're going to be doing that by hand inside of the notebook. So let's do that one step at a time and then wrap it up into a little helper. So there are a couple things you're going to need to actually set up the execution environment. Every runtime actually runs within a session. You could of course have multiple sessions that are running in some production environment. Here we're going to have a single session. we're going to go ahead and just set up the in memory service which provides context and state for the agent as it's running. We're going to create the session service based on that memory and also for the particular agent that we're writing. And also assuming that we have a user, we're going to assume that we have a single user, a single application that we're running and a single session. All that is going to be used here in the create_session to actually create a single executable state to actually be run and then the runner is going to run that state given the agent that it has. So here the runner combines the agent that we want to run, our hello world agent. The app_name is going to be the same as we passed into the session here. And also the session_service is going to be the session within which this agent runs. Again in a production environment, there might be multiple sessions, there might be multiple applications that are using this agent. That's why these are all different from arguments to run. Okay, now there's a lot to walk through to just actually do the running of the agent as well. So let's go through this step by step and take our time. We know that we just have this runner available that we just defined. And we're going to do just a single loop through with a single user message and allow the runner to execute the agent reacting to that message from the user. So we're going to define an user message and I'm simply going to say, hello, I'm ABK. And we'll go ahead and print that message out in a friendly way so we ourselves can see as this is running what's going on. That message that is just a string has to get packed up into a data structure that Google ADK the runtime environment expects. Usually this is handled behind the scenes for you of course, but we're going to do this by hand. So we're going to create this content class here. and the role of the content that we're creating is from the user, this is a user message. content can have multiple parts. So we're going to create an array of parts where one of the parts is has some text where the text is a user message. This is all bundled up to do more complex things. Here we're just trying to run a single message from a single user, but this gives you lots of flexibility when you have lots of extra things going on. and this is basically creating a content event, but then has to get processed by the agent runtime system. Also, we're going to go ahead and set up a response from the agent system, in case it doesn't do anything. So after you pass in a message, we're going to actually go through running that message and letting the agent get a chance to respond, but it's possible that the agent doesn't respond. There's this notion of having a final response, which is a flag that an agent can set in different ways to actually indicate, okay, I'm done thinking about this or I'm done processing this. So you can go on and go back to the user. In case the agent does nothing, a final response may not get set. So we preset it here with a default value to let us know that nothing has happened. We're going to run through without any verbose output right now, and you can see in a second what the verbose output might look like. This is the event loop itself. well, one step I should say of the event, you know, for this one message where given the user message, we're going to asynchronously use the runner and call out to the LLM using the message that was passed in, the agent that was defined to use that model and all the instructions that we had given it. So the first thing that you'll notice is that when we call this async, we also pass in all the same information we had for creating the session. This is because you can run things again, there can be multiple sessions. is going on. So you have multiple event loops running around. And asynchronously, you pass in here's the context that I'm currently operating in. It's for this user in this session and here's the content that's getting passed in, go ahead and execute this once with the LLM do some work on this. This is inside of a for loop because this is an agentic system. So once the agent is given this message, it might do a bunch of things before actually having that final response. So the event loop here pulls out events from the asynchronous running of the agent, every event is some update from the agent itself. As the events come in, if the verbose mode is turned on, we'll print out, here's an event that happened, here's who created that event. The initial event, of course, should be that the user message was passed in, so we should see the event author was the user themselves, and you'll see the content of the event as well. You can take a look what's happening here in the debug statement. bunch of things get printed out which are helpful to understand what's going on. And then this is the key concept for keeping the event loop running for as long as you'd like it to run. Hopefully the agent will run only as much as it needs to. But as each event comes in, there's a flag that says, okay, this is the final event, I'm done processing this, the agent is done processing. If it has set this, then you know that okay, the processing is over. You can take a look at the content and decide what the final response might be. Here we're taking a look at the content if they have some multiple parts. if it's a non-zero array of text that come back, we're going to take the first message back from the parts zero here, grab the text from that. And here we're making some assumptions that the first text response in the first part is actually the right one. Turn that into the final response text. Alternatively, the agent could have decided this is my final response and by the way, I don't have actually a response, what I've asked the runtime system to do is actually to escalate. Now here escalation ends up meaning the agent is running, knows that it's a sub agent and it can't handle or it can't do any more work with what's currently available. So it's basically saying, hey, escalate this to somebody else, either my parent or to some other agent that might be appropriate. In that case, the final response text that we're going to set is based on that. We're going to say that the agent has decided to escalate the current handling of the message and then we don't have any specific message or there's an error message if there is one there and return that as the final message. all that is to continue to run through one round through the event loop, instigated by a single user message, and then at the end we'll go ahead and print out the final agent response. That's kind of a lot to go through, but keep that in your heads a little bit as you're going through all the notebooks. Internally, this is really what's happening to actually make all the the machinery run. For all that work, there we go. Simple chat. Hello, I'm ABK. Hello to you, ABK. Of course, because we're going to be doing a lot of calling to agents, manually. We're going to set up the helper class to actually let us do that a little bit more easily. And I'm going to call that helper class AgentCaller. This AgentCaller basically wraps up those functions that we went through each one at a time here, and also the event loop itself into one class. And if you look through the details, it is doing all the things that we'd seen above. It's saving the single user ID, the single session ID, the current agent that we're going to be running and also the runner that's been set up. It has access to getting the session that it uses internally for actually managing stuff. And this call function here is what actually instigates one round trip through the event loop based on a user message. So it does just what we saw above, takes the user message, packages it up so that it's available as a Content to be processed by the agent. And then it runs through the event loop. Same kind of debug options available, as well. And this is really a stylistic choice. I'm not a native Python developer and so this is the way I like to do things. I prefer having factory methods for things that are a little bit complex to construct. So rather than having a complex constructor inside of that class, I created a separate class that actually does the construction. And this here, this utility function make_agent_caller is what we'll create an instance of the AgentCaller. You will be passing into it all the necessary information. mention that it has, what's the agent that you want to be running? And also, what is the initial state of that agent where the state is the agent's internal memory. All of this should look just like what we had run through before, but now packaged up into, here's the different elements, the major components that should be combined into the agent caller. What is the current app, the user, the session ID, create a session service for both memory and also for managing the session itself. And then finally, also create a runner, then having constructed all of those things, pass those into the agent caller which wraps those all up into an execution loop. Okay, now that we have some utility functions for running an agent, we can run a conversation with the say hello uh agent that we said that we created earlier. So to create a new agent caller for our hello_agent, we're going to actually call out the helper function, the make_agent_caller, passing in the hello_agent itself. And we'll just call that the hello_agent_caller. Then to run a conversation, we're going to have an asynchronous function that has multiple user messages that we're going to pass in to the agent caller using the call method. First I'll say hello, I'm ABK. and then I'll say I am excited. We'll go ahead and run that conversation and see what that looks like. Awesome. We have created a friendly chat agent. Good work. So, now that you've gotten this far, you know the basics of about using Google ADK, how to create an agent, how to give that agent tools that it can use. Then also how to create an execution environment for that agent. Finally, with all that in hand, you can script interactions with the agents here in the notebook, passing in user messages that let you then test out how the agent's going to work. So, this fantastic agent caller class that we created and then this construction method, the factory method called make agent caller, we're going to be using that through all the other notebooks to make it easy for once we've defined an agent, we can easily create a runtime environment using that function. So, you'll get a lot of use with that.