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.
In the previous lesson, you learned how to create an agent using Google's ADK. And now we're going to create multiple agents, a team of agents working together. We're going to have a root agent and then two sub agents. We'll build on what we did previously where we had a say hello agent. We'll introduce a new agent that can say goodbye, and then we'll have another agent that puts them together into one multi-agent system. As usual, we'll go ahead and start with importing the libraries that we need. And then of course, we'll go ahead and set up the LLM that we're going to be using. Again, this is open AI. We'll define the LLM and we'll also go ahead and do this quick sanity check by passing out a message to make sure that it's ready. Always fun to see what open AI might say on any given day. Usually with friendly messages, it's pretty good about saying friendly things back. Next, you can set up the agent caller. We're going to use the same class that we defined before in Lesson 3 part 1. Rather than defining all that again, we're just going to import the tool that we need. If you remember from part one, we have this helper function make_agent_caller that's going to create the agent caller class with all that it needs to run. Okay, with everything ready, you can now begin to create the multi-agent system. You're going to create multiple specialized agents, each designed for a specific capability. You'll create one agent in charge of handling greetings, that's the say hello agent we had before. The new agent is going to take charge of saying goodbye. To combine those, you're going to have what's called a root agent and you'll hear terms like orchestrator or coordinator. parameter, top-level agent. It all really means having an agent that's going to wrap up to other agents and then manage their execution. That root agent is going to receive the initial user request, the hello or whatever the user decides to say, and then decide how to process that either itself or by what's called delegating, handing off to one of the sub agents. So the root agent, this top-level orchestrator is going to be delegating both hello and goodbye hopefully to the sub agents that specialize in those tasks. Okay, let's start to assemble our multi-agent system. Again, we're going to have the say hello agent that we had before. We're going to add a say goodbye agent and then we'll combine them together. First, we'll define the tools for our sub-agents. The first tool is the same say hello tool that we had from the previous lesson. This is the hello world function, takes in a person's name and creates a hello world message as a response. To that, we're going to include a say goodbye function. Now, this tool is going to be even simpler. simpler than the say_hello because it doesn't accept any arguments whatsoever. It has a constant reply, uh Goodbye from Cypher, no matter what you want to do. This is what you're going to get as a response saying goodbye to the user. With those two tools defined, we can now define some agents that will use those tools. Now, we're going to have a special agent for greeting and a special agent for farewell. But you'll see it's pretty straightforward. Now as we're defining these sub-agents, I really want to emphasize the best practices here and how important it is to have really good descriptions of each of these agents and also instructions that you're handing to them. You're going to spend most of your time once you've set up any kind of multi-agent system, most your time is going to be spent in actually optimizing these instructions. This is back to classic prompt engineering. and also this description to repeat what I had said in the previous lesson, the description is what allows other agents to know what does this agent do and when should I use this agent. That's called delegation. And the instructions are for the agent itself understand what is its purpose, what is it trying to do, and what tools does it have available and when to use them. So this is our greeting sub agent. It's the say hello agent that has access to only the say hello tool. And then we'll add our farewell agent. The farewell agent is pretty straightforward. It's similar to the hello agent. You can look through the description here of what the instructions are. And these instructions are actually useful to take a closer look at. It seems obvious. Okay, your job as the farewell agent is to respond whenever a user says goodbye in some kind of way. Well, people say goodbye in lots of different ways. So this is a very small example of how to do some few-shot learning within the agent instructions. So here we have in parentheses some examples like when the user uses words like bye, goodbye, thanks bye, or see you. Those are all ways of saying goodbye. Now, most LLMs don't need this extra bit of examples, but this is a good practice to have overall to think how can help the LLM understand what this agent's purpose is and actually when to execute and what to do in its role. And of course we're going to hand it the say_goodbye tool that we defined earlier. Great. You now have two sub-agents ready to go, and now we need to combine them. We're going to do that by defining the root agent, which is going to understand both of these sub agents and when to delegate a task to them. It's similar to doing tool calling, but the agent knows that it's talking to another agent. So the entire conversational history gets passed along. I'm going to actually control how the conversation gets passed along to the subagent. So it's a little bit different than tool call. But the same idea is current workflow is going to go from the agent who is in charge, here the root agent, to one of the sub agents. So this is similar to tool calling, but let's take a careful look at how agent delegation works here. We know that the agents themselves have described what their role is, like the the say hello agent versus the say goodbye agent, both have some understanding about what it is they're supposed to do, and their descriptions. describe to other agents what it is their job is. We're going to double down on that in the instructions that we give to the root agent. The root agent is be told, your job is to coordinate a team of sub-agents, as you can see that in the instructions here. and that its primary goal is to be friendly. In order to be friendly, it has actually two specialized sub-agents. So you describe what those are so that the agent understands when it actually should use the sub-agents. This is like describing when you should use particular tools. The greeting_agent should be used for handling simple greetings like hello or hi. And of course, the farewell agent should be used for saying goodbye, whether that's bye, see you or anything else. And we're explicit here about saying when those messages are received from the user, go ahead and delegate to these sub agents to actually respond to the user rather than responding directly as the coordinator. So if this works well, the coordinator shouldn't do anything other than answer general questions and when you say hello or goodbye, it should delegate to these sub-agents for actually executing response or generating a response to the user. This top level coordinator doesn't have any tools to use whatsoever. It can only chat or delegate to the sub agents. We're also going to hand it the list of sub agents here in the sub_agents key. So the sub agents are the greeting_subagent and the farewell_subagent. Now that we have a team of agents all put together into a multi-agent system, let's go ahead and interact with it. This is similar to what we did in part 1 of Lesson 3, where we had an asynchronous function that managed a conversation. So here the conversation is going to be just what we did before. Hello, I'm ABK, and then later, rather than saying I'm excited, this transcript is going to say, thanks, bye. This is going to be the second user message. We've also added this true here. So, if you look at the call to this agent caller, the second argument that's passed in is whether or not to be verbose in responses. We're going to turn on verbosity here so that you can see all of what's going on behind the scenes so you can understand both the user message going in, see the delegation, see the tool calls and also see the responses. So a lot to walk through, but it's worth walking through at least once to get familiar with what you can see when you're debugging an agent interaction. All right, this starts off innocently enough. We get the user's message, hello I'm ABK. And you can see that the initial event comes from the friendly agent team. So this is the top level coordinator, so the team coordinator that's going to call the sub agents. And you can see the first action that it takes is that it wants to transfer_to_agent. So transfer or delegating to a subagent. And it's going to transfer to the greeting_subagent. So that's perfect. In response to hello, I'm ABK. The top level coordinator realized, I've got an agent who's really good at saying hello, let me transfer control over to that agent. So the transfer response doesn't have any, you know, value whatsoever, it's just a None. But what we should see next is that the greeting subagent takes control. So here's another event, but now from the from the greeting sub agent, and it's going to look at the transcript and see the user's message and realize that, okay, it's now my job to actually do something about this. And the greeting sub agent is going to respond by making a tool call. You can see this FunctionCall here and the name of the function that it wants to call is called say_hello, and it's going to pass in some arguments where the one argument that's passing in is the person_name is ABK. So the agent has realized that from the statement here, of course, Hello I'm ABK. is able to extract that ABK must be the person's name, and it's going to make a function call to the tool passing in that name. And then you can receive the response. Here's the function response, and the response is Hello to you, ABK. Perfect. Now, the greeting sub agent is done, it goes ahead and you can see that the final message is True. So that's the flag that says, I'm done processing this message. We can go ahead and terminate this particular event loop. And the final agent response instances of being the response that came back from the tool, Hello to you, ABK. We send in the second query then or message from the user, and the user says Thanks, bye! And again, the top level agent is going to go ahead. Oh actually, no, it's still in the control of the greeting subagent. So the greeting subagent itself is going to realize, okay, this is not something I should use. Please transfer this to the farewell subagent. So it's aware of the other agents that are available, transfers to the farewell subagent. and that is the end of that function response. Here you can see then a message from the farewell_subagent_v1 itself. It again is going to look at the transcript now that it's been given control of the current conversation. The first thing it's going to do is realize, okay, the user said goodbye, let me go ahead and call the say_goodbye tool to figure out how to respond to them. And so here's the function_call to say_goodbye, and of course it takes no arguments, as expected. but the response to it, is the constant that we saw earlier in the tool definition. It's going to get a farewell that is Goodbye from Cypher! and that again is the final response for this event loop. And so that is going to be the final agent response, Goodbye from Cypher! Okay, that was a lot to walk through. It's worth taking the time to do that for anything else we do in these notebooks. You can really see all of the interactions about tool calls, agent delegation, and also the responses and the changes in state overall. So a really useful to do. we're not going to do that for every notebook, but if you'd like to on your own time, please do take the time to really understand what's happening behind the scenes, because what's happening behind the scenes is going to affect, of course, how you do the overall agent definitions and also the agent orchestration. You're going to take one more step in this multi-agent system. We have an execution environment for agents. We've got a team of multiple agents that can work together. They can delegate tasks to each other, and then each of those agents also has tools that they can use. The last important component for all agent systems is having memory. Memory is just internal state happening for a particular session across any agents that are involved in that session, and of course the user themselves. What is session state really? Session state in Google ADK, by default is really just a dictionary that's available that you can update keys within that dictionary. And when you update values of those keys, Google ADK tracks those changes. It tracks deltas to the state, basically. and updates the overall session state to keep it consistent across all agents, whether they're running in parallel or running sequentially because it's an asynchronous system at its heart, it manages all the state updates. Agents have a few different ways of interacting with state. One way, the the way that I prefer and the primary method for interacting with state is through the tool context. So whenever a tool is called, there's an extra parameter that's available that has the context within which that tool has been called. Given that tool context, the tool itself then has access to the current state or the current memory of the agent, and it can use that memory for actually making either different decisions, creating different output, whatever is appropriate for the memory. The other way that agents can interact with state is by using an output key. So, rather than using a tool call for updating some memory or for accessing memory, you can take the actual output from an agent that final response, and rather than just turning that back as the response from the agent, you can save that into state by defining an output key. So when you see this final true, the message related to the final true here it'll be the text Goodbye from Cypher! could get used for updating state itself. That's very convenient sometimes and we'll do a little bit of that later on in some of these notebooks. Okay, in this next step, we're going to set up some memory. We always have the in memory session that we're using right now. So the the memory is saved just within RAM that's being persisted to a database. Of course, that's the best thing to do for a production system, but for convenience, having in memory state is perfectly fine. We're going to update both of our tools to actually take advantage of using that in memory state. So, both for say hello and say goodbye, we're going to update those tools to take advantage of the tool context to update session state or memory. You should do this one at a time and take a careful look at the differences here. You'll notice that we're importing ToolContext from google.adk ToolContext is the class that takes care of keeping track of the session state and in addition to other things that is useful for tools to have access to. So we're going to update the say_hello function to be say_hello_stateful. It still accepts an argument for the user's name, but it now has this extra argument called tool_context. It's the tool_context that's going to let us get access to the ToolContext passed in for the session by the execution environment. So, in the tool context, there's this state dictionary, and we're just going to update the username key and the name that was passed into the function, we're going to use for updating the session memory or the session state for the username key. So this is pretty straightforward, username, username. Now we have a dictionary that is shared across all agents and across all tools that will have that value that's passed into this function. This is a way of sort of keeping track of things by natural use of functions. and also then preserving them for later use. For debugging purposes, when this tool is called, we'll go ahead and print out the update to the session state, particularly to the user_name key. And then of course, we'll do what we had done in the previous say hello, we'll send a query off to Neo4J, using a query parameter that is the $user_name. So let's go ahead and define that. Now that you've defined that, you can define the same thing for the say goodbye. And again, as you're defining this, this is just an update to the to say goodbye that we had before. Now the difference is that this say goodbye, even though it doesn't take an argument for anything related to the person. It has no custom arguments here. It does get this tool_context passed in. And I should mention that this is something that Google ADK does automatically when it sees a tool with the last argument to the tool is ToolContext, it automatically injects that to the tool call. So when say_goodbye_stateful is called here, it's going to get tool_context, but nothing else. But, because say_hello_stateful has set the state, the user name into the state. say goodbye, can actually access the username, so now we have remembered what the user's name is. We can access the state for the state key username and then have a default value in case the user didn't identify themselves. But we can get the from the dictionary the username or default, assign that to the username, then use that for actually generating a goodbye statement. You can then define a new stateful agent for farewell. And again, that's going to be the same as we had as the previous farewell agent, but now it's going to call the say_goodbye_stateful tool. As before, you can combine those into a multi-agent system by having a root level agent, a coordinator agent that's going to use both of those as sub_agents. And this is exactly the same as the other root agent that we had, but now it's just using all the sub agents that have ultimately down at their core, these tools that are stateful based. You now have a multi-agent system that takes advantage of memory. You can give it a try. You will use our friend, to make_agent_caller, passing in that new root_agent_stateful that is the top level coordinator, create a caller for that. And to see the change in the session, we'll go ahead and get the session directly from the caller, remember there was a utility function for getting that if you look back at the agent caller class. We'll go ahead and get the session and then based on that session, we'll print out the initial state when this is created. And we should see that there's nothing in the current state. As expected, the initial state is empty. You can now define a conversation. It's the same conversation that we've been having. Say hello, say goodbye, and let's see what happens. Actually, I'm going to take out these verbose statements right now, and you can decide to keep those in or add them if you'd like, run it either way. But just keep the output to a minimum. We'll go ahead and run this without the debugging statements. And what's really important here is that we saw that the initial state was empty. and we expect that once these calls are done, the initial state should turn into a final state. Here we're going to get it again, and that the final state should actually have the user's name as defined by the call to say hello. Awesome. So our utility functions printed out this update to the user_name here. And we also see when we get the session and then get the state from that session that the final state includes the user_name ABK. Fantastic. Everything's working as expected. Now if you'd like to, you can actually run this in an interactive environment even within this notebook. You can set up a small helper function that's going to have a loop for getting messages from the user and then just calling our caller, passing in that message. This will run as long as the user doesn't pass in a message that says exit. When you set that up and then run it, you'll see a little box appear. So when you run the interactive session, you can type in a message and just see what happens. and I'll start by not revealing my name, see what happens. Okay, that's kind of funny. So the LLM decided that my name is there based on this message. That's not really great. Let's correct it. Let's say And you can see just by passing in my name is ABK, that the tool context actually updated with ABK. So there's a tool call still to say hello to ABK, and because of that, the LLM correctly identified my name is ABK, saved that into memory. This is fantastic. If I say goodbye again, we should now get a nice message by ABK as before. Give this a try, interact with it and see what you can come up with. And of course if you'd like to change the any of the code and see what the impact is of that. Okay, you've gotten all the way to the end of Lesson 3. You've created a basic agent that is a single agent that says hello. And then you created a multi-agent system with two sub-agents specialized in saying hello or saying goodbye, and then a root agent that coordinates their interactions with the user. With this in hand, you're ready to continue and to actually doing agentic knowledge graph construction.