diff --git a/docs/autogen.md b/docs/autogen.md index a9aa2abb..1a04a3d7 100644 --- a/docs/autogen.md +++ b/docs/autogen.md @@ -53,6 +53,7 @@ memgpt_autogen_agent = create_memgpt_autogen_agent_from_config( interface_kwargs=interface_kwargs, default_auto_reply="...", skip_verify=False, # NOTE: you should set this to True if you expect your MemGPT AutoGen agent to call a function other than send_message on the first turn + auto_save=False, # NOTE: set this to True if you want the MemGPT AutoGen agent to save its internal state after each reply - you can also save manually with .save() ) ``` @@ -62,11 +63,29 @@ Now this `memgpt_autogen_agent` can be used in standard AutoGen scripts: import autogen # ... assuming we have some other AutoGen agents other_agent_1 and 2 -groupchat = autogen.GroupChat(agents=[memgpt_autogen_agent, other_agent_1, other_agent_2], messages=[], max_round=12) +groupchat = autogen.GroupChat(agents=[memgpt_autogen_agent, other_agent_1, other_agent_2], messages=[], max_round=12, speaker_selection_method="round_robin") ``` [examples/agent_groupchat.py](https://github.com/cpacker/MemGPT/blob/main/memgpt/autogen/examples/agent_groupchat.py) contains an example of a groupchat where one of the agents is powered by MemGPT. If you are using OpenAI, you can also run the example using the [notebook](https://github.com/cpacker/MemGPT/blob/main/memgpt/autogen/examples/memgpt_coder_autogen.ipynb). +### Saving and loading + +If you're using MemGPT AutoGen agents inside a Python script, you can save the internal state of the agent (message history, memory, etc.) by calling `.save()`: +```python +# You can also set auto_save = True in the creation function +memgpt_autogen_agent.save() +``` + +To load an existing agent, you can use the `load_autogen_memgpt_agent` function: +```python +from memgpt.autogen.memgpt_agent import load_autogen_memgpt_agent + +# To load an AutoGen+MemGPT agent you previously created, you can use the load function: +memgpt_autogen_agent = load_autogen_memgpt_agent(agent_config={"name": "MemGPT_agent"}) +``` + +Because AutoGen MemGPT agents are really just MemGPT agents under-the-hood, you can interact with them via standard MemGPT interfaces such as the [MemGPT Python Client](https://memgpt.readme.io/docs/python_client) or [MemGPT API](https://memgpt.readme.io/reference/api). However, be careful when using AutoGen MemGPT agents outside of AutoGen scripts, since the context (chain of messages) may become confusing for the MemGPT agent to understand as you are mixing AutoGen groupchat conversations with regular user-agent 1-1 conversations. + In the next section, we'll go through the example in depth to demonstrate how to set up MemGPT and AutoGen to run with a local LLM backend. ## Example: connecting AutoGen + MemGPT to non-OpenAI LLMs diff --git a/docs/python_client.md b/docs/python_client.md index 6a852cba..2e535366 100644 --- a/docs/python_client.md +++ b/docs/python_client.md @@ -4,7 +4,7 @@ excerpt: Developing using the MemGPT Python client category: 6580dab16cade8003f996d17 --- -The fastest way to integrate MemGPT with your own Python projects is through the `MemGPT` client class: +The fastest way to integrate MemGPT with your own Python projects is through the `MemGPT` [client class](https://github.com/cpacker/MemGPT/blob/main/memgpt/client/client.py): ```python from memgpt import MemGPT diff --git a/memgpt/agent.py b/memgpt/agent.py index eca165c3..9a9a9a4b 100644 --- a/memgpt/agent.py +++ b/memgpt/agent.py @@ -316,6 +316,19 @@ class Agent(object): self._messages = new_messages self.messages_total += len(added_messages) + def append_to_messages(self, added_messages: List[dict]): + """An external-facing message append, where dict-like messages are first converted to Message objects""" + added_messages_objs = [ + Message.dict_to_message( + agent_id=self.agent_state.id, + user_id=self.agent_state.user_id, + model=self.model, + openai_message_dict=msg, + ) + for msg in added_messages + ] + self._append_to_messages(added_messages_objs) + def _swap_system_message(self, new_system_message: Message): assert isinstance(new_system_message, Message) assert new_system_message.role == "system", new_system_message @@ -875,7 +888,7 @@ class Agent(object): # print(f"Agent.save {new_agent_state.id} :: preupdate:\n\tmessages={new_agent_state.state['messages']}") # self.ms.update_agent(agent=new_agent_state) - def update_state(self): + def update_state(self) -> AgentState: updated_state = { "persona": self.memory.persona, "human": self.memory.human, diff --git a/memgpt/autogen/examples/agent_autoreply.py b/memgpt/autogen/examples/agent_autoreply.py index 5488bba7..abd8ffc2 100644 --- a/memgpt/autogen/examples/agent_autoreply.py +++ b/memgpt/autogen/examples/agent_autoreply.py @@ -23,7 +23,7 @@ if LLM_BACKEND == "openai": model = "gpt-4" openai_api_key = os.getenv("OPENAI_API_KEY") - assert openai_api_key, "You must set OPENAI_API_KEY to run this example" + assert openai_api_key, "You must set OPENAI_API_KEY or set LLM_BACKEND to 'local' to run this example" # This config is for AutoGen agents that are not powered by MemGPT config_list = [ @@ -109,7 +109,7 @@ elif LLM_BACKEND == "local": "preset": DEFAULT_PRESET, "model": None, # only required for Ollama, see: https://memgpt.readme.io/docs/ollama "context_window": 8192, # the context window of your model (for Mistral 7B-based models, it's likely 8192) - "model_wrapper": "airoboros-l2-70b-2.1", # airoboros is the default wrapper and should work for most models + "model_wrapper": "chatml", # chatml is the default wrapper "model_endpoint_type": "lmstudio", # can use webui, ollama, llamacpp, etc. "model_endpoint": "http://localhost:1234", # the IP address of your LLM backend }, diff --git a/memgpt/autogen/examples/agent_docs.py b/memgpt/autogen/examples/agent_docs.py index 93e84261..90b706df 100644 --- a/memgpt/autogen/examples/agent_docs.py +++ b/memgpt/autogen/examples/agent_docs.py @@ -26,7 +26,7 @@ if LLM_BACKEND == "openai": model = "gpt-4" openai_api_key = os.getenv("OPENAI_API_KEY") - assert openai_api_key, "You must set OPENAI_API_KEY to run this example" + assert openai_api_key, "You must set OPENAI_API_KEY or set LLM_BACKEND to 'local' to run this example" # This config is for AutoGen agents that are not powered by MemGPT config_list = [ @@ -112,7 +112,7 @@ elif LLM_BACKEND == "local": "preset": DEFAULT_PRESET, "model": None, # only required for Ollama, see: https://memgpt.readme.io/docs/ollama "context_window": 8192, # the context window of your model (for Mistral 7B-based models, it's likely 8192) - "model_wrapper": "airoboros-l2-70b-2.1", # airoboros is the default wrapper and should work for most models + "model_wrapper": "chatml", # chatml is the default wrapper "model_endpoint_type": "lmstudio", # can use webui, ollama, llamacpp, etc. "model_endpoint": "http://localhost:1234", # the IP address of your LLM backend }, @@ -153,7 +153,13 @@ memgpt_agent = create_memgpt_autogen_agent_from_config( skip_verify=False, # NOTE: you should set this to True if you expect your MemGPT AutoGen agent to call a function other than send_message on the first turn ) # NOTE: you need to follow steps to load document first: see https://memgpt.readme.io/docs/autogen#part-4-attaching-documents-to-memgpt-autogen-agents -memgpt_agent.load_and_attach("memgpt_research_paper", "directory") +memgpt_agent.load_and_attach( + name="memgpt_research_paper", + type="directory", + input_dir=None, + input_files=["memgpt_research_paper.pdf"], + # force=True, +) # Initialize the group chat between the agents groupchat = autogen.GroupChat(agents=[user_proxy, memgpt_agent], messages=[], max_round=12, speaker_selection_method="round_robin") diff --git a/memgpt/autogen/examples/agent_groupchat.py b/memgpt/autogen/examples/agent_groupchat.py index dd15b63c..d4c61a8a 100644 --- a/memgpt/autogen/examples/agent_groupchat.py +++ b/memgpt/autogen/examples/agent_groupchat.py @@ -12,7 +12,7 @@ Begin by doing: import os import autogen -from memgpt.autogen.memgpt_agent import create_memgpt_autogen_agent_from_config +from memgpt.autogen.memgpt_agent import create_memgpt_autogen_agent_from_config, load_autogen_memgpt_agent from memgpt.constants import LLM_MAX_TOKENS, DEFAULT_PRESET LLM_BACKEND = "openai" @@ -24,7 +24,7 @@ if LLM_BACKEND == "openai": model = "gpt-4" openai_api_key = os.getenv("OPENAI_API_KEY") - assert openai_api_key, "You must set OPENAI_API_KEY to run this example" + assert openai_api_key, "You must set OPENAI_API_KEY or set LLM_BACKEND to 'local' to run this example" # This config is for AutoGen agents that are not powered by MemGPT config_list = [ @@ -110,7 +110,7 @@ elif LLM_BACKEND == "local": "preset": DEFAULT_PRESET, "model": None, # only required for Ollama, see: https://memgpt.readme.io/docs/ollama "context_window": 8192, # the context window of your model (for Mistral 7B-based models, it's likely 8192) - "model_wrapper": "airoboros-l2-70b-2.1", # airoboros is the default wrapper and should work for most models + "model_wrapper": "chatml", # chatml is the default wrapper "model_endpoint_type": "lmstudio", # can use webui, ollama, llamacpp, etc. "model_endpoint": "http://localhost:1234", # the IP address of your LLM backend }, @@ -173,10 +173,20 @@ else: interface_kwargs=interface_kwargs, default_auto_reply="...", # Set a default auto-reply message here (non-empty auto-reply is required for LM Studio) skip_verify=False, # NOTE: you should set this to True if you expect your MemGPT AutoGen agent to call a function other than send_message on the first turn + auto_save=False, # Set this to True if you want the MemGPT AutoGen agent to save its internal state after each reply - you can also save manually with .save() ) + # If you'd like to save this agent at any point, you can do: + # coder.save() + + # You can also autosave by setting auto_save=True, in which case coder.save() will be called automatically + + # To load an AutoGen+MemGPT agent you previously created, you can use the load function: + # coder = load_autogen_memgpt_agent(agent_config={"name": "MemGPT_coder"}) + + # Initialize the group chat between the user and two LLM agents (PM and coder) -groupchat = autogen.GroupChat(agents=[user_proxy, pm, coder], messages=[], max_round=12) +groupchat = autogen.GroupChat(agents=[user_proxy, pm, coder], messages=[], max_round=12, speaker_selection_method="round_robin") manager = autogen.GroupChatManager(groupchat=groupchat, llm_config=llm_config) # Begin the group chat with a message from the user diff --git a/memgpt/autogen/examples/memgpt_coder_autogen.ipynb b/memgpt/autogen/examples/memgpt_coder_autogen.ipynb index e9a7c94e..2a19069b 100644 --- a/memgpt/autogen/examples/memgpt_coder_autogen.ipynb +++ b/memgpt/autogen/examples/memgpt_coder_autogen.ipynb @@ -21,7 +21,7 @@ }, "outputs": [], "source": [ - "%pip install pyautogen" + "%pip install pyautogen==0.2.0" ] }, { @@ -94,6 +94,12 @@ }, { "cell_type": "code", + "execution_count": null, + "id": "flVCXXKirZ-c", + "metadata": { + "id": "flVCXXKirZ-c" + }, + "outputs": [], "source": [ "# The user agent\n", "user_proxy = autogen.UserProxyAgent(\n", @@ -145,16 +151,16 @@ " interface_kwargs=interface_kwargs,\n", " default_auto_reply=\"...\", # Set a default auto-reply message here (non-empty auto-reply is required for LM Studio)\n", " )" - ], - "metadata": { - "id": "flVCXXKirZ-c" - }, - "id": "flVCXXKirZ-c", - "execution_count": null, - "outputs": [] + ] }, { "cell_type": "code", + "execution_count": null, + "id": "GvLSBuEhreO1", + "metadata": { + "id": "GvLSBuEhreO1" + }, + "outputs": [], "source": [ "# Initialize the group chat between the user and two LLM agents (PM and coder)\n", "groupchat = autogen.GroupChat(agents=[user_proxy, pm, coder], messages=[], max_round=12)\n", @@ -165,16 +171,13 @@ " manager,\n", " message=\"I want to design an app to make me one million dollars in one month. Yes, your heard that right.\",\n", ")" - ], - "metadata": { - "id": "GvLSBuEhreO1" - }, - "id": "GvLSBuEhreO1", - "execution_count": null, - "outputs": [] + ] } ], "metadata": { + "colab": { + "provenance": [] + }, "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", @@ -191,9 +194,6 @@ "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.6" - }, - "colab": { - "provenance": [] } }, "nbformat": 4, diff --git a/memgpt/autogen/interface.py b/memgpt/autogen/interface.py index d994179f..19438bd3 100644 --- a/memgpt/autogen/interface.py +++ b/memgpt/autogen/interface.py @@ -47,9 +47,9 @@ class AutoGenInterface(object): def __init__( self, message_list=None, - fancy=False, + fancy=True, # only applies to the prints, not the appended messages show_user_message=False, - show_inner_thoughts=False, + show_inner_thoughts=True, show_function_outputs=False, debug=False, ): @@ -65,34 +65,36 @@ class AutoGenInterface(object): self.message_list = [] def internal_monologue(self, msg): - # ANSI escape code for italic is '\x1B[3m' + # NOTE: never gets appended if self.debug: print(f"inner thoughts :: {msg}") if not self.show_inner_thoughts: return - message = f"\x1B[3m{Fore.LIGHTBLACK_EX}💭 {msg}{Style.RESET_ALL}" if self.fancy else f"[inner thoughts] {msg}" + # ANSI escape code for italic is '\x1B[3m' + message = f"\x1B[3m{Fore.LIGHTBLACK_EX}💭 {msg}{Style.RESET_ALL}" if self.fancy else f"[MemGPT agent's inner thoughts] {msg}" print(message) - # self.message_list.append(message) def assistant_message(self, msg): + # NOTE: gets appended if self.debug: print(f"assistant :: {msg}") - message = f"{Fore.YELLOW}{Style.BRIGHT}🤖 {Fore.YELLOW}{msg}{Style.RESET_ALL}" if self.fancy else msg - self.message_list.append(message) + # message = f"{Fore.YELLOW}{Style.BRIGHT}🤖 {Fore.YELLOW}{msg}{Style.RESET_ALL}" if self.fancy else msg + self.message_list.append(msg) def memory_message(self, msg): + # NOTE: never gets appended if self.debug: print(f"memory :: {msg}") message = f"{Fore.LIGHTMAGENTA_EX}{Style.BRIGHT}🧠 {Fore.LIGHTMAGENTA_EX}{msg}{Style.RESET_ALL}" if self.fancy else f"[memory] {msg}" - # self.message_list.append(message) print(message) def system_message(self, msg): + # NOTE: gets appended if self.debug: print(f"system :: {msg}") message = f"{Fore.MAGENTA}{Style.BRIGHT}🖥️ [system] {Fore.MAGENTA}{msg}{Style.RESET_ALL}" if self.fancy else f"[system] {msg}" - self.message_list.append(message) print(message) + self.message_list.append(msg) def user_message(self, msg, raw=False): if self.debug: @@ -129,6 +131,7 @@ class AutoGenInterface(object): else: message = f"{Fore.GREEN}{Style.BRIGHT}🧑 {Fore.GREEN}{msg_json}{Style.RESET_ALL}" if self.fancy else f"[user] {msg}" + # TODO should we ever be appending this? self.message_list.append(message) def function_message(self, msg): @@ -139,6 +142,7 @@ class AutoGenInterface(object): if isinstance(msg, dict): message = f"{Fore.RED}{Style.BRIGHT}⚡ [function] {Fore.RED}{msg}{Style.RESET_ALL}" + # TODO should we ever be appending this? self.message_list.append(message) return diff --git a/memgpt/autogen/memgpt_agent.py b/memgpt/autogen/memgpt_agent.py index 01d8b50b..23545f22 100644 --- a/memgpt/autogen/memgpt_agent.py +++ b/memgpt/autogen/memgpt_agent.py @@ -1,217 +1,61 @@ -from autogen.agentchat import Agent, ConversableAgent, UserProxyAgent, GroupChat, GroupChatManager -from memgpt.agent import Agent as _Agent - +import uuid from typing import Callable, Optional, List, Dict, Union, Any, Tuple +from autogen.agentchat import Agent, ConversableAgent, UserProxyAgent, GroupChat, GroupChatManager + +from memgpt.agent import Agent as MemGPTAgent from memgpt.autogen.interface import AutoGenInterface -from memgpt.persistence_manager import LocalStateManager import memgpt.system as system import memgpt.constants as constants import memgpt.utils as utils import memgpt.presets.presets as presets -from memgpt.config import AgentConfig, MemGPTConfig +from memgpt.config import MemGPTConfig +from memgpt.credentials import MemGPTCredentials from memgpt.cli.cli import attach from memgpt.cli.cli_load import load_directory, load_webpage, load_index, load_database, load_vector_database from memgpt.agent_store.storage import StorageConnector, TableType +from memgpt.metadata import MetadataStore, save_agent +from memgpt.data_types import AgentState, User, LLMConfig, EmbeddingConfig -def create_memgpt_autogen_agent_from_config( - name: str, - system_message: Optional[str] = "You are a helpful AI Assistant.", - is_termination_msg: Optional[Callable[[Dict], bool]] = None, - max_consecutive_auto_reply: Optional[int] = None, - human_input_mode: Optional[str] = "ALWAYS", - function_map: Optional[Dict[str, Callable]] = None, - code_execution_config: Optional[Union[Dict, bool]] = None, - llm_config: Optional[Union[Dict, bool]] = None, - # config setup for non-memgpt agents: - nonmemgpt_llm_config: Optional[Union[Dict, bool]] = None, - default_auto_reply: Optional[Union[str, Dict, None]] = "", - interface_kwargs: Dict = None, - skip_verify: bool = False, -): - """Same function signature as used in base AutoGen, but creates a MemGPT agent - - Construct AutoGen config workflow in a clean way. - """ - llm_config = llm_config["config_list"][0] - - if interface_kwargs is None: - interface_kwargs = {} - - # The "system message" in AutoGen becomes the persona in MemGPT - persona_desc = utils.get_persona_text(constants.DEFAULT_PERSONA) if system_message == "" else system_message - # The user profile is based on the input mode - if human_input_mode == "ALWAYS": - user_desc = "" - elif human_input_mode == "TERMINATE": - user_desc = "Work by yourself, the user won't reply until you output `TERMINATE` to end the conversation." - else: - user_desc = "Work by yourself, the user won't reply. Elaborate as much as possible." - - # If using azure or openai, save the credentials to the config - config = MemGPTConfig.load() - # input(f"llm_config! {llm_config}") - # input(f"config! {config}") - if llm_config["model_endpoint_type"] in ["azure", "openai"] or llm_config["model_endpoint_type"] != config.model_endpoint_type: - # we load here to make sure we don't override existing values - # all we want to do is add extra credentials - - if llm_config["model_endpoint_type"] == "azure": - config.azure_key = llm_config["azure_key"] - config.azure_endpoint = llm_config["azure_endpoint"] - config.azure_version = llm_config["azure_version"] - llm_config.pop("azure_key") - llm_config.pop("azure_endpoint") - llm_config.pop("azure_version") - - elif llm_config["model_endpoint_type"] == "openai": - config.openai_key = llm_config["openai_key"] - llm_config.pop("openai_key") - - # else: - # config.model_endpoint_type = llm_config["model_endpoint_type"] - - config.save() - - # if llm_config["model_endpoint"] != config.model_endpoint: - # config.model_endpoint = llm_config["model_endpoint"] - # config.save() - - # Create an AgentConfig option from the inputs - llm_config.pop("name", None) - llm_config.pop("persona", None) - llm_config.pop("human", None) - agent_config = AgentConfig( - name=name, - persona=persona_desc, - human=user_desc, - **llm_config, - ) - - if function_map is not None or code_execution_config is not None: - raise NotImplementedError - - autogen_memgpt_agent = create_autogen_memgpt_agent( - agent_config, - default_auto_reply=default_auto_reply, - is_termination_msg=is_termination_msg, - interface_kwargs=interface_kwargs, - skip_verify=skip_verify, - ) - - if human_input_mode != "ALWAYS": - coop_agent1 = create_autogen_memgpt_agent( - agent_config, - default_auto_reply=default_auto_reply, - is_termination_msg=is_termination_msg, - interface_kwargs=interface_kwargs, - skip_verify=skip_verify, - ) - if default_auto_reply != "": - coop_agent2 = UserProxyAgent( - "User_proxy", - human_input_mode="NEVER", - default_auto_reply=default_auto_reply, - ) - else: - coop_agent2 = create_autogen_memgpt_agent( - agent_config, - default_auto_reply=default_auto_reply, - is_termination_msg=is_termination_msg, - interface_kwargs=interface_kwargs, - skip_verify=skip_verify, - ) - - groupchat = GroupChat( - agents=[autogen_memgpt_agent, coop_agent1, coop_agent2], - messages=[], - max_round=12 if max_consecutive_auto_reply is None else max_consecutive_auto_reply, - ) - assert nonmemgpt_llm_config is not None - manager = GroupChatManager(name=name, groupchat=groupchat, llm_config=nonmemgpt_llm_config) - return manager - - else: - return autogen_memgpt_agent - - -def create_autogen_memgpt_agent( - agent_config, - # interface and persistence manager - skip_verify=False, - interface=None, - interface_kwargs={}, - persistence_manager=None, - persistence_manager_kwargs=None, - default_auto_reply: Optional[Union[str, Dict, None]] = "", - is_termination_msg: Optional[Callable[[Dict], bool]] = None, -): - """ - See AutoGenInterface.__init__ for available options you can pass into - `interface_kwargs`. For example, MemGPT's inner monologue and functions are - off by default so that they are not visible to the other agents. You can - turn these on by passing in - ``` - interface_kwargs={ - "debug": True, # to see all MemGPT activity - "show_inner_thoughts: True # to print MemGPT inner thoughts "globally" - # (visible to all AutoGen agents) - } - ``` - """ - # TODO: more gracefully integrate reuse of MemGPT agents. Right now, we are creating a new MemGPT agent for - # every call to this function, because those scripts using create_autogen_memgpt_agent may contain calls - # to non-idempotent agent functions like `attach`. - - interface = AutoGenInterface(**interface_kwargs) if interface is None else interface - if persistence_manager_kwargs is None: - persistence_manager_kwargs = { - "agent_config": agent_config, - } - persistence_manager = LocalStateManager(**persistence_manager_kwargs) if persistence_manager is None else persistence_manager - - memgpt_agent = presets.create_agent_from_preset( - agent_config.preset, - agent_config, - agent_config.model, - agent_config.persona, # note: extracting the raw text, not pulling from a file - agent_config.human, # note: extracting raw text, not pulling from a file - interface, - persistence_manager, - ) - - autogen_memgpt_agent = MemGPTAgent( - name=agent_config.name, - agent=memgpt_agent, - default_auto_reply=default_auto_reply, - is_termination_msg=is_termination_msg, - skip_verify=skip_verify, - ) - return autogen_memgpt_agent - - -class MemGPTAgent(ConversableAgent): +class MemGPTConversableAgent(ConversableAgent): def __init__( self, name: str, - agent: _Agent, - skip_verify=False, - concat_other_agent_messages=False, + agent: MemGPTAgent, + skip_verify: bool = False, + auto_save: bool = False, + concat_other_agent_messages: bool = False, is_termination_msg: Optional[Callable[[Dict], bool]] = None, default_auto_reply: Optional[Union[str, Dict, None]] = "", - # TODO: pass in MemGPT config (needed to create DB connections) ): - super().__init__(name) + """A wrapper around a MemGPT agent that implements the AutoGen ConversibleAgent functions + + This allows the MemGPT agent to be used in an AutoGen groupchat + """ + super().__init__(name, llm_config=False) self.agent = agent self.skip_verify = skip_verify + self.auto_save = auto_save + self.concat_other_agent_messages = concat_other_agent_messages - self.register_reply([Agent, None], MemGPTAgent._generate_reply_for_user_message) + self.register_reply([Agent, None], MemGPTConversableAgent._generate_reply_for_user_message) self.messages_processed_up_to_idx = 0 self._default_auto_reply = default_auto_reply self._is_termination_msg = is_termination_msg if is_termination_msg is not None else (lambda x: x == "TERMINATE") + config = MemGPTConfig.load() + self.ms = MetadataStore(config) + + def save(self): + """Save the underlying MemGPT agent to the database""" + try: + save_agent(agent=self.agent, ms=self.ms) + except Exception as e: + print(f"Failed to save MemGPT AutoGen agent\n{self.agent}\nError: {str(e)}") + raise + def load(self, name: str, type: str, **kwargs): # call load function based on type if type == "directory": @@ -229,22 +73,16 @@ class MemGPTAgent(ConversableAgent): def attach(self, data_source: str): # attach new data - attach(self.agent.config.name, data_source) - - # update agent config - self.agent.config.attach_data_source(data_source) - - # reload agent with new data source - # TODO: @charles we will need to pass in the MemGPT config here to get the DB URIs (not contained in agent) - self.agent.persistence_manager.archival_memory.storage = StorageConnector.get_archival_storage_connector( - agent_config=self.agent.config - ) + attach(agent_name=self.agent.agent_state.name, data_source=data_source) def load_and_attach(self, name: str, type: str, force=False, **kwargs): # check if data source already exists - data_sources = StorageConnector.get_metadata_storage_connector(TableType.DATA_SOURCES).get_all() - data_sources = [source.name for source in data_sources] - if name in data_sources and not force: + data_source_options = self.ms.list_sources(user_id=self.agent.agent_state.user_id) + data_source_options = [s.name for s in data_source_options] + + kwargs["user_id"] = self.agent.agent_state.user_id + + if name in data_source_options and not force: print(f"Data source {name} already exists. Use force=True to overwrite.") self.attach(name) else: @@ -289,6 +127,9 @@ class MemGPTAgent(ConversableAgent): sender: Optional[Agent] = None, config: Optional[Any] = None, ) -> Tuple[bool, Union[str, Dict, None]]: + assert isinstance( + self.agent.interface, AutoGenInterface + ), f"MemGPT AutoGen Agent is using the wrong interface - {self.agent.interface}" self.agent.interface.reset_message_list() new_messages = self.find_new_messages(messages) @@ -299,7 +140,7 @@ class MemGPTAgent(ConversableAgent): user_message = "\n".join([self.format_other_agent_message(m) for m in new_messages]) else: # Extend the MemGPT message list with multiple 'user' messages, then push the last one with agent.step() - self.agent.messages.extend(new_messages[:-1]) + self.agent.append_to_messages(new_messages[:-1]) user_message = new_messages[-1] elif new_messages_count == 1: user_message = new_messages[0] @@ -334,8 +175,13 @@ class MemGPTAgent(ConversableAgent): return True, None # Pass back to AutoGen the pretty-printed calls MemGPT made to the interface - pretty_ret = MemGPTAgent.pretty_concat(self.agent.interface.message_list) + pretty_ret = MemGPTConversableAgent.pretty_concat(self.agent.interface.message_list) self.messages_processed_up_to_idx += new_messages_count + + # If auto_save is on, save after every full step + if self.auto_save: + self.save() + return True, pretty_ret @staticmethod @@ -355,3 +201,288 @@ class MemGPTAgent(ConversableAgent): ret["content"] = "..." return ret + + +def update_config_from_dict(config_object: Union[LLMConfig, EmbeddingConfig], config_dict: dict) -> bool: + """Utility method used in the agent creation process for AutoGen + + Update the attributes of a configuration object based on a dictionary. + + :param config_object: The configuration object to be updated. + :param config_dict: The dictionary containing new values for the configuration. + """ + was_modified = False + for attr in dir(config_object): + # Filter out private attributes and methods + if not attr.startswith("_") and not callable(getattr(config_object, attr)): + if attr in config_dict: + # Cast the value to the type of the attribute in config_object + attr_type = type(getattr(config_object, attr)) + try: + setattr(config_object, attr, attr_type(config_dict[attr])) + was_modified = True + except TypeError: + print(f"Type mismatch for attribute {attr}, cannot cast {config_dict[attr]} to {attr_type}") + + return was_modified + + +def load_autogen_memgpt_agent( + agent_config: dict, + skip_verify: bool = False, + auto_save: bool = False, + interface: bool = None, + interface_kwargs: dict = {}, + default_auto_reply: Optional[Union[str, Dict, None]] = "", + is_termination_msg: Optional[Callable[[Dict], bool]] = None, +) -> MemGPTConversableAgent: + """Load a MemGPT agent into a wrapped ConversableAgent class""" + if "name" not in agent_config: + raise ValueError("Must provide 'name' in agent_config to load an agent") + + interface = AutoGenInterface(**interface_kwargs) if interface is None else interface + + config = MemGPTConfig.load() + # Create the default user, or load the specified user + ms = MetadataStore(config) + if "user_id" not in agent_config: + user_id = uuid.UUID(config.anon_clientid) + user = ms.get_user(user_id=user_id) + if user is None: + ms.create_user(User(id=user_id)) + user = ms.get_user(user_id=user_id) + if user is None: + raise ValueError(f"Failed to create default user {str(user_id)} in database.") + else: + user_id = uuid.UUID(agent_config["user_id"]) + user = ms.get_user(user_id=user_id) + + # Make sure that the agent already exists + agent_state = ms.get_agent(agent_name=agent_config["name"], user_id=user.id) + if agent_state is None: + raise ValueError(f"Couldn't find an agent named {agent_config['name']} in the agent database") + + # Create the agent object directly from the loaded state (not via preset creation) + try: + memgpt_agent = MemGPTAgent(agent_state=agent_state, interface=interface) + except Exception as e: + print(f"Failed to create an agent object from agent state =\n{agent_state}") + raise + + # If the user provided new config information, write it out to the agent + # E.g. if the user is trying to load the same agent, but on a new LLM backend + llm_config_was_modified = update_config_from_dict(memgpt_agent.agent_state.llm_config, agent_config) + embedding_config_was_modified = update_config_from_dict(memgpt_agent.agent_state.embedding_config, agent_config) + if llm_config_was_modified or embedding_config_was_modified: + save_agent(agent=memgpt_agent, ms=ms) + + # After creating the agent, we then need to wrap it in a ConversableAgent so that it can be plugged into AutoGen + autogen_memgpt_agent = MemGPTConversableAgent( + name=agent_state.name, + agent=memgpt_agent, + default_auto_reply=default_auto_reply, + is_termination_msg=is_termination_msg, + skip_verify=skip_verify, + auto_save=auto_save, + ) + return autogen_memgpt_agent + + +def create_autogen_memgpt_agent( + agent_config: dict, + skip_verify: bool = False, + auto_save: bool = False, + interface: bool = None, + interface_kwargs: dict = {}, + default_auto_reply: Optional[Union[str, Dict, None]] = "", + is_termination_msg: Optional[Callable[[Dict], bool]] = None, +) -> MemGPTConversableAgent: + """ + See AutoGenInterface.__init__ for available options you can pass into + `interface_kwargs`. For example, MemGPT's inner monologue and functions are + off by default so that they are not visible to the other agents. You can + turn these on by passing in + ``` + interface_kwargs={ + "debug": True, # to see all MemGPT activity + "show_inner_thoughts: True # to print MemGPT inner thoughts "globally" + # (visible to all AutoGen agents) + } + ``` + """ + interface = AutoGenInterface(**interface_kwargs) if interface is None else interface + + config = MemGPTConfig.load() + llm_config = config.default_llm_config + embedding_config = config.default_embedding_config + + # Overwrite parts of the LLM and embedding configs that were passed into the config dicts + llm_config_was_modified = update_config_from_dict(llm_config, agent_config) + embedding_config_was_modified = update_config_from_dict(embedding_config, agent_config) + + # Create the default user, or load the specified user + ms = MetadataStore(config) + if "user_id" not in agent_config: + user_id = uuid.UUID(config.anon_clientid) + user = ms.get_user(user_id=user_id) + if user is None: + ms.create_user(User(id=user_id)) + user = ms.get_user(user_id=user_id) + if user is None: + raise ValueError(f"Failed to create default user {str(user_id)} in database.") + else: + user_id = uuid.UUID(agent_config["user_id"]) + user = ms.get_user(user_id=user_id) + + agent_state = AgentState( + name=agent_config["name"], + user_id=user_id, + persona=agent_config["persona"], + human=agent_config["human"], + llm_config=llm_config, + embedding_config=embedding_config, + preset=agent_config["preset"], + ) + try: + memgpt_agent = presets.create_agent_from_preset( + agent_state=agent_state, + interface=interface, + persona_is_file=False, + human_is_file=False, + ) + # Save agent in database immediately after writing + save_agent(agent=memgpt_agent, ms=ms) + except ValueError as e: + raise ValueError(f"Failed to create agent from provided information:\n{agent_config}\n\nError: {str(e)}") + + # After creating the agent, we then need to wrap it in a ConversableAgent so that it can be plugged into AutoGen + autogen_memgpt_agent = MemGPTConversableAgent( + name=agent_state.name, + agent=memgpt_agent, + default_auto_reply=default_auto_reply, + is_termination_msg=is_termination_msg, + skip_verify=skip_verify, + auto_save=auto_save, + ) + return autogen_memgpt_agent + + +def create_memgpt_autogen_agent_from_config( + name: str, + system_message: Optional[str] = "You are a helpful AI Assistant.", + is_termination_msg: Optional[Callable[[Dict], bool]] = None, + max_consecutive_auto_reply: Optional[int] = None, + human_input_mode: Optional[str] = "ALWAYS", + function_map: Optional[Dict[str, Callable]] = None, + code_execution_config: Optional[Union[Dict, bool]] = None, + llm_config: Optional[Union[Dict, bool]] = None, + # config setup for non-memgpt agents: + nonmemgpt_llm_config: Optional[Union[Dict, bool]] = None, + default_auto_reply: Optional[Union[str, Dict, None]] = "", + interface_kwargs: Dict = None, + skip_verify: bool = False, + auto_save: bool = False, +) -> MemGPTConversableAgent: + """Same function signature as used in base AutoGen, but creates a MemGPT agent + + Construct AutoGen config workflow in a clean way. + """ + llm_config = llm_config["config_list"][0] + + if interface_kwargs is None: + interface_kwargs = {} + + # The "system message" in AutoGen becomes the persona in MemGPT + persona_desc = utils.get_persona_text(constants.DEFAULT_PERSONA) if system_message == "" else system_message + # The user profile is based on the input mode + if human_input_mode == "ALWAYS": + user_desc = "" + elif human_input_mode == "TERMINATE": + user_desc = "Work by yourself, the user won't reply until you output `TERMINATE` to end the conversation." + else: + user_desc = "Work by yourself, the user won't reply. Elaborate as much as possible." + + # If using azure or openai, save the credentials to the config + config = MemGPTConfig.load() + credentials = MemGPTCredentials.load() + + if ( + llm_config["model_endpoint_type"] in ["azure", "openai"] + or llm_config["model_endpoint_type"] != config.default_llm_config.model_endpoint_type + ): + # we load here to make sure we don't override existing values + # all we want to do is add extra credentials + + if llm_config["model_endpoint_type"] == "azure": + credentials.azure_key = llm_config["azure_key"] + credentials.azure_endpoint = llm_config["azure_endpoint"] + credentials.azure_version = llm_config["azure_version"] + llm_config.pop("azure_key") + llm_config.pop("azure_endpoint") + llm_config.pop("azure_version") + + elif llm_config["model_endpoint_type"] == "openai": + credentials.openai_key = llm_config["openai_key"] + llm_config.pop("openai_key") + + credentials.save() + + # Create an AgentConfig option from the inputs + llm_config.pop("name", None) + llm_config.pop("persona", None) + llm_config.pop("human", None) + agent_config = dict( + name=name, + persona=persona_desc, + human=user_desc, + **llm_config, + ) + + if function_map is not None or code_execution_config is not None: + raise NotImplementedError + + autogen_memgpt_agent = create_autogen_memgpt_agent( + agent_config, + default_auto_reply=default_auto_reply, + is_termination_msg=is_termination_msg, + interface_kwargs=interface_kwargs, + skip_verify=skip_verify, + auto_save=auto_save, + ) + + if human_input_mode != "ALWAYS": + coop_agent1 = create_autogen_memgpt_agent( + agent_config, + default_auto_reply=default_auto_reply, + is_termination_msg=is_termination_msg, + interface_kwargs=interface_kwargs, + skip_verify=skip_verify, + auto_save=auto_save, + ) + if default_auto_reply != "": + coop_agent2 = UserProxyAgent( + "User_proxy", + human_input_mode="NEVER", + default_auto_reply=default_auto_reply, + ) + else: + coop_agent2 = create_autogen_memgpt_agent( + agent_config, + default_auto_reply=default_auto_reply, + is_termination_msg=is_termination_msg, + interface_kwargs=interface_kwargs, + skip_verify=skip_verify, + auto_save=auto_save, + ) + + groupchat = GroupChat( + agents=[autogen_memgpt_agent, coop_agent1, coop_agent2], + messages=[], + max_round=12 if max_consecutive_auto_reply is None else max_consecutive_auto_reply, + ) + assert nonmemgpt_llm_config is not None + manager = GroupChatManager(name=name, groupchat=groupchat, llm_config=nonmemgpt_llm_config) + return manager + + else: + return autogen_memgpt_agent diff --git a/memgpt/cli/cli.py b/memgpt/cli/cli.py index a5f082a6..7698fbe3 100644 --- a/memgpt/cli/cli.py +++ b/memgpt/cli/cli.py @@ -647,8 +647,46 @@ def run( run_agent_loop(memgpt_agent, config, first, ms, no_verify) # TODO: add back no_verify +def delete_agent( + agent_name: str = typer.Option(help="Specify agent to delete"), + user_id: str = None, +): + """Delete an agent from the database""" + # use client ID is no user_id provided + config = MemGPTConfig.load() + ms = MetadataStore(config) + if user_id is None: + user = create_default_user_or_exit(config, ms) + else: + user = ms.get_user(user_id=uuid.UUID(user_id)) + + try: + agent = ms.get_agent(agent_name=agent_name, user_id=user.id) + except Exception as e: + typer.secho(f"Failed to get agent {agent_name}\n{e}", fg=typer.colors.RED) + sys.exit(1) + + if agent is None: + typer.secho(f"Couldn't find agent named '{agent_name}' to delete", fg=typer.colors.RED) + sys.exit(1) + + confirm = questionary.confirm(f"Are you sure you want to delete agent '{agent_name}' (id={agent.id})?", default=False).ask() + if confirm is None: + raise KeyboardInterrupt + if not confirm: + typer.secho(f"Cancelled agent deletion '{agent_name}' (id={agent.id})", fg=typer.colors.GREEN) + return + + try: + ms.delete_agent(agent_id=agent.id) + typer.secho(f"🕊️ Successfully deleted agent '{agent_name}' (id={agent.id})", fg=typer.colors.GREEN) + except Exception as e: + typer.secho(f"Failed to delete agent '{agent_name}' (id={agent.id})", fg=typer.colors.RED) + sys.exit(1) + + def attach( - agent: str = typer.Option(help="Specify agent to attach data to"), + agent_name: str = typer.Option(help="Specify agent to attach data to"), data_source: str = typer.Option(help="Data source to attach to avent"), user_id: uuid.UUID = None, ): @@ -662,7 +700,8 @@ def attach( from tqdm import tqdm ms = MetadataStore(config) - agent = ms.get_agent(agent_name=agent, user_id=user_id) + agent = ms.get_agent(agent_name=agent_name, user_id=user_id) + print(agent.id, agent.user_id, user_id) source = ms.get_source(source_name=data_source, user_id=user_id) assert source is not None, f"Source {data_source} does not exist for user {user_id}" @@ -697,7 +736,7 @@ def attach( total_agent_passages = dest_storage.size() typer.secho( - f"Attached data source {data_source} to agent {agent}, consisting of {len(passages)}. Agent now has {total_agent_passages} embeddings in archival memory.", + f"Attached data source {data_source} to agent {agent_name}, consisting of {len(passages)}. Agent now has {total_agent_passages} embeddings in archival memory.", fg=typer.colors.GREEN, ) except KeyboardInterrupt: diff --git a/memgpt/cli/cli_load.py b/memgpt/cli/cli_load.py index a473422e..50d4df82 100644 --- a/memgpt/cli/cli_load.py +++ b/memgpt/cli/cli_load.py @@ -8,7 +8,7 @@ memgpt load --name [ADDITIONAL ARGS] """ -from typing import List +from typing import List, Optional from tqdm import tqdm import numpy as np import typer @@ -222,39 +222,41 @@ default_extensions = ".txt,.md,.pdf" @app.command("directory") def load_directory( name: str = typer.Option(help="Name of dataset to load."), - input_dir: str = typer.Option(None, help="Path to directory containing dataset."), - input_files: List[str] = typer.Option(None, help="List of paths to files containing dataset."), + input_dir: Optional[str] = typer.Option(None, help="Path to directory containing dataset."), + input_files: Optional[List[str]] = typer.Option(None, help="List of paths to files containing dataset."), recursive: bool = typer.Option(False, help="Recursively search for files in directory."), extensions: str = typer.Option(default_extensions, help="Comma separated list of file extensions to load"), - user_id: str = typer.Option(None, help="User ID to associate with dataset."), + user_id: Optional[str] = typer.Option(None, help="User ID to associate with dataset."), ): try: from llama_index import SimpleDirectoryReader - if recursive: + if recursive == True: assert input_dir is not None, "Must provide input directory if recursive is True." if input_dir is not None: reader = SimpleDirectoryReader( - input_dir=input_dir, + input_dir=str(input_dir), recursive=recursive, - required_exts=[ext.strip() for ext in extensions.split(",")], + required_exts=[ext.strip() for ext in str(extensions).split(",")], ) else: - reader = SimpleDirectoryReader(input_files=input_files) + assert input_files is not None, "Must provide input files if input_dir is None" + reader = SimpleDirectoryReader(input_files=[str(f) for f in input_files]) # load docs docs = reader.load_data() - store_docs(name, docs, user_id) + store_docs(str(name), docs, user_id) except ValueError as e: typer.secho(f"Failed to load directory from provided information.\n{e}", fg=typer.colors.RED) + raise @app.command("webpage") def load_webpage( name: str = typer.Option(help="Name of dataset to load."), - urls: List[str] = typer.Option(None, help="List of urls to load."), + urls: Optional[List[str]] = typer.Option(None, help="List of urls to load."), ): try: from llama_index import SimpleWebPageReader diff --git a/memgpt/main.py b/memgpt/main.py index ea64ab4c..c2b847c5 100644 --- a/memgpt/main.py +++ b/memgpt/main.py @@ -24,7 +24,7 @@ import memgpt.agent as agent import memgpt.system as system import memgpt.constants as constants import memgpt.errors as errors -from memgpt.cli.cli import run, attach, version, server, open_folder, quickstart, migrate +from memgpt.cli.cli import run, attach, version, server, open_folder, quickstart, migrate, delete_agent from memgpt.cli.cli_config import configure, list, add, delete from memgpt.cli.cli_load import app as load_app from memgpt.agent_store.storage import StorageConnector, TableType @@ -50,6 +50,8 @@ app.add_typer(load_app, name="load") app.command(name="migrate")(migrate) # benchmark command app.command(name="benchmark")(bench) +# delete agents +app.command(name="delete-agent")(delete_agent) def clear_line(strip_ui=False): @@ -120,7 +122,6 @@ def run_agent_loop(memgpt_agent, config: MemGPTConfig, first, ms: MetadataStore, # TODO: alternatively, only list sources with compatible embeddings, and print warning about non-compatible sources data_source_options = ms.list_sources(user_id=memgpt_agent.agent_state.user_id) - data_source_options = [s.name for s in data_source_options] if len(data_source_options) == 0: typer.secho( 'No sources available. You must load a souce with "memgpt load ..." before running /attach.', @@ -133,7 +134,10 @@ def run_agent_loop(memgpt_agent, config: MemGPTConfig, first, ms: MetadataStore, valid_options = [] invalid_options = [] for source in data_source_options: - if source.embedding_model == memgpt_agent.embedding_model and source.embedding_dim == memgpt_agent.embedding_dim: + if ( + source.embedding_model == memgpt_agent.agent_state.embedding_config.embedding_model + and source.embedding_dim == memgpt_agent.agent_state.embedding_config.embedding_dim + ): valid_options.append(source.name) else: invalid_options.append(source.name) @@ -148,7 +152,7 @@ def run_agent_loop(memgpt_agent, config: MemGPTConfig, first, ms: MetadataStore, data_source = questionary.select("Select data source", choices=valid_options).ask() # attach new data - attach(memgpt_agent.config.name, data_source) + attach(memgpt_agent.agent_state.name, data_source) continue diff --git a/memgpt/presets/presets.py b/memgpt/presets/presets.py index ab698807..6325f930 100644 --- a/memgpt/presets/presets.py +++ b/memgpt/presets/presets.py @@ -11,7 +11,7 @@ preset_options = list(available_presets.keys()) # def create_agent_from_preset(preset_name, agent_config, model, persona, human, interface, persistence_manager): -def create_agent_from_preset(agent_state: AgentState, interface: AgentInterface): +def create_agent_from_preset(agent_state: AgentState, interface: AgentInterface, persona_is_file: bool = True, human_is_file: bool = True): """Initialize a new agent from a preset (combination of system + function)""" # Input validation @@ -25,8 +25,8 @@ def create_agent_from_preset(agent_state: AgentState, interface: AgentInterface) raise ValueError(f"'state' must be uninitialized (empty)") preset_name = agent_state.preset - persona_file = agent_state.persona - human_file = agent_state.human + persona = agent_state.persona + human = agent_state.human model = agent_state.llm_config.model from memgpt.agent import Agent @@ -67,8 +67,8 @@ def create_agent_from_preset(agent_state: AgentState, interface: AgentInterface) # functions: dict, # schema definitions ONLY (function code linked at runtime) # messages: List[dict], # in-context messages agent_state.state = { - "persona": get_persona_text(persona_file), - "human": get_human_text(human_file), + "persona": get_persona_text(persona) if persona_is_file else persona, + "human": get_human_text(human) if human_is_file else human, "system": gpt_system.get_system_text(preset_system_prompt), "functions": preset_function_set_schemas, "messages": None, diff --git a/tests/test_load_archival.py b/tests/test_load_archival.py index 4173b08d..b84007bf 100644 --- a/tests/test_load_archival.py +++ b/tests/test_load_archival.py @@ -165,7 +165,7 @@ def test_load_directory(metadata_storage_connector, passage_storage_connector, c # attach data print("Attaching data...") - attach(agent=agent.name, data_source=name, user_id=user_id) + attach(agent_name=agent.name, data_source=name, user_id=user_id) # test to see if contained in storage assert len(passages) == conn.size()