From 2050949366b7e25bb13f4b5ed36c2bbba7f6ee4d Mon Sep 17 00:00:00 2001 From: Arsenii Shatokhin Date: Thu, 1 Feb 2024 08:15:34 +0400 Subject: [PATCH] Added ability to set settings_path when initializing Agency, refactored docstrings, returned previous synchronous SendMessage tool description --- agency_swarm/agency/agency.py | 80 +++++++++++++++++++---------------- agency_swarm/agents/agent.py | 54 ++++++++++++++--------- tests/test_agency.py | 8 +++- 3 files changed, 83 insertions(+), 59 deletions(-) diff --git a/agency_swarm/agency/agency.py b/agency_swarm/agency/agency.py index 5c3fccca..8148aee7 100644 --- a/agency_swarm/agency/agency.py +++ b/agency_swarm/agency/agency.py @@ -28,21 +28,28 @@ class ThreadsCallbacks(TypedDict): class Agency: ThreadType = Thread - send_message_tool_description = """Use this tool for synchronous communication with other agents within your agency. For ongoing dialogue, resend messages to specific agents. Communication is synchronous, without post-response tasks. Relay agent responses to the user, who lacks direct access. Continue using the tool for continuous interaction until task completion.""" + send_message_tool_description = """Use this tool to facilitate direct, synchronous communication between specialized agents within your agency. When you send a message using this tool, you receive a response exclusively from the designated recipient agent. To continue the dialogue, invoke this tool again with the desired recipient agent and your follow-up message. Remember, communication here is synchronous; the recipient agent won't perform any tasks post-response. You are responsible for relaying the recipient agent's responses back to the user, as the user does not have direct access to these replies. Keep engaging with the tool for continuous interaction until the task is fully resolved.""" send_message_tool_description_async = """Use this tool for asynchronous communication with other agents within your agency. Initiate tasks by messaging, and check status and responses later with the 'GetResponse' tool. Relay responses to the user, who instructs on status checks. Continue until task completion.""" - def __init__(self, agency_chart: List, shared_instructions: str = "", shared_files: List = None, + def __init__(self, + agency_chart: List, + shared_instructions: str = "", + shared_files: List = None, async_mode: Literal['threading'] = None, - settings_callbacks: SettingsCallbacks = None, threads_callbacks: ThreadsCallbacks = None): + settings_path: str = "./settings.json", + settings_callbacks: SettingsCallbacks = None, + threads_callbacks: ThreadsCallbacks = None): """ Initializes the Agency object, setting up agents, threads, and core functionalities. Parameters: - agency_chart: The structure defining the hierarchy and interaction of agents within the agency. - shared_instructions (str, optional): A path to a file containing shared instructions for all agents. Defaults to an empty string. - shared_files (list, optional): A list of folder paths with files containing shared resources for all agents. Defaults to an empty list. - settings_callbacks (SettingsCallbacks, optional): A dictionary containing functions to load and save settings for the agency. The keys must be "load" and "save". Both values must be defined. Defaults to None. - threads_callbacks (ThreadsCallbacks, optional): A dictionary containing functions to load and save threads for the agency. The keys must be "load" and "save". Both values must be defined. Defaults to None. + agency_chart: The structure defining the hierarchy and interaction of agents within the agency. + shared_instructions (str, optional): A path to a file containing shared instructions for all agents. Defaults to an empty string. + shared_files (list, optional): A list of folder paths with files containing shared resources for all agents. Defaults to an empty list. + async_mode (str, optional): The mode for asynchronous message processing. Defaults to None. + settings_path (str, optional): The path to the settings file for the agency. Must be json. If file does not exist, it will be created. Defaults to None. + settings_callbacks (SettingsCallbacks, optional): A dictionary containing functions to load and save settings for the agency. The keys must be "load" and "save". Both values must be defined. Defaults to None. + threads_callbacks (ThreadsCallbacks, optional): A dictionary containing functions to load and save threads for the agency. The keys must be "load" and "save". Both values must be defined. Defaults to None. This constructor initializes various components of the Agency, including CEO, agents, threads, and user interactions. It parses the agency chart to set up the organizational structure and initializes the messaging tools, agents, and threads necessary for the operation of the agency. Additionally, it prepares a main thread for user interactions. """ @@ -55,6 +62,7 @@ def __init__(self, agency_chart: List, shared_instructions: str = "", shared_fil self.agents = [] self.agents_and_threads = {} self.shared_files = shared_files if shared_files else [] + self.settings_path = settings_path self.settings_callbacks = settings_callbacks self.threads_callbacks = threads_callbacks @@ -78,12 +86,12 @@ def get_completion(self, message: str, message_files=None, yield_messages=True): Retrieves the completion for a given message from the main thread. Parameters: - message (str): The message for which completion is to be retrieved. - message_files (list, optional): A list of file ids to be sent as attachments with the message. Defaults to None. - yield_messages (bool, optional): Flag to determine if intermediate messages should be yielded. Defaults to True. + message (str): The message for which completion is to be retrieved. + message_files (list, optional): A list of file ids to be sent as attachments with the message. Defaults to None. + yield_messages (bool, optional): Flag to determine if intermediate messages should be yielded. Defaults to True. Returns: - Generator or final response: Depending on the 'yield_messages' flag, this method returns either a generator yielding intermediate messages or the final response from the main thread. + Generator or final response: Depending on the 'yield_messages' flag, this method returns either a generator yielding intermediate messages or the final response from the main thread. """ gen = self.main_thread.get_completion(message=message, message_files=message_files, yield_messages=yield_messages) @@ -102,8 +110,9 @@ def demo_gradio(self, height=600, dark_mode=True): Launches a Gradio-based demo interface for the agency chatbot. Parameters: - height (int, optional): The height of the chatbot widget in the Gradio interface. Default is 600. - dark_mode (bool, optional): Flag to determine if the interface should be displayed in dark mode. Default is True. + height (int, optional): The height of the chatbot widget in the Gradio interface. Default is 600. + dark_mode (bool, optional): Flag to determine if the interface should be displayed in dark mode. Default is True. + This method sets up and runs a Gradio interface, allowing users to interact with the agency's chatbot. It includes a text input for the user's messages and a chatbot interface for displaying the conversation. The method handles user input and chatbot responses, updating the interface dynamically. """ try: @@ -169,7 +178,7 @@ def run_demo(self): This function continuously prompts the user for input and displays responses from the agency's main thread. It leverages the generator pattern for asynchronous message processing. Output: - Outputs the responses from the agency's main thread to the command line. + Outputs the responses from the agency's main thread to the command line. """ while True: console.rule() @@ -207,13 +216,15 @@ def _init_agents(self): """ if self.settings_callbacks: loaded_settings = self.settings_callbacks["load"]() - with open(self.agents[0].get_settings_path(), 'w') as f: + with open(self.settings_path, 'w') as f: json.dump(loaded_settings, f, indent=4) for agent in self.agents: if "temp_id" in agent.id: agent.id = None + agent.add_shared_instructions(self.shared_instructions) + agent.settings_path = self.settings_path if self.shared_files: if isinstance(agent.files_folder, str): @@ -239,7 +250,7 @@ def _init_threads(self): No input parameters. Output Parameters: - This method does not return any value but updates the agents_and_threads attribute with initialized Thread objects. + This method does not return any value but updates the agents_and_threads attribute with initialized Thread objects. """ # load thread ids loaded_thread_ids = {} @@ -273,7 +284,7 @@ def _parse_agency_chart(self, agency_chart): Parses the provided agency chart to initialize and organize agents within the agency. Parameters: - agency_chart: A structure representing the hierarchical organization of agents within the agency. + agency_chart: A structure representing the hierarchical organization of agents within the agency. It can contain Agent objects and lists of Agent objects. This method iterates through each node in the agency chart. If a node is an Agent, it is set as the CEO if not already assigned. @@ -317,10 +328,10 @@ def _add_agent(self, agent): Adds an agent to the agency, assigning a temporary ID if necessary. Parameters: - agent (Agent): The agent to be added to the agency. + agent (Agent): The agent to be added to the agency. Returns: - int: The index of the added agent within the agency's agents list. + int: The index of the added agent within the agency's agents list. This method adds an agent to the agency's list of agents. If the agent does not have an ID, it assigns a temporary unique ID. It checks for uniqueness of the agent's name before addition. The method returns the index of the agent in the agency's agents list, which is used for referencing the agent within the agency. """ @@ -340,7 +351,7 @@ def _read_instructions(self, path): Reads shared instructions from a specified file and stores them in the agency. Parameters: - path (str): The file path from which to read the shared instructions. + path (str): The file path from which to read the shared instructions. This method opens the file located at the given path, reads its contents, and stores these contents in the 'shared_instructions' attribute of the agency. This is used to provide common guidelines or instructions to all agents within the agency. """ @@ -372,11 +383,11 @@ def _create_send_message_tool(self, agent: Agent, recipient_agents: List[Agent]) Parameters: - agent (Agent): The agent who will be sending messages. - recipient_agents (List[Agent]): A list of recipient agents who can receive messages. + agent (Agent): The agent who will be sending messages. + recipient_agents (List[Agent]): A list of recipient agents who can receive messages. Returns: - SendMessage: A SendMessage tool class that is dynamically created and configured for the given agent and its recipient agents. This tool allows the agent to send messages to the specified recipients, facilitating inter-agent communication within the agency. + SendMessage: A SendMessage tool class that is dynamically created and configured for the given agent and its recipient agents. This tool allows the agent to send messages to the specified recipients, facilitating inter-agent communication within the agency. """ recipient_names = [agent.name for agent in recipient_agents] recipients = Enum("recipient", {name: name for name in recipient_names}) @@ -485,13 +496,13 @@ def get_agent_by_name(self, agent_name): Retrieves an agent from the agency based on the agent's name. Parameters: - agent_name (str): The name of the agent to be retrieved. + agent_name (str): The name of the agent to be retrieved. Returns: - Agent: The agent object with the specified name. + Agent: The agent object with the specified name. Raises: - Exception: If no agent with the given name is found in the agency. + Exception: If no agent with the given name is found in the agency. """ for agent in self.agents: if agent.name == agent_name: @@ -503,10 +514,10 @@ def get_agents_by_names(self, agent_names): Retrieves a list of agent objects based on their names. Parameters: - agent_names: A list of strings representing the names of the agents to be retrieved. + agent_names: A list of strings representing the names of the agents to be retrieved. Returns: - A list of Agent objects corresponding to the given names. + A list of Agent objects corresponding to the given names. """ return [self.get_agent_by_name(agent_name) for agent_name in agent_names] @@ -515,7 +526,7 @@ def get_agent_ids(self): Retrieves the IDs of all agents currently in the agency. Returns: - List[str]: A list containing the unique IDs of all agents. + List[str]: A list containing the unique IDs of all agents. """ return [agent.id for agent in self.agents] @@ -523,11 +534,8 @@ def get_agent_names(self): """ Retrieves the names of all agents in the agency. - Parameters: - None - Returns: - List[str]: A list of names of all agents currently part of the agency. + List[str]: A list of names of all agents currently part of the agency. """ return [agent.name for agent in self.agents] @@ -536,7 +544,7 @@ def get_recipient_names(self): Retrieves the names of all agents in the agency. Returns: - A list of strings, where each string is the name of an agent in the agency. + A list of strings, where each string is the name of an agent in the agency. """ return [agent.name for agent in self.agents] @@ -545,6 +553,6 @@ def get_class_folder_path(self): Retrieves the absolute path of the directory containing the class file. Returns: - str: The absolute path of the directory where the class file is located. + str: The absolute path of the directory where the class file is located. """ return os.path.abspath(os.path.dirname(inspect.getfile(self.__class__))) diff --git a/agency_swarm/agents/agent.py b/agency_swarm/agents/agent.py index ae4fd044..35b55ea1 100644 --- a/agency_swarm/agents/agent.py +++ b/agency_swarm/agents/agent.py @@ -27,27 +27,37 @@ def assistant(self, value): def functions(self): return [tool for tool in self.tools if issubclass(tool, BaseTool)] - def __init__(self, id: str = None, name: str = None, description: str = None, instructions: str = "", - tools: List[Union[Type[BaseTool], Type[Retrieval], Type[CodeInterpreter]]] = None, - files_folder: Union[List[str], str] = None, schemas_folder: Union[List[str], str] = None, - api_headers: Dict[str, Dict[str, str]] = None, api_params: Dict[str, Dict[str, str]] = None, - file_ids: List[str] = None, metadata: Dict[str, str] = None, model: str = "gpt-4-turbo-preview"): + def __init__( + self, + id: str = None, + name: str = None, + description: str = None, + instructions: str = "", + tools: List[Union[Type[BaseTool], Type[Retrieval], Type[CodeInterpreter]]] = None, + files_folder: Union[List[str], str] = None, + schemas_folder: Union[List[str], str] = None, + api_headers: Dict[str, Dict[str, str]] = None, + api_params: Dict[str, Dict[str, str]] = None, + file_ids: List[str] = None, + metadata: Dict[str, str] = None, + model: str = "gpt-4-turbo-preview" + ): """ Initializes an Agent with specified attributes, tools, and OpenAI client. Parameters: - id (str, optional): Loads the assistant from OpenAI assistant ID. Assistant will be created or loaded from settings if ID is not provided. Defaults to None. - name (str, optional): Name of the agent. Defaults to the class name if not provided. - description (str, optional): A brief description of the agent's purpose. Defaults to None. - instructions (str, optional): Path to a file containing specific instructions for the agent. Defaults to an empty string. - tools (List[Union[Type[BaseTool], Type[Retrieval], Type[CodeInterpreter]]], optional): A list of tools (as classes) that the agent can use. Defaults to an empty list. - files_folder (Union[List[str], str], optional): Path or list of paths to directories containing files associated with the agent. Defaults to None. - schemas_folder (Union[List[str], str], optional): Path or list of paths to directories containing OpenAPI schemas associated with the agent. Defaults to None. - api_headers (Dict[str,Dict[str, str]], optional): Headers to be used for the openapi requests. Each key must be a full filename from schemas_folder. Defaults to an empty dictionary. - api_params (Dict[str, Dict[str, str]], optional): Extra params to be used for the openapi requests. Each key must be a full filename from schemas_folder. Defaults to an empty dictionary. - file_ids (List[str], optional): List of file IDs for files associated with the agent. Defaults to an empty list. - metadata (Dict[str, str], optional): Metadata associated with the agent. Defaults to an empty dictionary. - model (str, optional): The model identifier for the OpenAI API. Defaults to "gpt-4-1106-preview". + id (str, optional): Loads the assistant from OpenAI assistant ID. Assistant will be created or loaded from settings if ID is not provided. Defaults to None. + name (str, optional): Name of the agent. Defaults to the class name if not provided. + description (str, optional): A brief description of the agent's purpose. Defaults to None. + instructions (str, optional): Path to a file containing specific instructions for the agent. Defaults to an empty string. + tools (List[Union[Type[BaseTool], Type[Retrieval], Type[CodeInterpreter]]], optional): A list of tools (as classes) that the agent can use. Defaults to an empty list. + files_folder (Union[List[str], str], optional): Path or list of paths to directories containing files associated with the agent. Defaults to None. + schemas_folder (Union[List[str], str], optional): Path or list of paths to directories containing OpenAPI schemas associated with the agent. Defaults to None. + api_headers (Dict[str,Dict[str, str]], optional): Headers to be used for the openapi requests. Each key must be a full filename from schemas_folder. Defaults to an empty dictionary. + api_params (Dict[str, Dict[str, str]], optional): Extra params to be used for the openapi requests. Each key must be a full filename from schemas_folder. Defaults to an empty dictionary. + file_ids (List[str], optional): List of file IDs for files associated with the agent. Defaults to an empty list. + metadata (Dict[str, str], optional): Metadata associated with the agent. Defaults to an empty dictionary. + model (str, optional): The model identifier for the OpenAI API. Defaults to "gpt-4-turbo-preview". This constructor sets up the agent with its unique properties, initializes the OpenAI client, reads instructions if provided, and uploads any associated files. """ @@ -66,6 +76,8 @@ def __init__(self, id: str = None, name: str = None, description: str = None, in self.metadata = metadata if metadata else {} self.model = model + self.settings_path = './settings.json' + # private attributes self._assistant: Any = None self._shared_instructions = None @@ -85,7 +97,7 @@ def init_oai(self): This method handles the initialization and potential updates of the agent's OpenAI assistant. It loads the assistant based on a saved ID, updates the assistant if necessary, or creates a new assistant if it doesn't exist. After initialization or update, it saves the assistant's settings. Output: - self: Returns the agent instance for chaining methods or further processing. + self: Returns the agent instance for chaining methods or further processing. """ # check if settings.json exists @@ -393,10 +405,10 @@ def _check_parameters(self, assistant_settings): Checks if the agent's parameters match with the given assistant settings. Parameters: - assistant_settings (dict): A dictionary containing the settings of an assistant. + assistant_settings (dict): A dictionary containing the settings of an assistant. Returns: - bool: True if all the agent's parameters match the assistant settings, False otherwise. + bool: True if all the agent's parameters match the assistant settings, False otherwise. This method compares the current agent's parameters such as name, description, instructions, tools, file IDs, metadata, and model with the given assistant settings. It uses DeepDiff to compare complex structures like tools and metadata. If any parameter does not match, it returns False; otherwise, it returns True. """ @@ -450,7 +462,7 @@ def _update_settings(self): # --- Helper Methods --- def get_settings_path(self): - return os.path.join("./", 'settings.json') + return self.settings_path def _read_instructions(self): class_path = os.path.normpath(os.path.join(self.get_class_folder_path(), self.instructions)) diff --git a/tests/test_agency.py b/tests/test_agency.py index 4ff819e3..776c760f 100644 --- a/tests/test_agency.py +++ b/tests/test_agency.py @@ -161,7 +161,7 @@ def test_4_agent_communication(self): def test_5_load_from_db(self): """it should load agents from db""" - os.rename("settings.json", "settings2.json") + # os.rename("settings.json", "settings2.json") previous_loaded_thread_ids = self.__class__.loaded_thread_ids previous_loaded_agents_settings = self.__class__.loaded_agents_settings @@ -177,10 +177,14 @@ def test_5_load_from_db(self): [ceo, agent1], [agent1, agent2]], shared_instructions="This is a shared instruction", + settings_path="./settings2.json", settings_callbacks=self.__class__.settings_callbacks, threads_callbacks=self.__class__.threads_callbacks, ) + # check that settings are the same + self.assertTrue(len(agency.agents) == len(self.__class__.agency.agents)) + os.remove("settings.json") os.rename("settings2.json", "settings.json") @@ -222,7 +226,7 @@ def test_7_async_agent_communication(self): time.sleep(10) - message = self.__class__.agency.get_completion("Please check response. If the you get TestAgent1's response, say 'success', if the agent does not respond, or if you get a system notification instead say 'error'.", + message = self.__class__.agency.get_completion("Please check response. If the you get TestAgent1's Response like that the message was sent to TestAgent2 or that the process was initiated, say 'success'. If you don't get TestAgent1's Response, or if you get a System Notification instead, or an error, say 'error'.", yield_messages=False) self.assertFalse('error' in message.lower())