In the Letta framework, agents are designed to be deployed as services. In this lesson, we'll go over how we can implement multi-agent collaboration using tools for cross-agent communication as well as sharing memory blocks. Let's have some fun! In the Letta framework, agents are designed to run as a service so that a real application can communicate with them via Rest APIs. So if you have something like a mobile app that's using a chatbot, that mobile app will send Rest API requests to some kind of Letta server that's backed up by a database, and then that server will return back a response that the mobile app can then use. So how can we have multi-agent collaboration when we have agents running separate services? And we want them to coordinate and communicate. So one solution is to have the agents themselves send each other messages. So for example you could send a Post request from an agent on one server to another. Another option is to have shared memory blocks. So you can have blocks that are in a shared persistent data store, and then have agents have synced to memory blocks across different services. In this lab, we're going to go over how to implement multi-agent collaboration using Letta. We're going to do this by essentially implementing a multi-agent recruiting workflow for evaluating leads and drafting personalized outreach emails. So we're going to have two different agents and evaluator agent that evaluates lead candidates. And then also an outreach agent, which draws personalized outreach emails. These two agents are going to be able to communicate and coordinate with each other to work together. These agents will also have shared memory. We can essentially implement this by creating a shared memory block that is attached to both agents. And this shared memory block will basically store memories about the company that the agents are working together to recruit for. So as always, I'm going to create my Letta client to connect to the running Letta service, and then I'm also going to paste in a helper function for printing the message. So we've used this already in other lessons. But I'm also going to add in a print for the type system message. And then also since we're streaming, also make sure to print user statistics when that's returned back in the streaming messages. So before we create our agents we're going to first create their shared memory block. So this is basically a block that we can create directly through the client that will be attached to both agents so that the agents can have shared memory. So this block is basically going to have a value which is the company description. So the company is called Agent OS and is deploying AI tools to make it easier to create and deploy LLM agents. And then we can use the client to create this block. So we'll pass in as the value the company description and then also have the block, have the label company, and then also a limit of 10,000 characters. So I can print out this return block object to see that it also now has this unique ID. So we can now reference this block either to modify or to retrieve it later on using this unique block ID with the client. So now let's create our two agents. So in this setting we're going to create both an outreach agent for drafting emails and then also an evaluation agent for evaluating different candidates. The first station, which is going to be the outreach agent, is going to have access to a draft candidate email tool. So this is basically a tool to write out an email draft in the contact field so that we can later review it or decide to send it. So we'll create this tool with the same upsert from function tool that we've used with the client before it. And so this will basically instantiate a Letta tool on the server. And now we can create our outreach agent. So we're going to give it basically a persona that kind of gives it some descriptions about how it should be acting. So we say in the persona you're responsible for drafting emails on behalf of a company with the draft candidate email tool. Candidates to email will be messaged to you. So we'll kind of go over this a bit more later. But strong candidates will be sent to this agent from the other agent, so that this outreach agent can draft an email for that candidate. So, we are now also creating the agent from the client. So we'll pass in the name Outreach Agent. We're also pass in one memory block for the persona that has the label persona and the value. And then we're actually also attaching the block that we created before. So we don't actually pass in the block here because the block has already been created. We instead pass in the block ID to tell Letta that this block ID that we previously made should be attached to this agent, and the data inside of this block should be contained within this agent's context window. And then of course, we also set the LLM model in the embedding model. And then also pass in this tool name. You can also do this by passing in the tool ID in the tool IDs. So next we're going to create the evaluation agent. So this agent will have a tool which is a reject tool so it can reject the candidate. So decide not to email a candidate. And we're going to create this reject tool first, so we can attach it to the agent. And for this agent we're going to construct a persona for it. So the eval persona that tells it we are looking for this set of skills. So things like front-end, software engineering experience with our LLMs. So ideal candidate should have these skills. And these weaker candidates that don't match the skill sets should go through the reject tool and strong candidate should be sent to the agent with the ID of the outreach agent that we just created. So by passing in the outreach agents ID into the persona, we're telling this agent which other agent should you message in order to communicate that this candidate is strong and we want to draft an email for them. So now we can actually create the eval agent. So similarly to the other agent, we're just going to provide one memory block for the persona with that Eval persona we just made defining the model and the LLM model and the embedding model. We're attaching the reject tool. So this time we're just passing it in with ID you can again also do this with name. And then we're passing in by name the send message to agent and wait for reply tool. So this tool is actually a Letta built-in tool. It allows the agents to message other agents running on the same server by specifying a message to send, and then also the id. So this evaluation will know the ID of the other outreach agent because we pass it the ID into the persona. You can also actually define tools like this yourself. So in previous labs we've defined tools that connect to the client and interact with the client. So you can of course create your own custom cross messaging tool by using the client. But Letta also provide some, which is what we're using here. And then we're actually also going to set include based tools to false. So like we mentioned previously Letta agents will by default have a bunch of tools related to sending messages memory management etc. but for simplification we're just going to set this to false. So this agent is more limited in scope. And then like the other agent, we're also going to attach the company block IDs so that this information is in its context window. And then finally we're actually going to specify something called tool rules. So this is kind of a way to constrain the agent's behavior. In this case we're asking the agent to basically exit the loop. So to terminate execution after it calls the send message to agent and wait for reply tool. So this basically ensures that we don't get the agent looping. Once the agent does send that message to the other agent, it stops. So you can see that because we set include based tools to false, this agent actually only has two tools the reject tool and the send message to agent and wait for reply tool. So now we can actually send some resume data to the agents. So we're going to read this resume from the local file system for candidate name Tony Stark. Then we're going to send this resume data to the eval agent directly. So we're going to send this message to evaluation with this ID and provide the content. Just please evaluate this resume with the resume data that we just read. And so we can see that the agent reasons it's evaluating Tony Stark's profile seem strong. And so then the agent decides to send the agent wait for reply to this other agent. So provides information about the candidate name and any other information that might be relevant and it decides to send this to the other agent with this agent ID of the outreach agent. So it knows this other agent ID, because we provide it in the persona, so it's able to reach out to this other agent, and we can see that this agent actually returns back a response. So it's able to call the send message tool and return back this response, which is this drafted email. And so we get back this tool return response. And then finally we print out the total usage statistics. So this is step count one since the evaluation only made one step. But there is of course another step that was made by the other agent that was doing outreach. We can of course also see the list of messages or the events that happened from the outreach agent side. So to do this, we can list out the messages for the outreach agent ID we're going to skip the first message since this message is basically just a system prompt sent to the LLM provider. So the other messages are kind of what's actually in this history. And so you can see that there's a couple of messages here. These are basically what we call the initial message sequence. To kind of give some in-context examples to the agent. You can of course changes if you like. And so we see here that there's a system message. So this is basically a message that this agent received from the other agent that has this incoming message from this agent ID, and also provides a hint that this agent should make sure to call send message at the end, reply back to this agent, and then we see this content about Tony Stark that was sent by the other agent. And so after it gets this alert, the agent decides to call draft candidate email. So it drafts this email and then it returns back this drafted email. And then also sends this message as a send message tool back to the other agent. So you can see that here. It's writing the send message. And so this was what was actually shared to the other agent. So these agents also have a shared memory block which we created at the beginning. So what this means is that if either agent edits this block then those changes will be reflected on both agents. So for example we can send a message to the outreach agent basically telling it, you know, the company has actually rebranded the Letta from Agent OS. And so the agent basically calls the core memory replace memory editing tool to replace Agent OS with Letta and confirms that and sends a message. And so, unsurprisingly, now if we retrieve the memory block of the eval agent, it now says Letta instead of Agent OS. But if we also retrieve the memory block of the outreach agent. Then we can see that this also says now, even though the outreach agent did not edit its memory, its memory now says that the company is called Letta. And this is again because each of these blocks is actually the same ID. So they both have you can see here like matching IDs. And so both of these agents, their context windows or in context memory is synchronized to the same memory block. So we just went over how you can create multi-agent coordination through tools. But often simpler way to do this is to actually use the multi-agent group abstraction inside of Letta. So before we get the setup, we're actually going to once again load in a print message multi-agent tool. So this is very similar to what we had before. But we're actually going to also print out the message dot name field. So in a multi-agent setting there's obviously multiple agents that are talking. So we use the name field to distinguish between multiple agents which will have different names. So we're now going to recreate our agents. So this is exactly the same agents as before. Except this time we don't need to provide any tooling to allow the agents to chat with each other. So you might recall before that we had a special send message to other agent tool that allowed the eval agent to communicate with outreach agent. But we're not going to use tools this time. We're just going to create both these agents separately. And now we can place both of these agents into a multi-agent group. So this is a very simple multi-agent example. We're basically just doing round robin between each of these agents in order. But this basically will group the agents together. And so they'll have a single group chat. So a single message history that's all shared. So both agents can see the other agents messages as well as user messages in a single thread, as opposed to in the other example, each agent has its own thread. But they communicate to each other via tools. So let's now try reading another resume. So basically a resume for a SpongeBob SquarePants. And so now we can actually message the group with this content. So the group messaging a group is very similar to messaging an agent. It's almost exactly the same API, except the only difference is instead of specifying an agent ID, we're just specifying a group ID. So we're basically just grabbing this round-robin group and then passing in its ID. And we're going to send this message which is a user message. And then just the content of the resume. So very similar to before, except we're just messaging a group. So we're now printing out the response stream from the group. And you can see here that we're printing the name of the agent. So basically who said this or who had this reasoning message. So we can see that the eval decides to reject SpongeBob SquarePants. And again this is all kind of in the same group chat. So both agents can see messages in here. And so this evaluation completes. And then this outreach agent actually decides to draft a rejection email to SpongeBob SquarePants. So again, because this agent is able to see the entire message history of the other agent, it decides to use this draft email tool. And then it sends out this message and then does reasoning. And then finally the outreach agent also sends a final message, which is just telling us that it draft a rejection email. And at the very end, we get the usage statistics of what all the agents did. You can maybe prompt this to get the outreach agent to not send an email at the end, but in this case, the agent decided to use this tool anyways. This is just another way to do multi-agent. Depending on your use case, you may want the agents to share a group chat, or you may want them to have separate threads and only communicate when necessary. So congratulations! You've now created a multi-agent collaborative setting and Letta in two different ways, both with having a group chat and then also through multi-agent tools. You've also created agents with shared memory so that they can share memories about the same concepts, whether it's an organization or a user, or other things that you might want to add to your agents.