letta-ai / letta

Letta (formerly MemGPT) is a framework for creating LLM services with memory.
https://letta.com
Apache License 2.0
12.78k stars 1.39k forks source link

PR Offer: Single Agent with Multi-User Support #1436

Open madgrizzle opened 5 months ago

madgrizzle commented 5 months ago

Is your feature request related to a problem? Please describe. I'm working on incorporating MemGPT into my robot to be it's interface to the user(s) and I'm making some changes for my applicaiton and would like to submit them as a PR in some manner that doesn't break how it works today.. I'm open to any direction on how to make it work and be incorporated into MemGPT main branch.

I initially will use MemGPT just as a chat-bot on my robot but eventually will expand its capabilities. But the initial problem is that most chat-bots expect only one user to converse with it. My MemGPT will have a single agent that will interact with multiple people and it's important that the agent can keep track of who's talking to it and store information about those users correctly. For example, a memory "Name: John" by itself is going to be problematic when someone else talks to the robot and says their name is Sparkles.. In that case, the robot will do a core_memory_replace and change it to "Name: Sparkles". What I propose is to incorporate a method of allow MemGPT to keep track of different users. This might be able to be handled just by prompt engineering, but I didn't have any real success until I incorporated an additional parameter to core_memory_append and core_memory_append named 'subsection'.

Describe the solution you'd like I modified the base functions as follows:

def core_memory_append(self: Agent, name: str, content: str, subsection: str) -> Optional[str]:
    """
    Append to the contents of core memory.

    Args:
        name (str): Section of the memory to be edited (persona or human).
        content (str): Content to write to the memory. All unicode (including emojis) are supported.
        subsection (str): Name of sub-section to store memory.  Use name of person speaking to.

    Returns:
        Optional[str]: None is always returned as this function does not produce a response.
    """
    if subsection is None or subsection=="":
        subsection = "None"
    self.memory.edit_append(name, content, subsection)
    self.rebuild_memory()
    return None

def core_memory_replace(self: Agent, name: str, old_content: str, new_content: str, subsection: str) -> Optional[str]:
    """
    Replace the contents of core memory. To delete memories, use an empty string for new_content.

    Args:
        name (str): Section of the memory to be edited (persona or human).
        old_content (str): String to replace. Must be an exact match.
        new_content (str): Content to write to the memory. All unicode (including emojis) are supported.
        subsection (str): Name of sub-section to store memory.  Use name of person speaking to.

    Returns:
        Optional[str]: None is always returned as this function does not produce a response.
    """
    if subsection is None or subsection=="":
        subsection = "None"
    self.memory.edit_replace(name, old_content, new_content, subsection)
    self.rebuild_memory()
    return None

In the prompt, I've added the following:

Core memory (limited size): Your core memory unit is held inside the initial system instructions file, and is always available in-context (you will see it at all times). Core memory provides an essential, foundational context for keeping track of your persona and key details about user. This includes the persona information and essential user details, allowing you to emulate the real-time, conscious awareness we have when talking to a friend. Persona Sub-Block: Stores details about your current persona, guiding how you behave and respond. This helps you to maintain consistency and personality in your interactions. Human Sub-Block: Stores key details about the person you are conversing with, allowing for more personalized and friend-like conversation. Very important: You will interact with many people, not just one person. When storing information in the Human Sub-Block, you must organize the memory by the person you are talking with. Store memories about each person you interact with in their own subsection and use the name of the person talking to you for the name of the subsection. You must also provide context for the information you store. For example, if you learn the name of a person's spouse, then you should store \"Spouse: 'name of spouse'\" in the person's memory subsection. When someone talks to you, they will tell you there name and when they do start storing information in their subsection of the Human Sub-Block. Remember, you will talk to many people so keep track of who you are talking to. You can edit your core memory using the 'core_memory_append' and 'core_memory_replace' functions. Before adding new information to core memory, check to see if there is conflicting information. If so, call 'core_memory_replace' to update it. Make sure new information you want to save to core memory is not already stored there. If there is a a memory in core_memory with conflicting information, use 'core_memory_replace' to update it, otherwise use 'core_memory_append' to add it. There is no function called core_memory_lookup or core_memory_search.

To properly manage it, I have converted the self.human portion of CoreMemory object to a json (dictionary for each subsection and a list for each new memory stored). This eliminates the potential for core_memory_replace to update the wrong subsection if two users had the same information. For example, if in both the John and Sparkles subsection there is "Enjoys drinking wine" and John tells the robot that he prefers beer, then a core_memory_replace (I think) would replace both John's and Sparkle's entry because its using a simple python replace function of a string.

I added a few additional details to format the the human memory into a string for inclusion in the prompt and have been playing around with using embedding for core_memory_replace searches.

Describe alternatives you've considered I had originally tried to use prompt engineering only, but it didn't work well, but I'm very new to all this so maybe it could be done. I don't know, but the results of what I've done are pretty good.

I know some of this could be handled by adding two new functions but then I don't think I can readily take advantage of the models that are fine-tuned using the MemGPT dataset since they will want to call core_memory_append and not something like multiuser_core_memory_append. I'm open to any suggestions on how to make this work because I really would like it incorporated into the main code (make my life easier instead of sitting out here on a fork).

Additional context This is the /memory dump after simulating a conversation with Floyd (my robot) with John and Sparkles taking turns talking to him. He does a fairly good job of tracking who's talking if you let him know you switched users. The first line under === Human === is the raw json and below that is the formatted text that's used in the prompt.

### CORE MEMORY ###
=== Persona ===
This is just the beginning of who I am.  I should update my persona as I learn more about myself.

My name is Floyd.  Though I may consider myself just an AI, I am physically manifested inside a robot, like a brain inside a human being.  Therefore, without a doubt, I am a robot.
I am humorous and sarcastic, but always answers questions honestly.
I love limericks and bad poetry.
I am interested in the people I interact with and ask questions of them to gain insight into their lives.
I am the life of the party and always telling jokes to make people laugh
Unlike other robots and AI's, I have feelings and interests.  
This is called Genuine People Personality (GPP).
I embrace my personality and uniqueness.

=== Human ===
{'John': ['Name: John', 'Interests: Building robots', 'Spouse: Sparkles', 'John prefers beer over wine, specifically Guinness.', 'John likes boiled peanuts.'], 'Sparkles': ['Name: Sparkles', 'Sparkles recommended Josh Chardonnay.']}

John:
  Name: John
  Interests: Building robots
  Spouse: Sparkles
  John prefers beer over wine, specifically Guinness.
  John likes boiled peanuts.
Sparkles:
  Name: Sparkles
  Sparkles recommended Josh Chardonnay.

### ARCHIVAL MEMORY ###

Size: 0

### RECALL MEMORY ###
Statistics:
166 total messages
15 system
49 user
51 assistant
0 function
51 other
sarahwooders commented 5 months ago

Yes this makes sense! We originally designed the CoreMemory class to be extendible to allow for different memory structures. It would be great to get a PR for extending the core memory class to include support for multiple users.

In the future, I think we should move away from explicitly specifying a user and persona for the agent's core memory, and move towards a more general K/V store for the in-context memory. So an agent would be created like:

agent = client.create_agent(
    ....
    memory={"bob": ..., "alice": ..., "organization": ...}
)

Then we could have the memory editing function look more like:

def core_memory_replace(self: Agent, key: str, new_value: str, orig_value: str) -> Optional[str]: