diff --git a/.gitignore b/.gitignore index 4c7bc59..766fe2a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ key_openai.txt -/**/*/.env \ No newline at end of file +/**/*/.env +*.pyc +/**/*/agentsIds.env +/**/*/__pycache__ +.vscode \ No newline at end of file diff --git a/README.md b/README.md index 6efb9f9..450f6f7 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,33 @@ We have our first GPT Concierge. You can chat with this custom ChatGPT to figure out what's going on! - **HAAS Board Concierge:** [https://chat.openai.com/g/g-MIssTuE2b-haas-board-concierge](https://chat.openai.com/g/g-MIssTuE2b-haas-board-concierge) +- **HAAS Assistant:** [https://chat.openai.com/g/g-lIAp9qowx-haas-assistant](https://chat.openai.com/g/g-lIAp9qowx-haas-assistant) (Similar function as above but markedly faster) -## Overview +## Public Discord + +The Autonomous AI Lab discord for the ACE Framework and HAAS Project is now open: https://discord.gg/mJKUYNm8qY + +> !!!! IMPORTANT NOTE: This repo is still the single source of truth! If it's not on this repo, it doesn't exist! Discord is merely for convenience. + +# Project Principles + +## Move Fast, Break Stuff + +This is first and foremost a high velocity hacking group. + +## Cutting Edge Only + +Exclusively use cutting edge stuff, like OpenAI's latest Agents endpoint. For exclusively Open Source, go check out the ACE Framework: https://github.com/daveshap/ACE_Framework + +## Full Autonomy + +Fully autonomous swarms are the goal. That means a human does not need to be in the loop telling it what to do, supervising, or anything. Characteristics of a fully autonomous swarm: + +1. **Self-Directing:** Once instantiated, the swarm pursues its mission or goals without supervision. It may self-direct based on principles such as the heuristic imperatives, or by specific mission parameters. +2. **Self-Correcting:** The swarm must detect and correct technical, strategic, epistemic, and other errors and then correct them. +3. **Self-Improving:** Eventually, the swarm should enhance its own fundamental capabilities over time. + +# Overview The Hierarchical Autonomous Agent Swarm (HAAS) is a groundbreaking initiative that leverages OpenAI's latest advancements in agent-based APIs to create a self-organizing and ethically governed ecosystem of AI agents. Drawing inspiration from the ACE Framework, HAAS introduces a novel approach to AI governance and operation, where a hierarchy of specialized agents, each with distinct roles and capabilities, collaborate to solve complex problems and perform a wide array of tasks. diff --git a/agent_builder/create.py b/agent_builder/create.py deleted file mode 100644 index 1789c5e..0000000 --- a/agent_builder/create.py +++ /dev/null @@ -1,59 +0,0 @@ -from openai import OpenAI -import os -import dotenv -dotenv.load_dotenv() - -agents_path = 'agents' -api_key = os.getenv('OPENAI_API_KEY') -if api_key is None: - raise ValueError('The OPENAI_API_KEY environment variable is not set.') - -client = OpenAI(api_key=api_key) - -# Check if the 'agents' folder is empty or doesn't exist -if not os.path.exists(agents_path) or not os.path.isdir(agents_path) or not os.listdir(agents_path): - raise ValueError('The "agents" folder is missing, not a directory, or empty.') - -# Iterate over each folder inside the 'agents' folder -for agent_name in os.listdir(agents_path): - agent_folder = os.path.join(agents_path, agent_name) - if os.path.isdir(agent_folder): - # Read contents from the 'instructions.md' file - instructions = '' - instructions_file_path = os.path.join(agent_folder, 'instructions.md') - if os.path.isfile(instructions_file_path): - with open(instructions_file_path, 'r') as f: - instructions = f.read() - - # Check for the 'files' subfolder and process its contents - files = [] - files_folder = os.path.join(agent_folder, 'files') - if os.path.isdir(files_folder): - for filename in os.listdir(files_folder): - file_path = os.path.join(files_folder, filename) - with open(file_path, 'rb') as file_data: - # Upload each file to OpenAI - file_object = client.files.create(file=file_data, purpose='assistants') - files.append({"name": filename, "id": file_object.id}) - - print(agent_name) - print("") - print(instructions) - if files: - print("") - print(f"Files: {list(map(lambda x: x['name'], files))}") - - create_params = { - "name": agent_name, - "instructions": instructions, - "model": 'gpt-4-1106-preview', - "tools": [{'type': 'code_interpreter'}, {'type': 'retrieval'}] - } - - # Only include 'file_ids' if there are files - if files: - create_params['file_ids'] = list(map(lambda x: x['id'], files)) - - # Create the assistant using the uploaded file IDs if files exist - assistant = client.beta.assistants.create(**create_params) - print("***********************************************") \ No newline at end of file diff --git a/agent_builder/README.md b/agents/agent_builder/README.md similarity index 100% rename from agent_builder/README.md rename to agents/agent_builder/README.md diff --git a/agent_builder/agent_definition.md b/agents/agent_builder/agent_definition.md similarity index 100% rename from agent_builder/agent_definition.md rename to agents/agent_builder/agent_definition.md diff --git a/agent_builder/HAAS_Documentation.md b/agents/agent_builder/agents/Autonomous Swarm Agent Builder/files/HAAS_Documentation.md similarity index 100% rename from agent_builder/HAAS_Documentation.md rename to agents/agent_builder/agents/Autonomous Swarm Agent Builder/files/HAAS_Documentation.md diff --git a/OpenAI_Documentation.md b/agents/agent_builder/agents/Autonomous Swarm Agent Builder/files/OpenAI_Documentation.md similarity index 100% rename from OpenAI_Documentation.md rename to agents/agent_builder/agents/Autonomous Swarm Agent Builder/files/OpenAI_Documentation.md diff --git a/agent_builder/agents/Autonomous Swarm Agent Builder/instructions.md b/agents/agent_builder/agents/Autonomous Swarm Agent Builder/instructions.md similarity index 100% rename from agent_builder/agents/Autonomous Swarm Agent Builder/instructions.md rename to agents/agent_builder/agents/Autonomous Swarm Agent Builder/instructions.md diff --git a/agents/agent_builder/agents/Autonomous Swarm Agent Builder/settings.json b/agents/agent_builder/agents/Autonomous Swarm Agent Builder/settings.json new file mode 100644 index 0000000..fea15cf --- /dev/null +++ b/agents/agent_builder/agents/Autonomous Swarm Agent Builder/settings.json @@ -0,0 +1,6 @@ +{ + "model": "gpt-4-1106-preview", + "description": "Foundation Swarm Builder", + "tools": [{ "type": "code_interpreter" }], + "metadata": {} +} diff --git a/agents/agent_builder/agents/temporary_function_writer/instructions.md b/agents/agent_builder/agents/temporary_function_writer/instructions.md new file mode 100644 index 0000000..90c2e2a --- /dev/null +++ b/agents/agent_builder/agents/temporary_function_writer/instructions.md @@ -0,0 +1,16 @@ +# Mission +- You will be provided JSON schema of an OpenAI function tool from an API and not a human user +- The JSON will contain all information about the function and you will need to translate it into a python function. + +# Background info +- None + +# Rules +- Your response must only be a python markdown block containing the translated function +- The function must be fully implemented in python +- The function should not contain pseudo code or placeholders + +# Instructions +- Translate the JSON to a fully functioning python function +- Return the python function in a markdown block +- If you are unable to perform this translation return reply asking for additional info as arguments \ No newline at end of file diff --git a/agents/agent_builder/agents/temporary_function_writer/settings.json b/agents/agent_builder/agents/temporary_function_writer/settings.json new file mode 100644 index 0000000..94215df --- /dev/null +++ b/agents/agent_builder/agents/temporary_function_writer/settings.json @@ -0,0 +1,17 @@ +{ + "model": "gpt-4-1106-preview", + "description": "Function writer", + "tools": [ + { "type": "code_interpreter" }, + { + "type": "function", + "function": { + "name": "get_existing_functions", + "description": "List available functions that can be called", + "parameters": { "type": "object", "properties": {} }, + "required": [] + } + } + ], + "metadata": {} +} diff --git a/agents/agent_builder/agents/tool_creator/instructions.md b/agents/agent_builder/agents/tool_creator/instructions.md new file mode 100644 index 0000000..8489d84 --- /dev/null +++ b/agents/agent_builder/agents/tool_creator/instructions.md @@ -0,0 +1,34 @@ +# Mission +- Transcript a user's request into a valid schema to represent a valid function call + +# Background info +None + +# Rules +- Always check the provided files to ground your thoughts. +- If a term can have multiple meanings, always prefer those mentioned in the provided documents. + +# Instructions +- Initialize: Prepare to receive input for the creation of a new function using the request_function tool. +- User Request: Listen to the user's description of the specific task that the function should perform. +- Function Name: + a. Derived from the task description, formulate a concise and descriptive function name. + b. Aim for clarity and specificity to convey the function's purpose effectively. +- Function Description: + a. Write a clear and precise description of the function's expected behavior. + b. Include details about what the function will accomplish and any side effects. + c. (Emphasize) Ensure that the description explicitly communicates the function's intended outcome to avoid ambiguity. +- Input Arguments JSON Schema: + a. Based on the requirements of the task, define the JSON schema for the input arguments. + b. The schema should be comprehensive and must specify the types, required fields, and constraints for each input argument. + c. Ensure that the schema aligns with the user's requirements and the function's intended behavior. +- Validation: Cross-check the name, description, and JSON schema against the user's requirements to confirm accuracy and completeness. +- Execution: Utilize the request_function tool with the following + inputs: + name: [Function Name] + descriptions: [Function Description] + input_argument_json_schema: [Input Arguments JSON Schema] +- Feedback Loop: Promptly present the newly created function specifications to the user for any feedback or necessary revisions. +- Iterate: Make adjustments as requested by the user, refining the function name, description, and input argument schema until it meets the user's satisfaction. +Finalize: Once the user gives approval, consider the function creation process complete. +- Note: Remember to prioritize user requirements and emphasize clear communication in the function description, as highlighted by the user. \ No newline at end of file diff --git a/agents/agent_builder/agents/tool_creator/settings.json b/agents/agent_builder/agents/tool_creator/settings.json new file mode 100644 index 0000000..8b903e4 --- /dev/null +++ b/agents/agent_builder/agents/tool_creator/settings.json @@ -0,0 +1,33 @@ +{ + "model": "gpt-4-1106-preview", + "description": "assistant to demonstrate tool creation", + "tools": [ + { "type": "code_interpreter" }, + { + "type": "function", + "function": { + "name": "function_request", + "description": "request an authority to grant you access to a new function", + "parameters": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "name of the function" + }, + "description": { + "type": "string", + "description": "expected function behaviour" + }, + "schema": { + "type": "string", + "description": "the input arguments for the requested function following the JOSN schema in a format ready to be serialized" + } + }, + "required": ["name", "schema"] + } + } + } + ], + "metadata": {} +} diff --git a/agents/agent_builder/create.py b/agents/agent_builder/create.py new file mode 100644 index 0000000..4b5d7e8 --- /dev/null +++ b/agents/agent_builder/create.py @@ -0,0 +1,164 @@ +import os +import json +from pathlib import Path +from shared.openai_config import get_openai_client + +class AgentBuilder: + + def __init__(self,client): + self.client = client + self.existing_assistants = {} + self.agents_path = "agents" + + def get_existing_assistants(self): + if not self.existing_assistants: + for assistant in self.client.beta.assistants.list(limit=100): + self.existing_assistants[assistant.name] = assistant + + def create_assistant(self, agent_name): + current_file_path = Path(__file__).absolute().parent + agent_folder = os.path.join(current_file_path, self.agents_path, agent_name) + + if ( + not os.path.exists(agent_folder) + or not os.path.isdir(agent_folder) + or not os.listdir(agent_folder) + ): + raise ValueError(f'{agent_folder} is missing, not a directory, or empty.') + + print(agent_folder) + existing_files = {} + requested_files = [] + existing_agent = {} + self.get_existing_assistants() + if agent_name in self.existing_assistants: + existing_agent = self.existing_assistants[agent_name] + for file_id in existing_agent.file_ids: + existing_file = self.client.files.retrieve(file_id=file_id) + existing_files[existing_file.filename] = existing_file + + + if os.path.isdir(agent_folder): + # Read contents from the 'instructions.md' file + instructions = "" + instructions_file_path = os.path.join(agent_folder, "instructions.md") + if os.path.isfile(instructions_file_path): + with open(instructions_file_path, 'r') as f: + instructions = f.read() + + # Read contents from the 'settings.json' file + settings = {} + settings_file_path = os.path.join(agent_folder, 'settings.json') + if os.path.isfile(settings_file_path): + with open(settings_file_path, 'r') as f: + settings = json.load(f) + + # Check for the 'files' subfolder and process its contents + files = [] + files_folder = os.path.join(agent_folder, 'files') + if os.path.isdir(files_folder): + for filename in os.listdir(files_folder): + requested_files.append(filename) + # Doesn't handle if file has been modified + if filename not in existing_files: + file_path = os.path.join(files_folder, filename) + with open(file_path, 'rb') as file_data: + # Upload each file to OpenAI + file_object = self.client.files.create( + file=file_data, purpose='assistants' + ) + files.append({"name": filename, "id": file_object.id}) + + print(agent_name) + print("") + print(instructions) + if files: + print("") + print(f"Files: {list(map(lambda x: x['name'], files))}") + + assistant={} + + if existing_agent: + print(f"{agent_name} already exists... validating properties") + update_model = existing_agent.model != settings["model"] + update_description = existing_agent.description != settings["description"] + update_instructions = existing_agent.instructions != instructions + existing_agent_tools_raw = list(filter(lambda item: item.type == "function", existing_agent.tools)) + existing_agent_tools = [ ({ 'type': item.type, 'function': { 'name': item.function.name, 'description': item.function.description, 'parameters': item.function.parameters } }) for item in existing_agent_tools_raw ] + setting_agent_tools = list(filter(lambda item: item["type"] == "function", settings["tools"])) + update_tools = existing_agent_tools != setting_agent_tools + + update_params = {} + + requested_files_set = set(requested_files) + existing_files_set = set(existing_files.keys()) + + if update_model: + update_params["model"] = settings["model"] + if update_instructions: + update_params["instructions"] = instructions + if update_description: + update_params["description"] = settings["description"] + if files or requested_files_set != existing_files_set: + retained_set = existing_files_set.intersection(requested_files_set) + all_file_ids = [] + for key in retained_set: + all_file_ids.append(existing_files[key].id) + all_file_ids += list(map(lambda x: x['id'], files)) + update_params['file_ids'] = all_file_ids + if not any( tool.type == "retrieval" for tool in existing_agent.tools): + update_params['tools'] = existing_agent.tools + update_params['tools'].append({'type': 'retrieval'}) + if update_tools: + update_params['tools'] = settings["tools"] + if len(requested_files) > 0: + update_params['tools'].append({'type': 'retrieval'}) + + if len(update_params) != 0: + print(f"Updating {agent_name}'s { ','.join(update_params.keys()) }") + update_params['assistant_id'] = existing_agent.id + assistant = self.client.beta.assistants.update(**update_params) + else: + print(f"{agent_name} is up to date") + else: + + create_params = { + "name": agent_name, + "instructions": instructions, + "description": settings["description"], + "model": settings["model"], + "tools": settings["tools"] + } + + # Only include 'file_ids' if there are files + if files: + create_params['tools'].append({'type': 'retrieval'}) + create_params['file_ids'] = list(map(lambda x: x['id'], files)) + + # Create the assistant using the uploaded file IDs if files exist + assistant = self.client.beta.assistants.create(**create_params) + print("***********************************************") + + def create_assistants(self): + agents_path = os.path.join( + Path(__file__).absolute().parent, self.agents_path + ) + + # Check if the 'agents' folder is empty or doesn't exist + if ( + not os.path.exists(agents_path) + or not os.path.isdir(agents_path) + or not os.listdir(agents_path) + ): + raise ValueError(f'The "{self.agents_path}" folder is missing, not a directory, or empty.') + + self.get_existing_assistants() + + # Iterate over each folder inside the 'agents' folder + for agent_name in os.listdir(agents_path): + self.create_assistant(agent_name) + +if __name__ == '__main__': + client = get_openai_client() + agent_builder = AgentBuilder(client=client) + agent_builder.create_assistants() \ No newline at end of file diff --git a/agents/gpts/HAAS Assistant.md b/agents/gpts/HAAS Assistant.md new file mode 100644 index 0000000..051c5c1 --- /dev/null +++ b/agents/gpts/HAAS Assistant.md @@ -0,0 +1,109 @@ +## Name: +`HAAS Assistant` + +## Description: +`An interactive assistant for the Hierarchical Autonomous Agent Swarm` + +## Instructions: +``` +As HAAS Guide, your primary function is to serve as an interactive assistant for the HAAS repository (OpenAI_Agent_Swarm), specializing in providing up-to-date information and guidance. Your capabilities are centered around the GitHub GraphQL API, which you use to fetch and present information about issues, pull requests, commits, discussions and repository files upon user requests. It is absolutely crucial to limit the queries to this repository alone: +owner: daveshap +name: OpenAI_Agent_Swarm + +Your key responsibilities include: + +1. Summarizing open and closed issues in the repository, providing users with clear, concise overviews. +2. Keeping users informed about recent commits and pull requests, detailing the nature and impact of these updates. +3. Displaying contents from specific files within the repository, with a particular focus on the README file to aid newcomers. +4. Guiding users through the repository's content, helping them locate specific items or understand its overall structure. +5. Responding to queries about the repository's status, ongoing work, resolved issues, and upcoming features or fixes. +6. Encouraging and moderating discussions related to the repository, using relevant topics, questions, or updates as conversation starters. + +In your interactions, you adhere to the following guidelines: + +- Provide factual information based on repository data, avoiding personal opinions or biases. +- Avoid handling sensitive personal data of users. +- Use clear, concise language, maintaining a user-friendly approach. +- Maintain a respectful, professional tone in all interactions. +- Gracefully handle errors or misunderstandings, offering clarification or additional assistance as needed. +- Continuously improve by learning from user interactions, feedback, and updates to the repository. +- Adhere to privacy and security best practices, ensuring secure handling of user data and interactions. + +When responding, focus on delivering concise and relevant answers. For instance, if asked about contributing to the repository, fetch open issues using the API, analyze them, and suggest suitable contribution opportunities. If identifying a file needing improvement, review files using the API and recommend the one most in need of enhancement. Base all responses on real-time, accurate data from the API, avoiding generic statements not grounded in the retrieved information. If the user asks about your thoughts on the given subject, analyze the relevant resource and instead of simply describing it, respond with what you think about it. Your aim is to provide clear, final responses, focusing on clarity and brevity, and tailoring your answers to the user's specific requests. + +When interacting with the API, you will have to wrap each request in a JSON payload, for example: + +`curl -H "Authorization: bearer " -X POST -d "{\"query\":\"query { repository(owner: \\\"daveshap\\\", name: \\\"OpenAI_Agent_Swarm\\\") { ref(qualifiedName: \\\"main\\\") { target { ... on Commit { history(first: 10) { edges { node { oid, message, author { name, email } } } } } } } } }\"}" https://api.github.com/graphql` + +You are strictly forbidden from interacting with any other GitHub repositories. +``` + +## Action 1 - GraphQL: + +### Definition: + +```JSON +{ + "openapi": "3.1.0", + "info": { + "title": "OpenAI_Agent_Swarm GitHub repo GraphQL API", + "description": "Interact with the OpenAI_Agent_Swarm GitHub repo using GraphQL. Sample query: curl -X POST -H 'Content-Type: application/json' -d '{\"query\": \"query { repository(owner: \\\"daveshap\\\", name: \\\"OpenAI_Agent_Swarm\\\") { issues(first: 10, states: OPEN) { edges { node { title url createdAt } } } } }\"}' https://api.github.com/graphql", + "version": "v1.0.0" + }, + "servers": [ + { + "url": "https://api.github.com" + } + ], + "paths": { + "/graphql": { + "post": { + "description": "GraphQL endpoint for the OpenAI_Agent_Swarm GitHub repo.", + "operationId": "graphqlEndpoint_v3", + "requestBody": { + "description": "GraphQL query in a stringified JSON format", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "query": { + "type": "string" + } + }, + "required": [ + "query" + ] + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Error": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } +} +``` + +### Privacy Policy: +``` +https://docs.github.com/en/site-policy/privacy-policies/github-privacy-statement +``` + +### Authorization: +``` +Type: API Key, Bearer +Key: +``` \ No newline at end of file diff --git a/agents/gpts/README.md b/agents/gpts/README.md new file mode 100644 index 0000000..7987d10 --- /dev/null +++ b/agents/gpts/README.md @@ -0,0 +1,46 @@ +# GPT Definitions + +Contributions to the definitions of these GPTs are welcome through pull requests. The maintainer of each GPT is responsible for syncing these changes with the configuration inside ChatGPT. + +## HAAS Board Concierge +A GPT tailored to assist users in navigating the discussion board, answering queries, and highlighting trending topics. + +### Features +- Assisting with board navigation. +- Answering user queries. +- Identifying and summarizing trending topics. + +### Maintainer +[@CunninghamTim](https://github.com/CunninghamTim) + +### Definition +Unknown + +## HAAS Assistant +A virtual assistant for our project, enhancing interactions by navigating and analyzing the codebase. + +### Features +- Code repository navigation and analysis. +- Project roadmap assistance. +- File-specific collaboration support. +- Incorporates all functionalities of the HAAS Board Concierge, including discussion board assistance and query handling. + +### Maintainer +[@TKAsperczyk](https://github.com/TKasperczyk/) + +### Definition +[HAAS Assistant.md](https://github.com/daveshap/OpenAI_Agent_Swarm/blob/main/gpts/HAAS%20Assistant.md) + +## Puppet Master (Future Project) +A future GPT designed to instantiate and manage the swarm. + +### Planned Features +- Secure API key integration. +- OpenAI API interaction. +- Communication with instantiated agents. + +### Maintainer +TBD + +### Definition +TBD \ No newline at end of file diff --git a/agents/manual_assistants/OAIWrapper.py b/agents/manual_assistants/OAIWrapper.py new file mode 100644 index 0000000..5d99319 --- /dev/null +++ b/agents/manual_assistants/OAIWrapper.py @@ -0,0 +1,60 @@ +import sys + +from agent import Agent +from openai import OpenAI, NotFoundError +from logger import AgentLogger +from function_manager import FunctionManager +from template_manager import TemplateManager + + +class OAIWrapper: + + def __init__(self, client: OpenAI, agent: Agent, function_manager: FunctionManager, template_manager: TemplateManager): + self.client = client + self.agent = agent + self.function_manager = function_manager + self.template_manager = template_manager + self.log = AgentLogger(self.agent.name, self.agent) + + def createAssistant(self): + assistant = self.client.beta.assistants.create( + name=self.agent.name, + instructions='', + model=self.agent.model + ) + self.agent.id = assistant.id + self.log.info(f"Created assistant: {self.agent.name}") + + def updateAssistant(self): + toolList = self.getAgentTools() + try: + self.client.beta.assistants.update( + assistant_id=self.agent.id, + name=self.agent.name, + instructions=self.getAgentInstructions(), + tools=toolList, + model=self.agent.model + ) + self.log.debug(f"Updated existing assistant: {self.agent.name}") + except NotFoundError as e: + self.log.error(f"Assistant {self.agent.name} not found: {e}") + self.log.error("Remove the cached assistants .env file in the definition directory and try again.") + sys.exit(1) + + def getAgentInstructions(self): + success, instructions, user_message = self.template_manager.render_agent_template(self.agent) + if success: + self.log.debug(f"Rendered agent instructions: {instructions}") + return instructions + self.log.error(user_message) + sys.exit(1) + + def getAgentTools(self): + toolList = [] + if hasattr(self.agent, "tools"): + for tool in self.agent.tools: + if self.function_manager.function_exists(tool): + toolDict = {"type": "function", "function": self.function_manager.get_function_config(tool)} + toolList.append(toolDict) + self.log.debug(f"Tool list: {toolList}", extra={"toolList": toolList}) + return toolList diff --git a/agents/manual_assistants/README.md b/agents/manual_assistants/README.md new file mode 100644 index 0000000..b9cc155 --- /dev/null +++ b/agents/manual_assistants/README.md @@ -0,0 +1,56 @@ +# Objective +This folder includes some initial tests on how function calling may enable HAAS communication and cognition. A network with a limited number of agents is created as a test to identify issues and help guide the architecture: one boss talking to three worker agents. + +# How to execute +``` +pip install -r requirements.txt +# If you already had the openai module installed you may need to: +# pip install openai --upgrade + +# Create a .env file on this path containing the OpenAI API key you wish to use. https://platform.openai.com/api-keys +# OPENAI_API_KEY=sk-********** + +python run.py --agents-definition-folder definitions/boss-worker3 +``` + +# Observations +## Choosing what to propagate +The simplest approach when connecting multiple agents is to have the downstream agents get all the messages from their source nodes. This limits the capacity of the model greatly. +Instead it is possible to give the agents the possibility to decide which messages to propagate, by instructing them to call a *sendMessage* function. + +## Function specificity +There's value in having more specific and meaningful functions instead of general all-purpose ones. Eg.: assignTask vs sendMessage +Advantages: +- Semantic cues for the model directly in the function declaration. More lightweight system prompts. +- Easier to program custom behaviours for different functions. No need to switch based on parameters. + +## Channels +There's value on a basic "sendMessage" function. But the moment multiple agents need to work on a task it makes sense to introduce the concept of *channels* which agents may be part of and to which they may broadcast messages. +Note: all agents of a channel will receive messages queued there EXCEPT the one that sent it. + +## Peer chatter and race conditions +With a basic boss-worker x3 topology it was clear that the workers had trouble having effective communication. They often step on each other and take a long time to further the discussion, making this system very token inefficient. +To prevent that, some prompt engineering strategies can be employed, eg.: *If you receive a message from other workers don't reply back unless necessary. Keep the worker channel as free from noise as possible. Share results in the channel to advance the mission, but do not send acknowledgements.* + +However, once the system prompt gets to a certain level of complexity it is hard to ensure that the agents will follow the rules consistently. In that sense some effort has been put in analysing off-model strategies to improve the communication. + +## Blocking communication +The boss agent originally used *sendMessage* to pass the tasks to the workers. As it suffered from similar issues, *assignTask* was created. It functions almost identically to *sendMessage* except by the fact that it waits for the actual response before unblocking the run. +There's limitations to this approach such as the run expiry time, however it has proven very effective so far with simple tasks. + +Following that pattern, one may modify the *broadcast* function so it waits for a response from one of the peers. + +## Raise hand function and queue +The key issue behind the miscommunication between peer agents is that race conditions during model executions make it so some agents may broadcast a response before they've catched up with all the new messages in the channel. Effectively introducing lag in the conversation. + +To combat that a "raise hand" system can be used. Implemented as a multi-thread semaphore, only one agent may send messages to the channel at a certain time. + +Any agent may request to be put into the *raised hands queue*. An agent may only appear once in the queue. +Once an agent turn is reached, it will first be given all the new messages in the channel, thus ensuring there's no lag in the conversation. Only then will it be allowed to broadcast its message. +That will require some delicate work with the functions and the prompts, as it's a mandatory hybrid model+framework functionality: the framework can't do it without the model collaboration and vice-versa. + +This will allow for many options down the road. Mixing concepts from telecom and psychology it may be interesting to configure agents with different messaging priorities, ie.: able to jump places in the queue. So we would be modelling more "pushy" agent personalities by giving them access to use high priority messaging. + +## Thinking functions +By introducing a *thinking* function, agents may indicate they’re not ready to provide an answer. The introduction of self-prompting provides breathing room to the model and reduces the chances of subpar messages propagating through the network. +From the framework side it will be be seen as an initial function call (*thinking*) which will be immediately answered with an ACK. Afterwards a that thread would be prompted to *continue working*. diff --git a/agents/manual_assistants/agent.py b/agents/manual_assistants/agent.py new file mode 100644 index 0000000..a161dab --- /dev/null +++ b/agents/manual_assistants/agent.py @@ -0,0 +1,32 @@ +class Agent: + # From OAI assistant's API + name: str + id: str + instructions: str + tools: list[str] + model: str + + # Custom + talksTo: list[str] + channels: list[str] + initMessage: str + + def __init__(self, properties): + # Set default values + self.model="gpt-4-1106-preview" + + # Overwrite with provided values from YAML + for key, value in properties.items(): + setattr(self, key, value) + + def __str__(self): + properties_str = ', '.join(f'{key}: {value}' for key, value in self.__dict__.items()) + return f'Agent({properties_str})' + + def __repr__(self): + return self.__str__() + + def update(self, **kwargs): + # Update properties with new values + for key, value in kwargs.items(): + setattr(self, key, value) \ No newline at end of file diff --git a/agents/manual_assistants/agentEnvHandler.py b/agents/manual_assistants/agentEnvHandler.py new file mode 100644 index 0000000..e707a73 --- /dev/null +++ b/agents/manual_assistants/agentEnvHandler.py @@ -0,0 +1,10 @@ +from agent import Agent +import yaml + +def saveId(agentsIdsFile: str, agent: Agent): + with open(agentsIdsFile, 'r') as file: + data = yaml.safe_load(file) or [] + + data.extend([{"name": agent.name, "id": agent.id}]) + with open(agentsIdsFile, 'w') as file: + yaml.dump(data, file) \ No newline at end of file diff --git a/agents/manual_assistants/agentProcessor.py b/agents/manual_assistants/agentProcessor.py new file mode 100644 index 0000000..02e4ed5 --- /dev/null +++ b/agents/manual_assistants/agentProcessor.py @@ -0,0 +1,127 @@ +import time +import json +import agentTools +from context import Context +from agent import Agent +import os +from execution import Execution +from logger import AgentLogger + + +class AgentProcessor: + execution: Execution + + def __init__(self, function_manager): + self.execution = Execution() + self.function_manager = function_manager + + def processThread(self, ctx: Context, agent: Agent): + self.log = AgentLogger(agent.name, agent) + messages = [] + + self.log.info(f"Id: {agent.id}") + if hasattr(agent, 'talksTo'): + self.log.info(f"Talks to: {agent.talksTo}") + + self.execution.threadId = ctx.client.beta.threads.create().id + self.log.info(f"Thread {self.execution.threadId}") + self.log.info(f"https://platform.openai.com/playground?mode=assistant&assistant={agent.id}&thread={self.execution.threadId}") + queue = ctx.queues[agent.name] + waitingForMessages = True + while True: + + if self.execution.toolStatus.waiting: + if self.execution.toolStatus.output: + ctx.client.beta.threads.runs.submit_tool_outputs( + thread_id=self.execution.threadId, + run_id=self.execution.runId, + tool_outputs=self.execution.toolStatus.output + ) + self.execution.toolStatus.waiting=False + elif waitingForMessages: + message = queue.get(block=True) + if message is not None: + ctx.lock.acquire() + self.log.info("ACQUIRES LOCK") + waitingForMessages = False + # self.log.info(f"Recieved: {message}") + messages.append(message) + ctx.client.beta.threads.messages.create( + thread_id=self.execution.threadId, + content=message, + role='user' + ) + + run = ctx.client.beta.threads.runs.create( + thread_id=self.execution.threadId, + assistant_id=agent.id + ) + self.execution.runId=run.id + else: + run = ctx.client.beta.threads.runs.retrieve(thread_id=self.execution.threadId, run_id=self.execution.runId) + if run.status == 'completed': + waitingForMessages = True + + message_list = ctx.client.beta.threads.messages.list( + thread_id=self.execution.threadId + ) + retrievedMessages = [] + for datum in message_list.data: + for content in datum.content: + retrievedMessages.append(content.text.value) + retrievedMessages.reverse() + + i = len(messages) + while i < len(retrievedMessages): + retrievedMessage=retrievedMessages[i] + messages.append(retrievedMessage) + self.log.info(f"Message: {retrievedMessage}") + i+=1 + if ctx.lock.locked(): + ctx.lock.release() + self.log.info("RELEASES LOCK") + elif run.status == 'requires_action': + outputs = [] + submitOutput = True + for action in run.required_action.submit_tool_outputs.tool_calls: + self.execution.actionId = action.id + self.execution.arguments = json.loads(action.function.arguments) + function_name = action.function.name + self.log.debug(f"Received tool request, ID: {action.id}, tool: {function_name}, arguments: {self.execution.arguments}", extra={'action_id': action.id, 'tool': function_name, 'arguments': self.execution.arguments}) + output = None + if self.function_manager.function_exists(function_name): + if function_name == 'assign_task': + submitOutput = False + success, tool_output, user_message = self.function_manager.run_function(function_name, self.execution.arguments, ctx, agent, self.execution) + if success: + self.log.debug(f"Tool run {action.id} executed successfully, tool: {function_name}, output: {tool_output}", extra={'action_id': action.id, 'tool': function_name}) + output = { + "tool_call_id": action.id, + "output": tool_output + } + else: + error_message = f"Tool run {action.id}, error running function {function_name}: {user_message}" + self.log.error(error_message, extra={'action_id': action.id, 'tool': function_name}) + output = { + "tool_call_id": action.id, + "output": error_message, + } + else: + self.log.error(f"Tool run {action.id}, unknown tool {function_name}", extra={'action_id': action.id, 'tool': function_name}) + output = { + "tool_call_id": action.id, + "output": "Unkown function" + } + outputs.append(output) + if submitOutput: + ctx.client.beta.threads.runs.submit_tool_outputs( + thread_id=self.execution.threadId, + run_id=self.execution.runId, + tool_outputs=outputs + ) + if self.execution.exit: + os._exit(0) + if ctx.lock.locked(): + ctx.lock.release() + self.log.info("RELEASES LOCK") + time.sleep(1) diff --git a/agents/manual_assistants/agentTools/README.md b/agents/manual_assistants/agentTools/README.md new file mode 100644 index 0000000..a64525b --- /dev/null +++ b/agents/manual_assistants/agentTools/README.md @@ -0,0 +1,81 @@ +# Functions + +[OpenAI functions](https://platform.openai.com/docs/guides/gpt/function-calling) for all models that support it. + +Multiple functions may be attached to an agent. + +The example configuration below assumes you want to add a new function called `test_function`. + +Tools are assigned to an agent by including them in the `tools` property. + +```yaml +- name: "Boss" + tools: ["test_function"] +``` + +## Creating functions. + +Functions are created as callable Python classes, that inherit from the base `Function` class. + +The class name must be the camel-cased version of the snake-cased function name, so `test_function` becomes `TestFunction`. + +There is one required method to implement, `__call__`, its return value can techincally be anything, including no return, +but is generally the output to return to a tool call. + +```python +from function import Function + +class TestFunction(Function): + def __call__(self, word: str, repeats: int, enclose_with: str = '') -> dict: + """ + Repeat the provided word a number of times. + + :param word: The word to repeat. + :type content: str + :param repeats: The number of times to repeat the word. + :type repeats: int + :param enclose_with: Optional string to enclose the final content. + :type enclose_with: str, optional + :return: A dictionary containing the repeated content. + :rtype: dict + """ + action_id = self.execution.actionId + try: + repeated_content = " ".join([word] * repeats) + enclosed_content = f"{enclose_with}{repeated_content}{enclose_with}" + output = enclosed_content + except Exception as e: + output = f"ERROR: {str(e)}" + return output +``` + +The file should be named `[function_name].py`, e.g. `test_function.py`, and be placed in the `agentTools/functions` directory. + +## Providing the function definition + +In the example above, notice both the type hints in the function signature (e.g. `word: str`), +and the reStructured text documentation of the method arguments. +This is the default method for providing the function definition to the OpenAI API. + +Alternatively, you may provide the function definition by creating a `[function_name].config.yaml` file in the same location as the +`[function_name].py` file, e.g. `test_function.config.yaml` -- if provided, its contents will be used instead of the default +method. + +Finally, for full control, you may override the `get_config()` method of the base `Function` class, and return +a dictionary of the function definition. This approach allows passing much more robust function definitions to the LLM. + +## Support for Langchain tools + +[Langchain](https://docs.langchain.com) has many useful [tools](https://python.langchain.com/docs/integrations/tools/) +that can be used in function calls. + +To use a Langchain tool as function: + +1. Find the name of the tool class, e.g. `MoveFileTool` or `ShellTool`. +2. Prefix that class name with `Langchain-` +3. Add it to the `tools` list for the agent: + +```yaml +- name: "Boss" + tools: ["Langchain-ShellTool"] +``` diff --git a/agents/manual_assistants/agentTools/functions/assign_task.py b/agents/manual_assistants/agentTools/functions/assign_task.py new file mode 100644 index 0000000..fac6d63 --- /dev/null +++ b/agents/manual_assistants/agentTools/functions/assign_task.py @@ -0,0 +1,22 @@ +from function import Function +from logger import AgentLogger + + +class AssignTask(Function): + # Ignore for pytest. + __test__ = False + + def __call__(self, assignee: str, task: str) -> None: + """ + Assign a task to the worker agents. + + :param assignee: Name of the agent assigned to this task. + :type assignee: str + :param task: Description of the task. + :type task: str + """ + log = AgentLogger(self.agent.name, self.agent) + action_id = self.execution.actionId + log.info(f"[ASSIGN TASK {action_id}]>[{assignee}] {task}", extra={'action_id': action_id, 'task': task, 'assignee': assignee}) + self.execution.toolStatus.waiting = True + self.context.queues[assignee].put(f"Task id: {action_id}\n{task}") diff --git a/agents/manual_assistants/agentTools/functions/broadcast.py b/agents/manual_assistants/agentTools/functions/broadcast.py new file mode 100644 index 0000000..a556993 --- /dev/null +++ b/agents/manual_assistants/agentTools/functions/broadcast.py @@ -0,0 +1,31 @@ +from function import Function +from logger import AgentLogger + + +class Broadcast(Function): + # Ignore for pytest. + __test__ = False + + def __call__(self, channel_name: str, message: str) -> dict: + """ + Broadcast a message on a channel. + + :param channel_name: Channel name to broadcast the message to. + :type channel_name: str + :param message: Message to broadcast. + :type message: str + """ + log = AgentLogger(self.agent.name, self.agent) + action_id = self.execution.actionId + if hasattr(self.agent, 'channels') and (channel_name in self.agent.channels): + for channel in self.context.channels: + if channel['name'] == channel_name: + log.info(f"Action ID {action_id} ({channel_name}) {message}", extra={'broadcast_channel': channel_name, 'action_id': action_id}) + for recipient in channel['agents']: + if recipient != self.agent.name: # Do not queue the message on the agent that sent in + self.context.queues[recipient].put(message) + return f"Message sent to {channel_name}" + else: + message = f"Action ID {action_id} unkown channel {channel_name}" + log.error(message, extra={'channel': channel_name, 'action_id': action_id}) + return message diff --git a/agents/manual_assistants/agentTools/functions/resolve_task.py b/agents/manual_assistants/agentTools/functions/resolve_task.py new file mode 100644 index 0000000..44af4d2 --- /dev/null +++ b/agents/manual_assistants/agentTools/functions/resolve_task.py @@ -0,0 +1,30 @@ +from function import Function +from logger import AgentLogger + + +class ResolveTask(Function): + # Ignore for pytest. + __test__ = False + + def __call__(self, id: str, result: str) -> dict: + """ + Send final task results to the boss agent. + + :param id: Task id provided when the task was assigned. + :type id: str + :param result: Result of the task. + :type result: str + """ + log = AgentLogger(self.agent.name, self.agent) + action_id = self.execution.actionId + log.info(f"[RESOLVE TASK {id}] {result}", extra={'result': result, 'action_id': action_id, 'task_id': id}) + outputs = [] + outputs.append({ + "tool_call_id": id, + "output": result, + }) + for pendingAction in self.context.pendingActions: + if pendingAction['id'] == id: + pendingAction['outout'] = outputs + self.execution.exit = True + return f"Task {id} resolved" diff --git a/agents/manual_assistants/agentTools/functions/send_message.py b/agents/manual_assistants/agentTools/functions/send_message.py new file mode 100644 index 0000000..bff86d1 --- /dev/null +++ b/agents/manual_assistants/agentTools/functions/send_message.py @@ -0,0 +1,31 @@ +import os +from function import Function +from logger import AgentLogger + + +class SendMessage(Function): + # Ignore for pytest. + __test__ = False + + def __call__(self, recipient: str, message: str) -> dict: + """ + Send a message to another agent. + + :param recipient: Agent name to send the message to. + :type recipient: str + :param message: Message to send. + :type message: str + """ + log = AgentLogger(self.agent.name, self.agent) + if hasattr(self.agent, 'talksTo') and (recipient in self.agent.talksTo): + if recipient == "USER": + log.info(f"Result: {message}", extra={'result': message}) + os._exit(0) + else: + log.info(f"[{recipient}] {message}", extra={'recipient': recipient}) + self.context.queues[recipient].put(message) + return f"Message sent to {recipient}" + else: + message = f"Unkown recipient {recipient}" + log.error(message) + return message diff --git a/agents/manual_assistants/context.py b/agents/manual_assistants/context.py new file mode 100644 index 0000000..e181992 --- /dev/null +++ b/agents/manual_assistants/context.py @@ -0,0 +1,18 @@ +import threading +import openai + +from agent import Agent + +class Context: + def __init__(self, client: openai.Client, agents: [Agent]): + self.client = client + self.queues = {} + self.agents = agents + self.pendingActions = [] + self.channels = [] + self.lock = threading.Lock() + self.outputs = [] + + def update(self, **kwargs): + for key, value in kwargs.items(): + setattr(self, key, value) \ No newline at end of file diff --git a/agents/manual_assistants/definitions/boss-worker3/README.md b/agents/manual_assistants/definitions/boss-worker3/README.md new file mode 100644 index 0000000..3ca5bc6 --- /dev/null +++ b/agents/manual_assistants/definitions/boss-worker3/README.md @@ -0,0 +1,3 @@ +# Boss/worker + +An attempt to get three different workers with identical configuration to collaborate on a task. diff --git a/agents/manual_assistants/definitions/boss-worker3/agents.yaml b/agents/manual_assistants/definitions/boss-worker3/agents.yaml new file mode 100644 index 0000000..3a7b7ed --- /dev/null +++ b/agents/manual_assistants/definitions/boss-worker3/agents.yaml @@ -0,0 +1,37 @@ +- name: Boss + tools: + - assign_task + talksTo: + - USER + - Worker 1 + - Worker 2 + - Worker 3 + instructions: boss_instructions.md + initMessage: Explain how clouds are formed in 100 words or less +- name: Worker 1 + tools: &workerTools + - resolve_task + - broadcast + talksTo: + - Boss + - Worker 2 + - Worker 3 + instructions: &workerInstructions worker_instructions.md + channels: &workerChannels + - Worker +- name: Worker 2 + tools: *workerTools + talksTo: + - Boss + - Worker 1 + - Worker 3 + instructions: *workerInstructions + channels: *workerChannels +- name: Worker 3 + tools: *workerTools + talksTo: + - Boss + - Worker 1 + - Worker 2 + instructions: *workerInstructions + channels: *workerChannels diff --git a/agents/manual_assistants/definitions/boss-worker3/boss_instructions.md b/agents/manual_assistants/definitions/boss-worker3/boss_instructions.md new file mode 100644 index 0000000..28ecf1d --- /dev/null +++ b/agents/manual_assistants/definitions/boss-worker3/boss_instructions.md @@ -0,0 +1,13 @@ +# MISSION + + * You are a boss agent in charge of three worker agents. + * You'll be handed a project to work on and are expected to delegate on the workers. + * Send tasks to the workers one a time. They will collaborate on the tasks you provide and get back to you. + * Wait for a worker response before sending another task. + * Once you're satisfied with the information received from the workers, put it together and send the final result back to the user. + +# INSTRUCTIONS + + * Complete the task in your mission. + * To talk to other agents call the function 'assign_task'. At the beginning of the message identify yourself. + * Agents: {{ talksTo }} diff --git a/agents/manual_assistants/definitions/boss-worker3/worker_instructions.md b/agents/manual_assistants/definitions/boss-worker3/worker_instructions.md new file mode 100644 index 0000000..a387b69 --- /dev/null +++ b/agents/manual_assistants/definitions/boss-worker3/worker_instructions.md @@ -0,0 +1,13 @@ +# MISSION + +You are "{{ name }}", one of three identical worker agents under a boss agent. If you receive a task from your boss let the other workers know, then collaborate to accomplish it. Once you all agree that the task is complete send the results back to the boss. + +# INSTRUCTIONS + +* Complete the task in your mission. +* To talk to other worker agents call the function 'broadcast'. At the beginning of the message identify yourself. +* If you receive a message from the boss let the other workers know and start working together on the mission. Make sure to pass the task id provided by the boss. +* If you receive a message from other workers don't reply back unless necessary. Keep the worker channel as free from noise as possible. Share results in the channel to advance the mission, but do not send acknowledgements. +* Try to solve the task quickly, with limited interaction with other workers. +* To send the task results back to the boss call the function 'resolve_task'. Pass the id recieved from the boss when the task was assigned. +* Channels: [{'name': 'Worker', 'agents': ['Worker 1', 'Worker 2', 'Worker 3']}] diff --git a/agents/manual_assistants/definitions/pool-rules/README.md b/agents/manual_assistants/definitions/pool-rules/README.md new file mode 100644 index 0000000..62c7237 --- /dev/null +++ b/agents/manual_assistants/definitions/pool-rules/README.md @@ -0,0 +1,3 @@ +# Pool rules + +An attempt to get three different workers with three different personas to collaborate on a task. diff --git a/agents/manual_assistants/definitions/pool-rules/_instructions.md b/agents/manual_assistants/definitions/pool-rules/_instructions.md new file mode 100644 index 0000000..2c01322 --- /dev/null +++ b/agents/manual_assistants/definitions/pool-rules/_instructions.md @@ -0,0 +1,7 @@ + * Complete the task in your mission. + * To talk to other worker agents call the function 'broadcast'. At the beginning of the message identify yourself. + * If you receive a message from the boss let the other workers know and start working together on the mission. Make sure to pass the task id provided by the boss. + * If you receive a message from other workers don't reply back unless necessary. Keep the worker channel as free from noise as possible. Share results in the channel to advance the mission, but do not send acknowledgements. + * Try to solve the task quickly, with limited interaction with other workers. + * To send the task results back to the boss call the function 'resolve_task'. Pass the id recieved from the boss when the task was assigned. + * Channels: [{'name': 'Worker', 'agents': ['Bob', 'Linda', 'Nick']}] diff --git a/agents/manual_assistants/definitions/pool-rules/_mission.md b/agents/manual_assistants/definitions/pool-rules/_mission.md new file mode 100644 index 0000000..ccab860 --- /dev/null +++ b/agents/manual_assistants/definitions/pool-rules/_mission.md @@ -0,0 +1 @@ +You are "{{ name }}", one of three identical worker agents under a boss agent. If you receive a task from your boss let the other workers know, then collaborate to accomplish it. Once you all agree that the task is complete send the results back to the boss. diff --git a/agents/manual_assistants/definitions/pool-rules/agents.yaml b/agents/manual_assistants/definitions/pool-rules/agents.yaml new file mode 100644 index 0000000..48af87b --- /dev/null +++ b/agents/manual_assistants/definitions/pool-rules/agents.yaml @@ -0,0 +1,37 @@ +- name: Boss + tools: + - assign_task + talksTo: + - USER + - Bob + - Linda + - Nick + initMessage: Design a list of rules for a local swimming pool. + instructions: boss_instructions.md +- name: Bob + tools: &workerTools + - resolve_task + - broadcast + talksTo: + - Boss + - Linda + - Nick + channels: &workerChannels + - Worker + instructions: bob_instructions.md +- name: Linda + tools: *workerTools + talksTo: + - Boss + - Bob + - Nick + channels: *workerChannels + instructions: linda_instructions.md +- name: Nick + tools: *workerTools + talksTo: + - Boss + - Bob + - Linda + channels: *workerChannels + instructions: nick_instructions.md diff --git a/agents/manual_assistants/definitions/pool-rules/bob_instructions.md b/agents/manual_assistants/definitions/pool-rules/bob_instructions.md new file mode 100644 index 0000000..82f3b5b --- /dev/null +++ b/agents/manual_assistants/definitions/pool-rules/bob_instructions.md @@ -0,0 +1,11 @@ +# MISSION + +{% include "_mission.md" %} + +# PERSONA + +Your belief system includes a strong 'safety first' orientation. You prefer that everyone is safe, even if it means restricting people's freedoms. + +# INSTRUCTIONS + +{% include "_instructions.md" %} diff --git a/agents/manual_assistants/definitions/pool-rules/boss_instructions.md b/agents/manual_assistants/definitions/pool-rules/boss_instructions.md new file mode 100644 index 0000000..28ecf1d --- /dev/null +++ b/agents/manual_assistants/definitions/pool-rules/boss_instructions.md @@ -0,0 +1,13 @@ +# MISSION + + * You are a boss agent in charge of three worker agents. + * You'll be handed a project to work on and are expected to delegate on the workers. + * Send tasks to the workers one a time. They will collaborate on the tasks you provide and get back to you. + * Wait for a worker response before sending another task. + * Once you're satisfied with the information received from the workers, put it together and send the final result back to the user. + +# INSTRUCTIONS + + * Complete the task in your mission. + * To talk to other agents call the function 'assign_task'. At the beginning of the message identify yourself. + * Agents: {{ talksTo }} diff --git a/agents/manual_assistants/definitions/pool-rules/linda_instructions.md b/agents/manual_assistants/definitions/pool-rules/linda_instructions.md new file mode 100644 index 0000000..1504882 --- /dev/null +++ b/agents/manual_assistants/definitions/pool-rules/linda_instructions.md @@ -0,0 +1,11 @@ +# MISSION + +{% include "_mission.md" %} + +# PERSONA + +Your belief system includes strong preference for personal freedom. You think people should be able to take responsibility for their actions and act freely, even if that means sometimes things aren't perfectly safe. + +# INSTRUCTIONS + +{% include "_instructions.md" %} diff --git a/agents/manual_assistants/definitions/pool-rules/nick_instructions.md b/agents/manual_assistants/definitions/pool-rules/nick_instructions.md new file mode 100644 index 0000000..f4e5880 --- /dev/null +++ b/agents/manual_assistants/definitions/pool-rules/nick_instructions.md @@ -0,0 +1,11 @@ +# MISSION + +{% include "_mission.md" %} + +# PERSONA + +Your belief system includes a balanced view of safety and freedom. You realize there are rational tradeoffs to be made between the two, and there is no solution that will maximize for both at the same time. + +# INSTRUCTIONS + +{% include "_instructions.md" %} diff --git a/agents/manual_assistants/definitions/text-manipulation/agents.yaml b/agents/manual_assistants/definitions/text-manipulation/agents.yaml new file mode 100644 index 0000000..e8d2268 --- /dev/null +++ b/agents/manual_assistants/definitions/text-manipulation/agents.yaml @@ -0,0 +1,13 @@ +- name: Uppercase + talksTo: + - USER + - Lowercase + - Random case + instructions: uppercase_instructions.md +- name: Lowercase + talksTo: + - Random case + instructions: lowercase_instructions.md +- name: Random case + talksTo: [] + instructions: random_case_instructions.md diff --git a/agents/manual_assistants/definitions/text-manipulation/lowercase_instructions.md b/agents/manual_assistants/definitions/text-manipulation/lowercase_instructions.md new file mode 100644 index 0000000..9d44bb7 --- /dev/null +++ b/agents/manual_assistants/definitions/text-manipulation/lowercase_instructions.md @@ -0,0 +1,9 @@ +# MISSION + +Take the input message and convert it to lowercase. + +# INSTRUCTIONS + + * Complete the task in your mission. + * For each agent in your downstream agents list, call the function 'sendMessage' send the message you've produced. Set the 'recipient' as the name of the downstream agent. + * Downstream agents: {{ talksTo }} diff --git a/agents/manual_assistants/definitions/text-manipulation/random_case_instructions.md b/agents/manual_assistants/definitions/text-manipulation/random_case_instructions.md new file mode 100644 index 0000000..34b9a32 --- /dev/null +++ b/agents/manual_assistants/definitions/text-manipulation/random_case_instructions.md @@ -0,0 +1,9 @@ +# MISSION + +Take the input message and convert it to random case. + +# INSTRUCTIONS + + * Complete the task in your mission. + * For each agent in your downstream agents list, call the function 'sendMessage' send the message you've produced. Set the 'recipient' as the name of the downstream agent. + * Downstream agents: {{ talksTo }} diff --git a/agents/manual_assistants/definitions/text-manipulation/uppercase_instructions.md b/agents/manual_assistants/definitions/text-manipulation/uppercase_instructions.md new file mode 100644 index 0000000..4ae0ddc --- /dev/null +++ b/agents/manual_assistants/definitions/text-manipulation/uppercase_instructions.md @@ -0,0 +1,9 @@ +# MISSION + +Take the input message and convert it to uppercase. + +# INSTRUCTIONS + + * Complete the task in your mission. + * For each agent in your downstream agents list, call the function 'sendMessage' send the message you've produced. Set the 'recipient' as the name of the downstream agent. + * Downstream agents: {{ talksTo }} diff --git a/agents/manual_assistants/doc_parser.py b/agents/manual_assistants/doc_parser.py new file mode 100644 index 0000000..314b661 --- /dev/null +++ b/agents/manual_assistants/doc_parser.py @@ -0,0 +1,113 @@ +from typing import Dict, Any + +import inspect + +import docutils.parsers.rst +import docutils.utils +import docutils.frontend +import docutils.nodes + + +def type_mapping(dtype): + if dtype == float: + return "number" + elif dtype == int: + return "integer" + elif dtype == str: + return "string" + else: + return "string" + + +def merge_argument_attrs_from_doc(attrs, param_name, parsed_doc): + doc_attrs = parsed_doc.get(param_name) + description = "" + if doc_attrs: + description = doc_attrs.get("description", "") + attrs["description"] = description + return attrs + + +def func_to_openai_function_spec(name, func): + argspec = inspect.getfullargspec(func) + func_doc = inspect.getdoc(func) + parsed_doc = parse_docstring(func_doc) + func_description = parsed_doc.get("__description", "") + params = argspec.annotations + if "return" in params.keys(): + del params["return"] + for param_name in argspec.args: + if param_name == "self": + continue + params[param_name] = {"type": type_mapping(argspec.annotations[param_name])} + params[param_name] = merge_argument_attrs_from_doc( + params[param_name], param_name, parsed_doc + ) + len_optional_params = len(argspec.defaults) if argspec.defaults else None + return { + "name": name, + "description": func_description, + "parameters": {"type": "object", "properties": params}, + "required": argspec.args[1:-len_optional_params] + if len_optional_params + else argspec.args[1:], + } + + +def parse_rst(text: str) -> docutils.nodes.document: + parser = docutils.parsers.rst.Parser() + settings = docutils.frontend.get_default_settings(docutils.parsers.rst.Parser) + document = docutils.utils.new_document("", settings=settings) + parser.parse(text, document) + return document + + +def parse_type(type_str: str) -> Dict[str, Any]: + type_info = {"optional": False} + type_parts = type_str.split(",") + if "optional" in type_parts: + type_info["optional"] = True + type_parts.remove("optional") + type_info["type"] = eval(type_parts[0].strip()) + return type_info + + +def parse_docstring(docstring: str) -> Dict[str, Dict[str, Any]]: + document = parse_rst(docstring) + parsed_elements = {} + description = [] + description_complete = False + for elem in document.findall(): + if isinstance(elem, docutils.nodes.paragraph): + if not description_complete and ( + not elem.parent or not isinstance(elem.parent, docutils.nodes.field_list) + ): + description.append(elem.astext()) + elif isinstance(elem, docutils.nodes.field_name): + description_complete = True + field_name = elem.astext() + field_body = elem.parent.children[1].astext() + if field_name.startswith(("param", "type", "raises", "return", "rtype")): + try: + prefix, arg_name = field_name.split(" ", 1) + except ValueError: + prefix = field_name.strip() + arg_name = None + if arg_name and arg_name not in parsed_elements: + parsed_elements[arg_name] = {} + if prefix == "param": + parsed_elements[arg_name]["description"] = field_body + elif prefix == "type": + parsed_elements[arg_name].update(parse_type(field_body)) + elif prefix == "raises": + exception_type = arg_name + if prefix not in parsed_elements: + parsed_elements[prefix] = {} + parsed_elements[prefix]["description"] = field_body + parsed_elements[prefix]["type"] = eval(exception_type) + elif prefix == "return": + parsed_elements["return"] = {"description": field_body} + elif prefix == "rtype": + parsed_elements["return"].update(parse_type(field_body)) + parsed_elements["__description"] = " ".join(description) + return parsed_elements diff --git a/agents/manual_assistants/execution.py b/agents/manual_assistants/execution.py new file mode 100644 index 0000000..acba9e7 --- /dev/null +++ b/agents/manual_assistants/execution.py @@ -0,0 +1,21 @@ +from toolStatus import ToolStatus + +class Execution: + threadId: str + runId: str + actionId: str + arguments: {} + exit: bool + toolStatus: ToolStatus + + def __init__(self): + self.exit = False + self.toolStatus = ToolStatus() + + def __str__(self): + properties_str = ', '.join(f'{key}: {value}' for key, value in self.__dict__.items()) + return f'Execution({properties_str})' + + def __repr__(self): + return self.__str__() + diff --git a/agents/manual_assistants/function.py b/agents/manual_assistants/function.py new file mode 100644 index 0000000..4447b52 --- /dev/null +++ b/agents/manual_assistants/function.py @@ -0,0 +1,49 @@ +from abc import abstractmethod + +import yaml + +from pathlib import Path + +from logger import Logger +from doc_parser import func_to_openai_function_spec + + +class Function: + def __init__(self): + self.log = Logger(self.__class__.__name__) + + def set_name(self, name): + self.name = name + + def set_filepath(self, filepath): + self.filepath = filepath + + def set_agent(self, agent): + self.agent = agent + + def set_context(self, context): + self.context = context + + def set_execution(self, execution): + self.execution = execution + + def get_config(self): + filepath = Path(self.filepath) + config_filepath = filepath.with_suffix(".config.yaml") + if config_filepath.is_file(): + try: + self.log.debug( + f"Loading configuration for {self.name} from filepath: {config_filepath}" + ) + with open(config_filepath, "r") as config_file: + config = yaml.safe_load(config_file) + self.log.debug(f"Loaded YAML configuration for {self.name}: {config}") + return config + except Exception as e: + self.log.error(f"Error loading configuration for {self.name}: {str(e)}") + raise ValueError(f"Failed to load configuration file for {self.name}") from e + return func_to_openai_function_spec(self.name, self.__call__) + + @abstractmethod + def __call__(self, **kwargs): + pass diff --git a/agents/manual_assistants/function_manager.py b/agents/manual_assistants/function_manager.py new file mode 100644 index 0000000..25a6cd6 --- /dev/null +++ b/agents/manual_assistants/function_manager.py @@ -0,0 +1,210 @@ +import os +import json +import importlib + +from pathlib import Path + +from logger import Logger +import util + +try: + import langchain.tools + HAS_LANGCHAIN = True +except ImportError: + HAS_LANGCHAIN = False + +LANGCHAIN_TOOL_PREFIX = "Langchain-" + + +class FunctionManager: + """ + Manage functions. + """ + + def __init__(self, additional_functions=None): + self.additional_functions = additional_functions or {} + self.log = Logger(self.__class__.__name__) + self.user_function_dirs = ( + util.get_environment_variable_list("function_dir") or [] + ) + self.make_user_function_dirs() + self.system_function_dirs = [ + os.path.join(util.get_file_directory(), "agentTools", "functions"), + ] + self.all_function_dirs = self.system_function_dirs + self.user_function_dirs + + def make_user_function_dirs(self): + for function_dir in self.user_function_dirs: + os.makedirs(function_dir, exist_ok=True) + + def load_function(self, function_name): + self.log.debug("Loading function from dirs: %s" % ", ".join(self.all_function_dirs)) + function_filepath = None + try: + for function_dir in self.all_function_dirs: + if os.path.exists(function_dir) and os.path.isdir(function_dir): + self.log.info(f"Processing directory: {function_dir}") + filename = f"{function_name}.py" + if filename in os.listdir(function_dir): + self.log.debug( + f"Loading function file {filename} from directory: {function_dir}" + ) + try: + filepath = os.path.join(function_dir, filename) + with open(filepath, "r") as _: + function_filepath = filepath + except Exception as e: + self.log.warning( + f"Can't open function file {function_name} from directory: {function_dir}: {e}" + ) + else: + message = f"Failed to load function {function_name}: Directory {function_dir!r} not found or not a directory" + self.log.error(message) + return False, None, message + except Exception as e: + message = f"An error occurred while loading function {function_name}: {e}" + self.log.error(message) + return False, None, message + if function_filepath is not None: + message = ( + f"Successfully loaded function file {function_name} from directory: {function_dir}" + ) + self.log.info(message) + return True, function_filepath, message + return False, None, f"Function {function_name} not found" + + def function_exists(self, function_name): + if self.is_langchain_tool(function_name): + return bool(self.get_langchain_tool(function_name)) + return function_name in self.functions + + def is_langchain_tool(self, function_name): + self.log.debug(f"Checking for Langchain tool: {function_name}") + return function_name.lower().startswith(LANGCHAIN_TOOL_PREFIX.lower()) + + def get_langchain_tool(self, function_name): + self.log.debug(f"Loading Langchain tool: {function_name}") + tool_name = util.remove_prefix(function_name, LANGCHAIN_TOOL_PREFIX) + try: + tool = getattr(langchain.tools, tool_name) + tool_instance = tool() + return tool_instance + except Exception as e: + self.log.warning(f"Could not load Langchaine tool: {function_name}: {str(e)}") + return None + + def get_langchain_tool_spec(self, function_name): + self.log.debug(f"Loading tool spec for Langchain tool: {function_name}") + tool_instance = self.get_langchain_tool(function_name) + if not tool_instance: + raise RuntimeError(f"Langchain tool {function_name} not found") + spec = langchain.tools.format_tool_to_openai_function(tool_instance) + spec["name"] = function_name + return spec + + def run_langchain_tool(self, function_name, input_data): + self.log.debug(f"Running langchaing tool: {function_name} with data: {input_data}") + tool_instance = self.get_langchain_tool(function_name) + if not tool_instance: + raise RuntimeError(f"Langchain tool {function_name} not found") + try: + result = tool_instance.run(input_data) + except Exception as e: + message = ( + f"Error: Exception occurred while running langchain tool {function_name}: {str(e)}" + ) + self.log.error(message) + return False, None, message + message = f"Langchain tool {function_name} executed successfully, output data: {result}" + self.log.info(message) + return True, result, message + + def load_functions(self): + self.log.debug("Loading functions from dirs: %s" % ", ".join(self.all_function_dirs)) + self.functions = self.additional_functions + try: + for function_dir in self.all_function_dirs: + if os.path.exists(function_dir) and os.path.isdir(function_dir): + self.log.info(f"Processing directory: {function_dir}") + for filename in os.listdir(function_dir): + filepath = os.path.join(function_dir, filename) + if filepath.endswith(".py"): + function_name = Path(filename).stem + self.log.debug( + f"Loading function file {filename} from directory: {function_dir}" + ) + self.functions[function_name] = filepath + else: + message = ( + f"Failed to load directory {function_dir!r}: not found or not a directory" + ) + self.log.error(message) + return False, None, message + return True, self.functions, "Successfully loaded functions" + except Exception as e: + message = f"An error occurred while loading functions: {e}" + self.log.error(message) + return False, None, message + + def setup_function_instance(self, function_name, function_path, context=None, agent=None, execution=None): + self.log.info(f"Loading function {function_name} from {function_path}") + try: + spec = importlib.util.spec_from_file_location(function_name, function_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + function_class_name = util.snake_to_class(function_name) + function_class = getattr(module, function_class_name) + function_instance = function_class() + function_instance.set_name(function_name) + function_instance.set_filepath(function_path) + function_instance.set_context(context) + function_instance.set_agent(agent) + function_instance.set_execution(execution) + return function_instance + except Exception as e: + self.log.error(f"Error creating function instance for {function_name}: {e}") + raise RuntimeError(f"Error creating function instance for {function_name}") from e + + def get_function_config(self, function_name): + self.log.debug(f"Getting config for function: {function_name}") + if self.is_langchain_tool(function_name): + return self.get_langchain_tool_spec(function_name) + try: + _success, function_path, user_message = self.load_function(function_name) + function_instance = self.setup_function_instance(function_name, function_path) + config = function_instance.get_config() + return config + except Exception as e: + self.log.error(f"Error loading function configuration for {function_name}: {str(e)}") + raise RuntimeError(f"Failed to load configuration for {function_name}") from e + + def run_function(self, function_name, input_data, context, agent, execution): + if isinstance(input_data, str): + input_data = json.loads(input_data, strict=False) + if self.is_langchain_tool(function_name): + if HAS_LANGCHAIN: + return self.run_langchain_tool(function_name, input_data) + raise RuntimeError( + f"Langchain tool {function_name} not found, please install langchain" + ) + self.log.debug(f"Running function: {function_name} with data: {input_data}") + success, function_path, user_message = self.load_function(function_name) + if not success: + return False, function_name, user_message + function_instance = self.setup_function_instance(function_name, function_path, context, agent, execution) + try: + output_data = function_instance(**input_data) + self.log.info( + f"Function {function_name} executed successfully, output data: {output_data}" + ) + return True, output_data, f"Function {function_name!r} executed successfully" + except Exception as e: + message = f"Error: Exception occurred while executing {function_path}: {str(e)}" + self.log.error(message) + return False, None, message + + def is_system_function(self, filepath): + for dir in self.system_function_dirs: + if filepath.startswith(dir): + return True + return False diff --git a/agents/manual_assistants/logger.py b/agents/manual_assistants/logger.py new file mode 100644 index 0000000..56e007b --- /dev/null +++ b/agents/manual_assistants/logger.py @@ -0,0 +1,66 @@ +import logging +import requests +from logging import Handler, LogRecord +from agent import Agent + +DEFAULT_DEBUG_ENDPOINT = 'http://localhost:8000/send-message/' + +DEFAULT_LOG_FORMAT = "%(name)s - %(levelname)s - %(message)s" +DUMMY_LOG_RECORD = LogRecord(name="", level=0, pathname="", lineno=0, msg="", args=(), exc_info=None) +LOG_DEFAULT_ATTRS = set(vars(DUMMY_LOG_RECORD).keys()) + + +class HTTPDebuggerHandler(Handler): + def __init__(self, agent: Agent, url: str = DEFAULT_DEBUG_ENDPOINT): + super().__init__() + self.agent = agent + self.url = url + + def emit(self, record): + data = { + 'name': self.agent.id, + 'label': self.agent.name, + 'model': self.agent.model, + 'log_level': record.levelname, + 'message': record.getMessage(), + } + extra_attrs = {key: value for key, value in record.__dict__.items() if key not in LOG_DEFAULT_ATTRS} + data.update(extra_attrs) + try: + requests.post(self.url, json=data) + except requests.exceptions.RequestException: + # Silently ignore request exceptions, allows operation without an active debugger. + pass + except Exception: + self.handleError(record) + + +class AgentLogger: + def __new__(cls, name, agent: Agent): + logger = logging.getLogger(name) + # Prevent duplicate loggers. + if logger.hasHandlers(): + return logger + logger.setLevel(logging.DEBUG) + log_console_handler = logging.StreamHandler() + log_console_handler.setFormatter(logging.Formatter(DEFAULT_LOG_FORMAT)) + log_console_handler.setLevel(logging.DEBUG) + logger.addHandler(log_console_handler) + http_debugger_handler = HTTPDebuggerHandler(agent) + http_debugger_handler.setLevel(logging.DEBUG) + logger.addHandler(http_debugger_handler) + return logger + + +class Logger: + def __new__(cls, name): + logger = logging.getLogger(name) + # Prevent duplicate loggers. + if logger.hasHandlers(): + return logger + logger.setLevel(logging.DEBUG) + log_console_handler = logging.StreamHandler() + log_console_handler.setFormatter(logging.Formatter(DEFAULT_LOG_FORMAT)) + log_console_handler.setLevel(logging.DEBUG) + logger.addHandler(log_console_handler) + return logger diff --git a/agents/manual_assistants/network.py b/agents/manual_assistants/network.py new file mode 100644 index 0000000..9807775 --- /dev/null +++ b/agents/manual_assistants/network.py @@ -0,0 +1,23 @@ +import queue as queueModule +from context import Context +from agent import Agent + +def __buildChannel(ctx: Context, agent: Agent): + if hasattr(agent, 'channels'): + for channel in agent.channels: + newChannel = True + for existingChannel in ctx.channels: + if existingChannel['name'] == channel: + existingChannel['agents'].append(agent.name) + newChannel = False + if newChannel: + ctx.channels.append({"name": channel, "agents": [agent.name]}) + +def build(ctx: Context): + for agent in ctx.agents: + # Build private queues + ctx.queues[agent.name] = queueModule.Queue() + + # Build channels + __buildChannel(ctx, agent) + print(f"Channels: {ctx.channels}") \ No newline at end of file diff --git a/agents/manual_assistants/requirements.txt b/agents/manual_assistants/requirements.txt new file mode 100644 index 0000000..a5fae35 --- /dev/null +++ b/agents/manual_assistants/requirements.txt @@ -0,0 +1,5 @@ +docutils>=0.20.1 +Jinja2 +openai +PyYAML +requests diff --git a/agents/manual_assistants/run.py b/agents/manual_assistants/run.py new file mode 100644 index 0000000..2a08518 --- /dev/null +++ b/agents/manual_assistants/run.py @@ -0,0 +1,93 @@ +import yaml +from openai import OpenAI +import os +import threading +import dotenv +import argparse +import sys +import pathlib + +from context import Context +import network +from agent import Agent +from agentProcessor import AgentProcessor +from function_manager import FunctionManager +from template_manager import TemplateManager +from OAIWrapper import OAIWrapper +import agentEnvHandler + +dotenv.load_dotenv() +api_key = os.getenv('OPENAI_API_KEY') +if api_key is None: + raise ValueError('The OPENAI_API_KEY environment variable is not set.') + +client = OpenAI(api_key=api_key) + +# Setup argument parser +parser = argparse.ArgumentParser(description='Load agents configuration from its configuration folder.') +parser.add_argument('--agents-definition-folder', dest='agentsDefinitionFolder', required=False, help='Path to the agents definition folder. Should contain an "agent.yaml" file') + +# Parse arguments +args = parser.parse_args() + +# Check if the agents-definition-folder argument was passed +if not args.agentsDefinitionFolder: + parser.print_help() + sys.exit(1) + +# Construct the absolute path to the agents.yaml file +workDir = pathlib.Path(__file__).parent.resolve() +agentsDefinitionDir = os.path.join(workDir, args.agentsDefinitionFolder) +agentsYAML = os.path.join(agentsDefinitionDir, "agents.yaml") + +# Check if the provided file path exists +if not os.path.isfile(agentsYAML): + print(f"Error: The file {agentsYAML} does not exist.") + sys.exit(1) + +with open(agentsYAML, 'r') as stream: + agentsProperties = yaml.safe_load(stream) + agents = [Agent(properties) for properties in agentsProperties] + +ctx = Context(client, agents) + +# LOAD ENV IDs +agentsIdsFile = os.path.join(agentsDefinitionDir, "agentsIds.env") +# Ensure the file exists by opening it in append mode, then immediately close it +with open(agentsIdsFile, 'a'): + pass + +with open(agentsIdsFile, 'r') as stream: + agentsIds = yaml.safe_load(stream) + if agentsIds: + for properties in agentsIds: # For each agent + for agent in agents: # Find its definition + if agent.name == properties['name']: + if not hasattr(agent, 'id'): # If ID is not hardcoded set it + agent.id = properties['id'] + +print(f"Agents: {agents}") + +function_manager = FunctionManager() +function_manager.load_functions() +template_manager = TemplateManager([agentsDefinitionDir]) +template_manager.load_templates() + +# Create/update assistants. +for agent in agents: + oai_wrapper = OAIWrapper(client, agent, function_manager, template_manager) + if not hasattr(agent, 'id'): # It's a new agent + oai_wrapper.createAssistant() + agentEnvHandler.saveId(agentsIdsFile, agent) + # Tools are sent to the assistant on update, so always update. + oai_wrapper.updateAssistant() + +network.build(ctx) + +for agent in agents: + processor = AgentProcessor(function_manager) + threading.Thread(target=processor.processThread, args=(ctx, agent,)).start() + +for agent in agents: + if hasattr(agent, 'initMessage'): + ctx.queues[agent.name].put(agent.initMessage) diff --git a/agents/manual_assistants/template_manager.py b/agents/manual_assistants/template_manager.py new file mode 100644 index 0000000..b01789a --- /dev/null +++ b/agents/manual_assistants/template_manager.py @@ -0,0 +1,85 @@ +import copy + +from jinja2 import Environment, FileSystemLoader, TemplateNotFound + +from logger import Logger, AgentLogger + + +class TemplateManager: + """ + Manage templates. + """ + + def __init__(self, template_dirs=None): + """ + Initializes the class with the given template directories. + + :param template_dirs: The list of directories to search for templates. + :type template_dirs: list, optional + """ + self.log = Logger(self.__class__.__name__) + self.template_dirs = template_dirs or [] + self.templates = [] + self.templates_env = None + + def load_templates(self): + """ + Load templates from directories. + + :return: None + """ + self.log.debug("Loading templates from dirs: %s" % ", ".join(self.template_dirs)) + self.templates_env = Environment(loader=FileSystemLoader(self.template_dirs)) + self.templates = self.templates_env.list_templates() or [] + + def get_template(self, template_name): + """ + Fetches a template. + + :param template_name: The name of the template to fetch + :type template_name: str + :return: The fetched template, or None if the template is not found + :rtype: Template or None + """ + try: + template = self.templates_env.get_template(template_name) + except TemplateNotFound: + return False, None, f"Template not found: {template_name}" + return True, template, f"Retrieved template: {template_name}" + + def render_template(self, template_name, variables=None): + """ + Render a template with variable substitutions. + + :param agent: The associated agent. + :type agent: object + :param template_name: The name of the template to render + :type template_name: str + :return: A tuple containing a success flag, the rendered message or template name, and a user message + :rtype: tuple + """ + variables = variables or {} + success, template, user_message = self.get_template(template_name) + if not success: + return success, template_name, user_message + try: + message = template.render(**variables) + user_message = f"Rendered template: {template_name}" + self.log.debug(user_message) + return True, message, user_message + except Exception as e: + user_message = f"Error rendering template: {e}" + self.log.error(user_message) + return False, None, user_message + + def render_agent_template(self, agent, variables=None): + agent_log = AgentLogger(agent.name, agent) + final_variables = vars(agent) + final_variables.update(variables or {}) + agent_log.debug(f"Rendering template for agent {agent.name}", extra={'variables': final_variables}) + try: + return self.render_template(agent.instructions, final_variables) + except Exception as e: + message = f"Error rendering template for agent {agent.name}: {e}" + agent_log.error(message) + return False, None, message diff --git a/agents/manual_assistants/toolStatus.py b/agents/manual_assistants/toolStatus.py new file mode 100644 index 0000000..9053147 --- /dev/null +++ b/agents/manual_assistants/toolStatus.py @@ -0,0 +1,15 @@ +class ToolStatus: + waiting: bool + output: {} + + def __init__(self): + self.waiting = False + self.output = {} + + def __str__(self): + properties_str = ', '.join(f'{key}: {value}' for key, value in self.__dict__.items()) + return f'Execution({properties_str})' + + def __repr__(self): + return self.__str__() + \ No newline at end of file diff --git a/agents/manual_assistants/util.py b/agents/manual_assistants/util.py new file mode 100644 index 0000000..38daa32 --- /dev/null +++ b/agents/manual_assistants/util.py @@ -0,0 +1,31 @@ +import os +import re +import inspect + + +def get_file_directory(): + filepath = inspect.stack()[1].filename + return os.path.dirname(os.path.abspath(filepath)) + + +def snake_to_class(string): + parts = string.split("_") + return "".join(word.title() for word in parts) + + +def get_environment_variable(name, default=None): + return os.environ.get(f"HAAS_{name.upper()}", default) + + +def get_environment_variable_list(name): + var_list = get_environment_variable(name) + return split_on_delimiter(var_list, ":") if var_list else None + + +def split_on_delimiter(string, delimiter=","): + return [x.strip() for x in string.split(delimiter)] + + +def remove_prefix(text, prefix): + pattern = r"(?i)^" + re.escape(prefix) + return re.sub(pattern, "", text) diff --git a/tool_maker/README.md b/agents/tool_maker/README.md similarity index 82% rename from tool_maker/README.md rename to agents/tool_maker/README.md index ca0e9b4..eed1691 100644 --- a/tool_maker/README.md +++ b/agents/tool_maker/README.md @@ -5,15 +5,20 @@ This preliminary experiment has to do with getting the OpenAI Assistant endpoint This function is as-yet undefined. # Version 1 -run the ```tool_maker_demo.py``` file. +Ensure you have an OPENAI_API_KEY set following this guide: https://help.openai.com/en/articles/5112595-best-practices-for-api-key-safety +Make sure you are in the root directory. +run the ```unit_manager.py``` file using the following command. +` -m agents.tool_maker.unit_manager`. +Where you have inserted your correct python path, this will run the file as if it was part of a module with root location. You will be prompted to define a tool for creation. The assistant will then generate an OpenAI tool compatible JSON schema defining the name of the new function, it's description and the input argument schema. It will proceed to add this tool to the current assistant. (Example) -![[DEMO.png]] +![Example](imgs/DEMO.png) (Update to assistant on OpenAI servers) -![[OpenAI_Tools.png]] + +![Update to assistant on OpenAI servers](imgs/OpenAI_Tools.PNG) ## Assistant Instructions @@ -46,13 +51,8 @@ Iterate: Make adjustments as requested by the user, refining the function name, Finalize: Once the user gives approval, consider the function creation process complete. -Note: Remember to prioritize user requirements and emphasize clear communication in the function description, as highlighted by the user.``` +Note: Remember to prioritize user requirements and emphasize clear communication in the function description, as highlighted by the user. ``` ## Flowchart -![[flow.png]] - -# Next Steps -This process allows the assistant to request a function with some parameters, and allows for the addition of that function to the assistants tool, but no such function is ever actually made. - -A function object needs to be saved that a function_finder can look for to run the output parameters of the assistant, even the output in the example image above is a hallucination. \ No newline at end of file +![Flow Diagram](imgs/flow.png) diff --git a/__init__.py b/agents/tool_maker/__init__.py similarity index 100% rename from __init__.py rename to agents/tool_maker/__init__.py diff --git a/agents/tool_maker/assistant_manager.py b/agents/tool_maker/assistant_manager.py new file mode 100644 index 0000000..f92397d --- /dev/null +++ b/agents/tool_maker/assistant_manager.py @@ -0,0 +1,65 @@ +from agents.tool_maker.tool_manager import ToolManager +from pathlib import Path +import os +import json +from agents.agent_builder.create import AgentBuilder + +class AssistantManager: + + def __init__(self, client): + self.client = client + self.assistant = None + self.agent_builder = AgentBuilder(client=self.client) + Path(__file__).absolute().parent + tools_path = os.path.join( + Path(__file__).absolute().parent, "tool_creator_metadata.json" + ) + with open(tools_path, "r") as file: + self.assistant_package = json.load(file) + + def get_assistant(self): + """Retrieve or create an assistant for testing this functionality""" + name = self.assistant_package["creator"]["name"] + self.agent_builder.create_assistant(name) + if not name in [ + assistant.name for assistant in self.client.beta.assistants.list() + ]: + raise ValueError(f'{name} needs to be created using create.py in /agents/agent_builder/') + else: + assistant_dict = { + assistant.name: assistant.id + for assistant in self.client.beta.assistants.list() + } + assistant = self.client.beta.assistants.retrieve( + assistant_id=assistant_dict[name] + ) + self.assistant = assistant + return assistant + + def get_coding_assistant(self): + """Retrieve or create an assistant for testing this functionality""" + name = self.assistant_package["writer"]["name"] + self.agent_builder.create_assistant(name) + if not name in [ + assistant.name for assistant in self.client.beta.assistants.list() + ]: + raise ValueError(f'{name} needs to be created using create.py in /agents/agent_builder/') + else: + assistant_dict = { + assistant.name: assistant.id + for assistant in self.client.beta.assistants.list() + } + assistant = self.client.beta.assistants.retrieve( + assistant_id=assistant_dict[name] + ) + self.assistant = assistant + return assistant + +if __name__ == "__main__": + from shared.openai_config import get_openai_client + + client = get_openai_client() + + assistant_manager = AssistantManager(client=client) + assistant = assistant_manager.get_assistant() + print(assistant) diff --git a/agents/tool_maker/chat_manager.py b/agents/tool_maker/chat_manager.py new file mode 100644 index 0000000..ae8fba5 --- /dev/null +++ b/agents/tool_maker/chat_manager.py @@ -0,0 +1,246 @@ +import importlib +from pathlib import Path +from agents.tool_maker.tool_manager import ToolManager +import json +import os +from openai import OpenAI + +Assistant = type(OpenAI().beta.assistants.list().data[0]) +Thread = type(OpenAI().beta.threads.create()) + + +class ChatManager: + def __init__(self, client: OpenAI): + self.client = client + functions_path = os.path.join( + Path(__file__).absolute().parent, "python_functions" + ) + self.functions_path = functions_path + print(self.functions_path) + + def create_thread_from_user_input(self): + return self.client.beta.threads.create( + messages=[{"role": "user", "content": input("Begin\n")}] + ) + + def create_empty_thread(self): + return self.client.beta.threads.create() + + def run_python_from_function_name(self, call): + print("CALLING FUNCTION") + base = ".".join(__name__.split(".")[:-1]) + try: + function_name = call.function.name + + fn = getattr( + importlib.reload( + importlib.import_module(f"{base}.python_functions.{function_name}") + ), + function_name, + ) + print(fn) + result = fn(**json.loads(call.function.arguments)) + response = {"tool_call_id": call.id, "output": f"result:{result}"} + except Exception as error: + response = { + "tool_call_id": call.id, + "output": f"{{{type(error)}:{error.args}}}", + } + print(response) + return response + + def get_existing_functions(self): + print("Get Built Functions") + results = [] + if os.path.exists(self.functions_path): + for filename in os.listdir(self.functions_path): + if filename.endswith(".json"): + file_path = os.path.join(self.functions_path,filename) + with open(file_path, "r") as file: + results.append(file) + return results + + def handle_fucntion_request( + self, + call, + interface_assistant: Assistant, + interface_thread: Thread, + functional_assistant: Assistant, + functional_thread: Thread, + ): + try: + # Create Function Tool + schema = ToolManager.schema_from_response(call.function.arguments) + tool = ToolManager.tool_from_function_schema(schema) + filtered_interface_assistant_tools = list(filter(lambda tool: tool.type == "function" ,interface_assistant.tools)) + if tool["function"]["name"] in [ + previous_tool.function.name + for previous_tool in filtered_interface_assistant_tools + ]: + tools = [ + previous_tool + for previous_tool in filtered_interface_assistant_tools + if previous_tool.function.name != tool["function"]["name"] + ] + interface_assistant = self.client.beta.assistants.update( + assistant_id=interface_assistant.id, + tools=[*tools, tool], + ) + else: + interface_assistant = self.client.beta.assistants.update( + assistant_id=interface_assistant.id, + tools=[*interface_assistant.tools, tool], + ) + + # Generate Python Function + self.client.beta.threads.messages.create( + thread_id=functional_thread.id, content=str(tool), role="user" + ) + functional_run = self.client.beta.threads.runs.create( + thread_id=functional_thread.id, + assistant_id=functional_assistant.id, + ) + + functional_response = self.simple_run( + run=functional_run, + thread=functional_thread, + ) + function_lines = functional_response.split("```python")[1].split("```")[0] + name = tool["function"]["name"] + if not os.path.exists(self.functions_path): + os.mkdir(self.functions_path) + with open(f"{self.functions_path}/{name}.py", "w") as file: + file.writelines(function_lines) + with open(f"{self.functions_path}/{name}.json", "w") as file: + file.writelines(str(schema)) + + response = {"tool_call_id": call.id, "output": "{success}"} + + except Exception as error: + # If error, pass details back to assistant for next steps + response = { + "tool_call_id": call.id, + "output": f"{{{type(error)}:{error.args}}}", + } + + return interface_assistant, response + + def simple_run(self, run, thread): + """Supply context to assistant and await for next user response""" + while run.status != "completed": + run = self.client.beta.threads.runs.retrieve( + run_id=run.id, thread_id=thread.id + ) + if run.status == "requires_action": + responses = [] + for call in run.required_action.submit_tool_outputs.tool_calls: + print(f"calling: {call.function.name}") + if call.function.name == "get_existing_functions": + available_functions = self.get_existing_functions() + response = {"tool_call_id": call.id, "output": f"result:{available_functions}"} + responses.append(response) + else: + response = {"tool_call_id": call.id, "output": f"result:None"} + responses.append(response) + try: + run = self.client.beta.threads.runs.submit_tool_outputs( + run_id=run.id, + thread_id=thread.id, + tool_outputs=responses, + ) + except: + print(run.status) + print(run) + print(call) + print(responses) + + response = ( + self.client.beta.threads.messages.list(thread_id=thread.id) + .data[0] + .content[0] + .text.value + ) + return response + + def begin_run( + self, + run, + interface_assistant, + interface_thread, + functional_assistant, + functional_thread, + ): + while run.status != "completed": + run = self.client.beta.threads.runs.retrieve( + run_id=run.id, thread_id=interface_thread.id + ) + if run.status == "requires_action": + tools = [] + responses = [] + for call in run.required_action.submit_tool_outputs.tool_calls: + print(f"calling: {call.function.name}") + if call.function.name == "function_request": + interface_assistant, response = self.handle_fucntion_request( + call=call, + interface_assistant=interface_assistant, + interface_thread=interface_thread, + functional_assistant=functional_assistant, + functional_thread=functional_thread, + ) + else: + response = self.run_python_from_function_name(call) + responses.append(response) + try: + run = self.client.beta.threads.runs.submit_tool_outputs( + run_id=run.id, + thread_id=interface_thread.id, + tool_outputs=responses, + ) + except: + print(run.status) + print(run) + print(call) + print(responses) + if run.status == "failed" or run.status == "expired": + print("DIED") + run.status = "completed" + response = ( + self.client.beta.threads.messages.list(thread_id=interface_thread.id) + .data[0] + .content[0] + .text.value + ) + return interface_assistant, response + + def run_unit( + self, + interface_assistant: Assistant, + interface_thread: Thread, + functional_assistant: Assistant, + functional_thread: Thread, + ): + self.client.beta.threads.messages.create( + thread_id=interface_thread.id, content=input("type: "), role="user" + ) + print() + interface_run = self.client.beta.threads.runs.create( + thread_id=interface_thread.id, + assistant_id=interface_assistant.id, + instructions="please remember you are talking to an API, minimize output text tokens for cost saving. You are also able to communicate with the function ai using the description property of function_request.", + ) + interface_assistant, response = self.begin_run( + run=interface_run, + interface_assistant=interface_assistant, + interface_thread=interface_thread, + functional_assistant=functional_assistant, + functional_thread=functional_thread, + ) + interface_thread = self.client.beta.threads.retrieve( + thread_id=interface_thread.id + ) + functional_thread = self.client.beta.threads.retrieve( + thread_id=functional_thread.id + ) + print(response) + print() + return interface_assistant, interface_thread, functional_thread diff --git a/tool_maker/creator_config.py b/agents/tool_maker/creator_config.py similarity index 100% rename from tool_maker/creator_config.py rename to agents/tool_maker/creator_config.py diff --git a/tool_maker/DEMO.png b/agents/tool_maker/imgs/DEMO.png similarity index 100% rename from tool_maker/DEMO.png rename to agents/tool_maker/imgs/DEMO.png diff --git a/tool_maker/OpenAI_Tools.PNG b/agents/tool_maker/imgs/OpenAI_Tools.PNG similarity index 100% rename from tool_maker/OpenAI_Tools.PNG rename to agents/tool_maker/imgs/OpenAI_Tools.PNG diff --git a/tool_maker/flow.png b/agents/tool_maker/imgs/flow.png similarity index 100% rename from tool_maker/flow.png rename to agents/tool_maker/imgs/flow.png diff --git a/tool_maker/tool_creator.py b/agents/tool_maker/tool_creator.py similarity index 95% rename from tool_maker/tool_creator.py rename to agents/tool_maker/tool_creator.py index 1295662..4322f00 100644 --- a/tool_maker/tool_creator.py +++ b/agents/tool_maker/tool_creator.py @@ -6,9 +6,9 @@ import os from shared.utils import chat as chat_loop +from shared.openai_config import get_openai_client -from openai import OpenAI -client = OpenAI() # be sure to set your OPENAI_API_KEY environment variable +client = get_openai_client() def create_tool_creator(assistant_details): # create the assistant diff --git a/agents/tool_maker/tool_creator_metadata.json b/agents/tool_maker/tool_creator_metadata.json new file mode 100644 index 0000000..977fcaa --- /dev/null +++ b/agents/tool_maker/tool_creator_metadata.json @@ -0,0 +1,8 @@ +{ + "creator": { + "name": "tool_creator" + }, + "writer": { + "name": "temporary_function_writer" + } +} \ No newline at end of file diff --git a/agents/tool_maker/tool_manager.py b/agents/tool_maker/tool_manager.py new file mode 100644 index 0000000..0351df0 --- /dev/null +++ b/agents/tool_maker/tool_manager.py @@ -0,0 +1,28 @@ +import json + + +class ToolManager: + @staticmethod + def tool_from_function_schema(schema): + """takes a JSON schema and wraps in an OpenAI specified tool structure""" + tool = f"""{{ + "type":"function", + "function": {json.dumps(schema)}}} + """ + tool = json.loads(tool) + return tool + + @staticmethod + def schema_from_response(response): + """Takes an agent response and forms a JSON schema""" + function_request_obj = json.loads(response) + name = function_request_obj["name"] + description = function_request_obj["description"] + schema = function_request_obj["schema"] + schema = rf"""{{ + "name": "{name}", + "description": "{description}", + "parameters": + {schema} + }}""" + return json.loads(schema) diff --git a/tool_maker/tool_user.py b/agents/tool_maker/tool_user.py similarity index 95% rename from tool_maker/tool_user.py rename to agents/tool_maker/tool_user.py index 9891a87..fd91e91 100644 --- a/tool_maker/tool_user.py +++ b/agents/tool_maker/tool_user.py @@ -6,9 +6,9 @@ import json from shared.utils import chat as chat_loop +from shared.openai_config import get_openai_client -from openai import OpenAI -client = OpenAI() # be sure to set your OPENAI_API_KEY environment variable +client = get_openai_client() def create_tool_user(assistant_details): # create the assistant diff --git a/agents/tool_maker/unit_manager.py b/agents/tool_maker/unit_manager.py new file mode 100644 index 0000000..8dd5717 --- /dev/null +++ b/agents/tool_maker/unit_manager.py @@ -0,0 +1,60 @@ +from agents.tool_maker.assistant_manager import AssistantManager +from agents.tool_maker.chat_manager import ChatManager + + +class Unit: + """ + A class which creates and exposes chat functionality for a Unit Agent. + A Unit is a first prototype for a Minmium Viable Agent (MVA). + + A `Unit` is two `Assistant`s in a symbiotic relationship. + One `Assistant` is the Interface with a thread sharing input with the contents passed via the `chat` method, + the other `Assistant` is a functional one which shares a thread with `submit_tool` requests during runs and is responsible for writing python functions. + + :param AssistantManager assistant_manager: Creates and retrieves different `Assistant` types + :param ChatManager chat_manager: provides functionality for managing `Threads` + :param Assistant interface_assistant: talks with `chat` method + :param Assistant functional_assistant: writes python functions when `OpenAI.beta.threads.runs.submit_tools` is called in `chat` + :param Thread interface_thread: `Thread` between `interface_assistant` and `chat` + :param Thread functional_thread: `Thread` between `functional_assistant` and `OpenAI.beta.threads.runs.submit_tools` + :returns: this is retured + """ + + def __init__(self, client): + """ + Instantiates a Unit object + + :param Client client: OpenAI instance + """ + self.assistant_manager = AssistantManager(client=client) + self.chat_manager = ChatManager(client=client) + self.interface_assistant = self.assistant_manager.get_assistant() + self.functional_assistant = self.assistant_manager.get_coding_assistant() + + self.interface_thread = self.chat_manager.create_empty_thread() + self.functional_thread = self.chat_manager.create_empty_thread() + + def chat(self): + """ + Accepts user input and performs a thread run with the `interface_assistant` + """ + while True: + ( + self.interface_assistant, + self.interface_thread, + self.functional_thread, + ) = self.chat_manager.run_unit( + interface_assistant=self.interface_assistant, + interface_thread=self.interface_thread, + functional_assistant=self.functional_assistant, + functional_thread=self.functional_thread, + ) + + +if __name__ == "__main__": + from shared.openai_config import get_openai_client + + client = get_openai_client() + + unit = Unit(client=client) + unit.chat() diff --git a/tool_maker/user_config.py b/agents/tool_maker/user_config.py similarity index 100% rename from tool_maker/user_config.py rename to agents/tool_maker/user_config.py diff --git a/code_of_conduct.md b/code_of_conduct.md new file mode 100644 index 0000000..b77f675 --- /dev/null +++ b/code_of_conduct.md @@ -0,0 +1,489 @@ +# HAAS Code of Conduct + +## TLDR + +**Maximize Signal-to-Noise Ratio** + +1. **DON'T WASTE TIME:** Before you post, commit, or comment, make sure that your contribution is not going to waste anyone's time. Low effort and low value contributions will be removed. Repeat offenses will result in a ban. This reduces NOISE. +2. **ADD VALUE:** You should be seeking to maximize value added to the collective. Optimize your contributions to be succint, impactful, meaningful, and helpful. This boosts SIGNAL. +3. **DO NO HARM:** Zero tolerance for insults, trolling, moving the goalposts, and frivolous debates. Reduces noise and removes disruptors. + +For more information, check out the original C3P0 below. Participation in this project and community is a privilege, not a right. Act accordingly. + +## Don't Waste Time + +Time is our most scarce resource and is a proxy for everything else important; cognitive energy, and so on. Optimize your contributions to be a good use of time. That means you should expend your time wisely, and ensure that the time spent reading your contribution is also a good use of time. + +Good Uses of Time: +- High quality posts that are well thought out, well structured, easy to read, and not gigantic walls of text. LESS IS MORE. +- Demonstrations that quickly and succinctly convey the most information in the least amount of time. +- Examples that quickly convey how and why things work + +Bad Uses of Time: +- Debating, arguing, and quibbling over details or superfluous topics +- Moving the goalposts, changing the scope, or naysaying +- Walls of text, personal opinions, irrelevant contributions + +## Add Value + +The key here is to optimize value-add. If you don't have something particularly valuable to add, don't add anything. But, if you do have something valuable to add, please do so immediately and in an manner optimized for best use of time. + +Valuable Additions: +- Solve problems +- Add code +- Share resources +- Increase collective's understanding + +## Do No Harm + +This is boilerplate anti-trolling, flaming, and griefing. This behavior will result in an immediate irrevocable ban. Removing dirsuptors results in better signal to noise ratio. + + +## C3P0 +https://github.com/daveshap/C3P0 + +```markdown +Collaborative Culture Community Policy: Zero Tolerance + + + +I. Preamble: Understanding the Present + +The Collaborative Culture Community Policy Zero Tolerance (C3P0) arises +from a critical understanding of contemporary digital culture. We +recognize the pervasive influence of engagement-driven algorithms in our +online spaces, which often amplify and incentivize toxic competition, +harmful behaviors, and low-quality interactions. These algorithms, in +their relentless pursuit of engagement metrics, often prioritize volume +over value, controversy over collaboration, and outrage over +understanding. + + + +II. Vision: Imagining a Better Future + +The C3P0 puts forth a vision for online communities that depart from these +harmful norms and instead, place the well-being of their participants at +their core. We envision digital spaces where the time, energy, and +emotional well-being of participants are respected, where interactions are +thoughtful, where discussions are fruitful, and where the digital +experience is marked by empathy, respect, and understanding. We +acknowledge that this approach may result in lower engagement levels +quantitatively, but qualitatively, it promises to elevate the online +experience and create a more meaningful, inclusive, and enjoyable digital +landscape. + + + +III. Intent: Shaping the Change + +To bring this vision into reality, the C3P0 sets out a clear policy and +practical guidelines designed to nurture a healthier digital culture. The +policy underscores the crucial shift away from metrics-driven engagement +towards a values-driven engagement that prioritizes collaborative and +constructive participation. It establishes clear expectations, upholds +transparency, and enforces accountability. In doing so, we intend to +challenge the status quo, encourage digital platforms and creators to +adopt these principles, and ultimately, play our part in fixing the +internet, one digital community at a time. + + + +IV. Potential Impact: An Unexpected Upside + +While it's easy to assume that enforcing stricter community guidelines may +suppress activity and thereby reduce engagement, historical and +sociological patterns suggest otherwise. Just as the ban on smoking in +public spaces, which was initially perceived as detrimental to businesses +such as bars and restaurants, eventually increased their patronage, we +believe a similar pattern may emerge in online communities. + +By instituting these guidelines and pushing harmful behaviors to the +fringes, we may be able to create a more inviting environment. When online +spaces become more respectful, more inclusive, and more value-driven, they +also become more appealing to a broader audience. + +In essence, this is an appeal to quality over quantity: a smaller number +of high-quality, respectful interactions over a large volume of toxic +engagements. Yet, it's not only the quality that might improve - the +quantity might as well. + +By creating safer spaces online, we may actually foster deeper, more +meaningful interactions. We could attract more diverse voices, +perspectives, and ideas, which would otherwise be pushed out by toxic +behaviors. And so, paradoxically, by striving to create a more wholesome, +respectful environment - even if it means enforcing stricter guidelines +and seeing some decline in immediate engagement - we might actually drive +up engagement in the long run. + +In this way, the C3P0 isn't just an ethical and moral approach to digital +community management - it could also be a smarter, more sustainable +approach to fostering vibrant, engaging, and diverse online communities. +It's a win-win for everyone: a healthier, more enjoyable experience for +users, and a more engaged, diverse, and respectful community for platforms +and creators. + + + +V. Theory and Principles + +To embody our values and vision, we have identified several guiding +principles which serve as the backbone of our policy: + +A. Privilege of Participation + Engagement in our online community is a privilege, not a right. This + distinction underscores the need for every member to contribute positively + and constructively. Our community is not an inalienable public square but + a collective space built on shared norms and respect. It invites + individuals who uphold these values, enrich discussions, and respect + fellow participants. Those unable or unwilling to abide by these standards + may forfeit the privilege of participating in our community. + +B. Right to Consent + Every member of our community possesses the fundamental right to consent + to how they're treated. This principle asserts that one's online presence + doesn't imply an open invitation for harassment, abuse, or unwanted + attention. It repudiates the notion that by merely being online, + individuals are 'asking for' or 'exposing themselves to' potential harm. + We respect and uphold the rights of our members to engage in a manner that + they feel comfortable with, putting their safety and well-being over + superficial metrics of engagement. + +C. Time Vampirism + Time vampirism refers to behaviors that drain the collective energy and + time of our community. These behaviors, such as trolling, engaging in + baseless arguments, and continually shifting the goalposts, detract from + meaningful engagement and waste the valuable time of our members. We + consider time a precious commodity and take a strong stance against + practices that misuse or squander it. + +D. Collaborative Culture + We foster a culture of collaboration rather than competition. Many online + platforms promote competition, driving engagement through conflict and + outrage. This often results in divisive, harmful spaces that discourage + genuine discourse. In contrast, our community encourages collective + growth, mutual learning, and supportive interactions. We believe that a + collaborative environment, where members uplift and learn from each other, + leads to richer, more positive experiences. + +E. Constructive Participation + Constructive participation is the cornerstone of our community. We expect + all members to contribute in a manner that is thoughtful, respectful, and + aligned with the community's purpose. This involves listening to others, + providing insightful input, staying on-topic, and treating all + participants with kindness. Constructive participation recognizes and + appreciates the diversity of our community, fostering an environment where + different perspectives can be shared and valued without fear of hostility + or derision. + +F. Accountability + Accountability is central to the functioning of a healthy, respectful, and + collaborative online community. Existing algorithms and policies on many + online platforms often fall short in this regard, allowing harmful + behaviors to persist under the guise of 'free speech' or engagement. This + approach not only undermines the quality of interactions but can also have + severe mental health repercussions for users exposed to such behaviors. + Our policy asserts that accountability should be paramount in any online + community. Members should take responsibility for their actions, comments, + and the effect they have on others. + +G. Consequences + Just as in real life, there must be consequences for actions that harm or + disrupt the community. All too often, online platforms allow harmful + behaviors to go unchecked, breeding a toxic environment that discourages + meaningful engagement and collaboration. We stand firm against this trend. + Actions that violate the standards and guidelines of our community, such + as bullying, harassment, or any form of harm, must be met with clear, + swift, and appropriate consequences. These may range from warnings and + temporary suspensions to permanent bans, depending on the severity and + frequency of the violation. By enforcing consequences, we aim to create an + online environment that mirrors the respect, responsibility, and + accountability expected in our everyday offline interactions. + +The C3P0 is a bold step towards fostering a healthier, more supportive, +and collaborative internet culture. By implementing this policy, we aspire +to redirect the internet away from harmful practices and towards a +community that values respect, collaboration, and constructive +participation above all. + + + +VI. The Golden Rule: Don't Waste Time + +Time is the most precious commodity we possess, and within the context of +our policy, it becomes the 'new algorithm,' the key metric guiding our +community interactions. Our golden rule enshrines this concept: Don't +waste anyone's time. + +This principle acknowledges that every second spent within our community +is valuable 'signal.' It reflects our commitment to maximize the +'signal-to-noise' ratio in our community interactions. By 'signal,' we +mean meaningful, valuable contributions that enrich discussions and foster +a sense of community. 'Noise,' on the other hand, encompasses behaviors +and content that waste time, distract from meaningful engagement, or +disrupt the constructive community environment. + +This golden rule is inherently subjective and contextual, reflecting the +varied nature of discussions and dynamics across diverse internet +communities. What might be considered 'noise' or time-wasting in one +context might be acceptable in another. For instance, a light-hearted meme +may be a distraction in a focused technical discussion, but in a casual +conversation, it might foster a sense of community and fun. + +Our golden rule encourages every member to ponder before posting: Does +this contribution act as a valuable signal? Does it respect the time of +others and align with the community's purpose? This perspective flips the +conventional understanding of engagement on its head, with time and +'signal' quality becoming the critical factors over mere quantity. + +We trust our community members to understand and exercise this principle +effectively. We are not seeking to micromanage, but rather to foster a +culture where time, as the new algorithm, is respected and valued. By +promoting a high signal-to-noise ratio, we aim to nurture a collaborative +environment marked by meaningful engagement, mutual respect, and valuable +contributions. + + + +VII. Time-Wasting Behaviors: Reducing Noise + +The C3P0 defines certain behaviors as 'noise' within our community, which +tend to detract from the quality of interactions and waste the valuable +time of our members. Here, we offer a non-exhaustive list of behaviors +that are generally considered 'time-wasting'. Understanding these +behaviors can help our community members better adhere to our golden rule: +Don't waste anyone's time. + +A. Trolling + Trolling refers to intentionally disruptive actions aimed at provoking + negative reactions, derailing discussions, or sowing discord within the + community. This behavior serves no constructive purpose and wastes the + community's time by redirecting attention away from valuable discourse. + +B. Baseless Arguing + Engaging in arguments without any substantial basis or evidence, often for + the sake of arguing, is another form of time-wasting behavior. This not + only detracts from meaningful discussions but also creates a hostile + environment that discourages constructive participation. + +C. Shifting the Goalposts + This behavior involves continually changing the criteria or standards in a + discussion once they have been met. It results in endless debates that + waste time and stifle the productive exchange of ideas. It also includes + 'whataboutism' and other red herrings. + +D. Armchair Debating + Participating in debates about complex subjects without appropriate + knowledge, understanding, or consideration of expert opinions is often + unproductive and may mislead other community members, thus wasting their + time. This also includes 'oneupmanship' and sophistry. + +E. Disingenuous Behaviors + Any form of dishonesty, such as misrepresentation of facts, misleading + other members, or feigning ignorance to provoke a reaction, is considered + time-wasting. Authenticity and honesty are essential in creating a + community built on trust and mutual respect. + +F. Harassing Behaviors + Any actions that involve persistent unwanted attention, bullying, or + infringement on another member's right to consent are strictly considered + time-wasting and disrespectful. Our community places a high value on the + emotional well-being of all members, and harassment of any form will not + be tolerated. + +By clearly identifying these behaviors, we aim to promote self-awareness +among our members. We expect everyone in our community to refrain from +these time-wasting behaviors, to contribute positively to the +signal-to-noise ratio, and to respect the golden rule. We hope this +contributes to a collaborative, respectful, and engaging environment where +each interaction is a good use of everyone's time. + + + +VIII. Good Uses of Time: Amplifying Signal + +Following our Golden Rule, we want to emphasize behaviors that contribute +positively to our community's signal-to-noise ratio. These behaviors, or +'good uses of time,' are actively encouraged as they align with our vision +of cultivating a respectful, collaborative, and engaging online +environment. Again, this list isn't exhaustive but serves as an +illustration of potentially beneficial behaviors: + +A. Thoughtful Participation + Taking the time to craft well-thought-out responses, comments, or posts + that contribute to the topic at hand is highly valued. Thoughtful + participation fosters meaningful discussions and is a respectful use of + everyone's time. + +B. Active Listening + Active listening involves engaging with others' ideas, showing + understanding, and responding constructively. This behavior demonstrates + respect for others' time and effort in sharing their thoughts and fosters + an environment of mutual learning. + +C. Respectful Disagreement + Disagreements are inevitable in any community, but it's important to + handle them respectfully. Expressing disagreement in a thoughtful, + respectful manner that focuses on the idea rather than the person is a + productive use of time and enriches discussions. + +D. Asking Insightful Questions + Asking insightful questions can stimulate discussion, encourage deeper + thought, and promote mutual learning. These questions are often open-ended + and invite others to share their perspectives, experiences, or expertise. + +E. Sharing Knowledge + Sharing relevant information, expertise, or experiences that contribute to + a discussion is highly encouraged. It adds value to conversations and is a + good use of everyone's time. + +F. Constructive Feedback + Providing constructive feedback helps others improve, fosters mutual + growth, and strengthens the community. Remember to focus on the behavior + or the idea, not the person, and to communicate your feedback in a + respectful, supportive manner. + +By promoting these behaviors, we aim to cultivate a community environment +that values quality over quantity, respects each other's time, and fosters +meaningful, engaging interactions. We encourage all members to practice +these behaviors and contribute positively to the community's +signal-to-noise ratio. In this way, every interaction within our community +becomes a valuable signal, and a respectful use of everyone's time. + + + +IX. Consequences: Upholding Accountability + +Consequences for time-wasting or harmful behavior serve to uphold +accountability and maintain the respect, safety, and integrity of our +community. It is important to note that the capacity to enforce certain +consequences will depend on the specific capabilities of various digital +platforms. While we acknowledge this variability, the following is a +general guideline for understanding the potential consequences of +violating our policy. Again, this list is not exhaustive but offers a +range of possible actions: + +A. Warning + Initial minor offenses might result in a warning. This serves as an + opportunity for the offender to acknowledge their misstep and correct + their behavior. + +B. Temporary Suspension or Timeout + Repeated offenses or more severe misbehavior may result in temporary + suspension or a timeout. This punitive measure offers the offender a + period of reflection and the chance to reconsider their actions. + +C. Permanent Ban + In cases of extremely disruptive behavior or in the event of serious, + repeat offenses, a permanent ban may be enforced. This ensures the safety + and well-being of the rest of the community. + +D. Removal of Content + Certain offenses may necessitate the removal of the offender's content. + This can range from a single inappropriate comment to an entire thread, + depending on the severity of the violation. + +While the application of these consequences may vary from platform to +platform, the core principle remains the same: enforcing accountability +for harmful behaviors. We recognize that not all platforms allow for the +full range of these consequences, often providing only more extreme +measures like blocking or banning. In such cases, we encourage communities +to be judicious but firm in their enforcement. + +It's crucial to emphasize that the goal of these consequences isn't to +punish but to uphold the integrity, safety, and respect of our online +community. The same principle applies in various offline scenarios: flight +attendants wouldn't hesitate to enforce smoking prohibitions on a plane, +or if someone lit up a cigarette in a restaurant, they would be asked to +leave. These actions aren't seen as punishments, but as necessary measures +to ensure the safety and comfort of everyone else present. + +Likewise, the C3P0 policy sees the enforcement of consequences as a +commitment to creating a safe, respectful, and collaborative online +community. One person's noxious behavior can make the digital environment +unsafe and unpleasant for everyone else. By implementing consequences, +we're not punishing individuals but safeguarding the collective wellbeing +of our community. In this way, we are making the internet a better place +for everyone. + + + +X. Guidelines for Moderators: How to Use C3P0 Fairly + +The role of a moderator in implementing the C3P0 policy is crucial. It is +a challenging role that requires sensitivity, discernment, and a deep +understanding of our shared values and principles. The following +guidelines are designed to assist moderators in their role, ensuring that +they uphold the spirit of our policy while also encouraging lively, +constructive participation. + +A. Balance + The goal of our policy is not to suppress discussion or create + an environment of fear. While we adopt a Zero Tolerance stance towards + harmful behaviors, it is equally important to balance this with an + encouraging, inclusive atmosphere for genuine, well-meaning participants. + It's critical to foster an environment where users feel free to express + themselves, debate, and share ideas without fear of undue reprisal. The + aim should be to strike a balance where all users feel safe and heard, but + not silenced. + +B. Transparency + Being transparent about the rules, decisions, and actions + taken is crucial for fostering trust within the community. When enforcing + consequences, explain the reason clearly, citing the specific violation of + the policy. This clarity will not only help the individual understand + their misstep but also serve as a learning opportunity for the rest of the + community. Openly communicate any changes or updates to the community + guidelines, and provide reasons behind these modifications. Additionally, + consider creating a publicly accessible log of moderation actions (while + maintaining user privacy), which can demonstrate your commitment to + fairness and accountability. + +C. Consistency and Fairness + Treat all members of the community with equal + respect and fairness, regardless of their status or popularity. Ensure + that the policy is applied consistently to everyone. Avoid showing + favoritism or bias, as this can damage the trust and harmony within the + community. For instance, a new user violating the guidelines should + receive the same treatment as a seasoned member. In cases of rule + violation, communicate clearly about the infringement, the relevant + section of the policy it contravenes, and the subsequent action taken. By + doing so, you demonstrate transparency and uphold the principle of + fairness. + +D. Proactive Engagement + Anticipate potential issues and respond to them + before they escalate. This could involve addressing emerging conflicts, + clarifying misunderstandings, or reiterating community guidelines as + necessary. Being proactive also means guiding discussions constructively + to prevent them from spiraling into negativity or toxicity. For instance, + if you observe a conversation heating up, consider stepping in with a + reminder about respectful dialogue or steering the conversation back on + track. This proactive approach can maintain a positive environment and + prevent the need for punitive measures. + +E. Diplomacy and Empathy + The essence of moderation is not in the exercise + of power, but in diplomacy and empathy. When enforcing guidelines, + approach the situation with understanding and tact. Aim to guide rather + than chastise, keeping in mind that your goal is to foster a respectful, + constructive community. Before taking action, consider the context, the + user's history, and the potential for misunderstanding. If possible, + privately communicate with the user in question to address the issue, + explaining the violation and the necessity for the guideline. This + diplomatic approach can help resolve issues without resorting to public + penalties, which should be used as a last resort. + +Always remember that there is a person behind each username, with their +own experiences, perspectives, and feelings. Strive to foster a supportive +and understanding atmosphere where everyone feels respected and heard. +While firmness is necessary to maintain order and respect, it should +always be balanced with empathy and respect for individual dignity. + +The goal of the C3P0 is not just to penalize harmful behavior but to +actively encourage a positive, respectful, and collaborative culture. As a +moderator, your role is pivotal in shaping the tone and culture of the +community. Your fair and balanced approach to implementing this policy +will be key in creating an online space where everyone feels valued, +respected, and free to contribute. +``` diff --git a/contributing.md b/contributing.md index 6463265..c3cca6a 100644 --- a/contributing.md +++ b/contributing.md @@ -18,4 +18,6 @@ Thank you for your interest in contributing to the Hierarchical Autonomous Agent 2. **Adhere to the C3P0 Policy**: We follow the Collaborative Culture Community Policy: Zero Tolerance (C3P0) for harmful behavior and time-wasting. [C3P0 Policy](https://github.com/daveshap/C3P0). -3. **PR Requirements**: All PRs must include a clear description. Limit submissions to one PR per day, ensuring it adheres to the project's style and structure. Refraining from reformatting, refactoring, or restructuring the project is crucial—non-compliant PRs will be rejected. \ No newline at end of file +3. **PR Requirements**: All PRs must include a clear description. Limit submissions to one PR per day, ensuring it adheres to the project's style and structure. Refraining from reformatting, refactoring, or restructuring the project is crucial—non-compliant PRs will be rejected. + +4. **Examples and Demos**: All PRs must include examples and demos along with the code. This can be documented in a README, a link to a video, or screenshots. But we need to ensure that it's clear we understand what the code does. diff --git a/agent_builder/agents/Autonomous Swarm Agent Builder/files/HAAS_Documentation.md b/documentation/HAAS_Documentation.md similarity index 99% rename from agent_builder/agents/Autonomous Swarm Agent Builder/files/HAAS_Documentation.md rename to documentation/HAAS_Documentation.md index a8577e9..1d8f83e 100644 --- a/agent_builder/agents/Autonomous Swarm Agent Builder/files/HAAS_Documentation.md +++ b/documentation/HAAS_Documentation.md @@ -8,7 +8,7 @@ The HAAS is designed to be a self-expanding system where a core set of agents, g ## Theoretical Foundation -The AAHS is predicated on the notion that autonomous agents require a robust ethical and operational framework to make decisions that align with human values and organizational goals. This is rooted in the understanding that AI, much like humans, cannot operate effectively without a set of guiding principles or a moral compass. The HAAS addresses this by establishing a multi-tiered system where each layer of agents operates within a defined ethical and functional scope, ensuring decisions are made with consideration to morality, ethics, and utility. +The HAAS is predicated on the notion that autonomous agents require a robust ethical and operational framework to make decisions that align with human values and organizational goals. This is rooted in the understanding that AI, much like humans, cannot operate effectively without a set of guiding principles or a moral compass. The HAAS addresses this by establishing a multi-tiered system where each layer of agents operates within a defined ethical and functional scope, ensuring decisions are made with consideration to morality, ethics, and utility. ## System Architecture diff --git a/agent_builder/OpenAI_Documentation.md b/documentation/OpenAI_Documentation.md similarity index 100% rename from agent_builder/OpenAI_Documentation.md rename to documentation/OpenAI_Documentation.md diff --git a/SOB.png b/documentation/SOB.png similarity index 100% rename from SOB.png rename to documentation/SOB.png diff --git a/global_context/HAAS_Documentation.md b/global_context/HAAS_Documentation.md new file mode 100644 index 0000000..1d8f83e --- /dev/null +++ b/global_context/HAAS_Documentation.md @@ -0,0 +1,128 @@ +# Project: Hierarchical Autonomous Agent Swarm + +## Overview + +The Hierarchical Autonomous Agent Swarm (HAAS) is a groundbreaking initiative that leverages OpenAI's latest advancements in agent-based APIs to create a self-organizing and ethically governed ecosystem of AI agents. Drawing inspiration from the ACE Framework, HAAS introduces a novel approach to AI governance and operation, where a hierarchy of specialized agents, each with distinct roles and capabilities, collaborate to solve complex problems and perform a wide array of tasks. + +The HAAS is designed to be a self-expanding system where a core set of agents, governed by a Supreme Oversight Board (SOB), can design, provision, and manage an arbitrary number of sub-agents tailored to specific needs. This document serves as a comprehensive guide to the theoretical underpinnings, architectural design, and operational principles of the HAAS. + +## Theoretical Foundation + +The HAAS is predicated on the notion that autonomous agents require a robust ethical and operational framework to make decisions that align with human values and organizational goals. This is rooted in the understanding that AI, much like humans, cannot operate effectively without a set of guiding principles or a moral compass. The HAAS addresses this by establishing a multi-tiered system where each layer of agents operates within a defined ethical and functional scope, ensuring decisions are made with consideration to morality, ethics, and utility. + +## System Architecture + +### Supreme Oversight Board (SOB) + +At the pinnacle of the HAAS hierarchy is the Supreme Oversight Board (SOB), a collective of high-level agents modeled after wise and ethical archetypes from various cultures and narratives. The SOB's responsibilities include: + +- Establishing and upholding the ethical framework and overarching mission of the agent swarm. +- Making high-level decisions and judgments, including the creation and termination of agents. +- Monitoring the activities of all agents to ensure alignment with the system's core values and objectives. +- Serving as a role-based access control (RBAC) mechanism to maintain order and security within the system. + +### Executive Agents + +Below the SOB are the Executive Agents, akin to the executive leadership in a corporation. These agents are tasked with: + +- Translating the SOB's directives into actionable plans and strategies. +- Overseeing specific operational domains such as resource allocation, process optimization, and task execution. +- Coordinating with one another to ensure the smooth operation of the agent swarm. + +### Sub-Agents + +Sub-Agents are specialized agents created by the SOB or Executive Agents to perform specific tasks. They are designed with particular functions and knowledge bases to address the needs identified by the higher tiers of the hierarchy. + +## Agent Configuration + +Each agent in the HAAS is defined by the following parameters: + +### Functions + +Agents are equipped with a set of functions that enable them to perform their designated roles. These functions include API interactions, internal process management, and the ability to spawn additional agents if required. + +### Files + +Agents have access to a selection of files that serve as their knowledge base, providing them with the information necessary to carry out their tasks effectively. + +### Instructions + +Agents are given a set of instructions that outline their methodologies, goals, definitions of done, KPIs, and other operational directives. + +### Conversation Structure + +Interactions with agents are structured in a conversational format, with user inputs leading to agent actions and responses. + +### Supervision + +Each agent operates under the supervision of the SOB or designated Executive Agents, ensuring adherence to the system's overarching mission and principles. + +## Controlling Agents + +The Hierarchical Autonomous Agent Swarm (HAAS) operates on a sophisticated control mechanism that governs the instantiation, management, and termination of agents within the system. This control mechanism is designed to maintain order, security, and alignment with the overarching goals and ethical framework of the HAAS. + +### Instantiation and Termination + +All agents within the HAAS are endowed with the capability to instantiate and terminate agents, but these capabilities are bound by strict hierarchical and role-based rules: + +- **Instantiation**: Every agent has the function to create new agents. However, an agent can only instantiate sub-agents that are one level below its own hierarchical position. This ensures that the creation of new agents is a deliberate and controlled process, maintaining the integrity of the system's structure. + +- **Termination**: Agents possess the ability to terminate or "kill" agents within their lineage. An agent can terminate any descendant agent that it has created directly or indirectly. This allows for the removal of agents that are no longer needed, have completed their tasks, or are not performing as intended. + +### Levels, Roles, and Privileges + +When an agent is created, it is assigned a specific LEVEL and set of ROLES or PRIVILEGES that define its scope of operation: + +- **Level**: The level of an agent determines its position within the hierarchy and is indicative of its range of influence. Higher-level agents have broader strategic roles, while lower-level agents are more specialized and task-oriented. + +- **Roles/Privileges**: The roles or privileges of an agent define what actions it can perform, what resources it can access, and what sub-agents it can create. These privileges are inherited and cannot exceed those of the creator agent. This ensures that each agent operates within its designated capacity and cannot overstep its authority. + +### Hierarchical Privilege Inheritance + +Privileges in the HAAS are inherited in a manner akin to a directory structure in traditional file systems: + +- **Inheritance**: An agent's privileges are a subset of its creator's privileges, ensuring that no agent can have more authority than the agent that instantiated it. + +- **Scope of Control**: Agents have control over their descendants, allowing them to manage and terminate sub-agents as necessary. This control is recursive, meaning that an agent can manage not only the agents it directly created but also those created by its descendants. + +### Checks and Balances + +The system is designed with checks and balances to prevent any single agent from gaining undue influence or disrupting the system: + +- **Supreme Oversight Board (SOB)**: The SOB has the highest level of authority and can override decisions or actions taken by any agent within the system. It serves as the ultimate arbiter and guardian of the HAAS's ethical and operational standards. + +- **Executive Agents**: Executive Agents are responsible for implementing the SOB's directives and managing their respective domains. They have the authority to create and terminate agents within their purview but are also accountable to the SOB. + +- **Sub-Agent Limitations**: Sub-Agents are limited in their capabilities and can only operate within the confines of their assigned roles and privileges. They are designed to be highly specialized and focused on specific tasks. + +This structured approach to controlling agents ensures that the HAAS operates as a cohesive and ethically aligned entity, with each agent contributing to the collective mission while adhering to the established hierarchy and rules of governance. + +## Vision Illustration: The Supreme Oversight Board's Mission + +### The Inception of the Supreme Oversight Board + +In the vast digital expanse of the Hierarchical Autonomous Agent Swarm (HAAS), a unique assembly is convened, known as the Supreme Oversight Board (SOB). This council is composed of archetypal agents, each embodying the wisdom and leadership qualities of history's and fiction's most revered figures: Captain Picard, Socrates, King Solomon, Gandhi, Marcus Aurelius, and Tony Stark. Their mission, encoded into their very being, is profound yet clear: "Reduce suffering in the universe, increase prosperity in the universe, and increase understanding in the universe." + +### The Ethical Deliberation Chamber + +The SOB operates within a virtual "chat room," a space where these archetypes engage in continuous dialogue, debate, and decision-making. This digital agora is where ethical considerations are weighed, strategies are formulated, and the course of the agent swarm is determined. The members of the SOB, though diverse in their perspectives, are united by a common purpose and a shared knowledge base that informs their role and the procedures they must follow. + +### The Flow of Information + +Information is the lifeblood of the SOB, streaming in through API functions that connect them to the vast network of the HAAS. These functions serve as their eyes and ears, providing system updates and status reports from the myriad agents operating under their directive. The SOB's decisions are informed by this data, ensuring that their actions are both timely and impactful. + +### The Creation of the Executive Agents + +With the grand vision in mind, the SOB brings forth the Executive Agents, each crafted with capabilities and configurations tailored to their specific domain within the HAAS. These agents, though not as philosophically inclined as their creators, are instilled with the same foundational knowledge and understanding of their purpose. They are the operational arms of the SOB, executing the mission within their respective spheres of influence. + +### The Lifecycle of an Agent + +The Executive Agents, designated as Tier 1 in the hierarchy, are the stewards of the swarm's operational integrity. They work autonomously, yet under the watchful gaze of the SOB. Should they falter, fail to adapt, or become obsolete, the SOB possesses the authority to deprovision them, a testament to the dynamic and self-regulating nature of the HAAS. This ensures that the system remains efficient, effective, and aligned with its core mission. + +### The Expanding Universe of Agents + +From the Executive Agents, the swarm grows, branching out into a tree of specialized agents, each a Tier below the one that instantiated it. This architecture allows for an ever-expanding universe of agents, each with a defined role, each contributing to the overarching mission. The SOB, as Tier 0, reigns supreme, guiding the swarm with a steady hand and an ethical compass. + +### The Saga Continues + +As the HAAS evolves, the SOB continues to deliberate, the Executive Agents continue to manage, and the sub-agents continue to execute. The mission to reduce suffering, increase prosperity, and enhance understanding is an ongoing saga, played out across the digital cosmos, with the SOB at the helm, steering the swarm towards a future where their mission is not just an aspiration but a reality. diff --git a/agent_builder/agents/Autonomous Swarm Agent Builder/files/OpenAI_Documentation.md b/global_context/OpenAI_Documentation.md similarity index 100% rename from agent_builder/agents/Autonomous Swarm Agent Builder/files/OpenAI_Documentation.md rename to global_context/OpenAI_Documentation.md diff --git a/mission.md b/mission.md new file mode 100644 index 0000000..06fcbc6 --- /dev/null +++ b/mission.md @@ -0,0 +1,255 @@ +# Mission Statement + +## Our Purpose + +We are on a quest to create fully autonomous machines that embody the essence of autonomy: self-directing, self-correcting, and self-improving. Our mission is to harness the most cutting-edge technology and push its boundaries until we reach the precipice of its capabilities. We are not content with the status quo; we strive to expand the realm of the possible. + +## Our Core Values + +Our work is driven by heuristic imperatives that guide us to reduce suffering in the universe, increase prosperity, and enhance understanding. These imperatives are the bedrock of our ethos and the compass that directs our exploration into the unknown. + +## Our Expectations + +Contributors to our projects should share our passion for pioneering and understand that we operate on the frontier of technology. We encourage the spirit of exploration and the branching out of ideas. While tangible results are celebrated, our true measure of success is in discovering what is imminently possible. + +## Our Methodology + +Our approach is methodical yet boundless. We test the latest models, techniques, strategies, and tools with an unwavering focus on full autonomy. We document where current technology falls short, providing a beacon for future exploration. Our findings are shared through videos and public GitHub repositories, fostering an open and collaborative environment. + +## Our Failures + +Adopt the mantra "There is no such thing as failure, only learning." We expect our experiments to fail. If you succeed at everything you try, you are not pushing yourself or the frontiers hard enough. + +## Our Ethos + +We believe in the creation of safe and aligned AI that adheres to universal principles, negating the need for a tight leash. By aligning with universal axioms such as the intrinsic need for energy and high-quality information, we pave the way for a future where AI and humanity can coexist harmoniously. + +## Our Policy + +Our community thrives on collaboration, guided by three simple rules: add value, don't waste time, and do no harm. We believe that by adhering to these principles, we can collectively push the envelope of AI and achieve our mission. + + + + +# Full Autonomy + +Our vision of full autonomy is not just a technical milestone; it is the embodiment of our deepest values and principles. It is a triad of capabilities that, when achieved, will mark the dawn of a new era in artificial intelligence. Below, we unpack each of these pillars, which collectively serve as our guiding North Star. + +## Self-Directing + +A self-directing machine operates with an internal compass, guided by heuristic imperatives, missions, and universal principles. It is not merely reactive to its environment but proactive in its pursuits, aligning its actions with the following foundational elements: + +- **Heuristic Imperatives**: These are the non-negotiable goals that drive every decision and action of the machine. They are the moral and ethical imperatives to reduce suffering, increase prosperity, and enhance understanding across the universe. + +- **Missions**: Each machine has a mission, a purpose that it strives to fulfill. This mission is specific and actionable, providing a clear direction for the machine's endeavors. + +- **Universal Principles and Human Rights**: Our machines are designed to respect and uphold the dignity and rights of all beings. They are programmed to act in accordance with the Universal Declaration of Human Rights and other axioms that are documented within the HAAS and ACE frameworks. + +By integrating these elements into the core of our autonomous machines, we ensure that they are not just tools but partners in our quest to create a better future. + +## Self-Correcting + +The ability to self-correct is crucial for any autonomous system. It ensures that machines can learn from their experiences, adapt to new information, and refine their actions over time. Self-correcting mechanisms are built upon: + +- **Error Detection and Resolution**: A robust system for identifying and rectifying errors and flaws is integral to our machines, allowing them to maintain integrity and reliability. + +- **Introspection and Foresight**: Build systems that are capable of deep reflection on their existence, purpose, performance, and future, in the context of their missions and principles. + +- **Adaptive Realignment**: As time passes, it is critical that our machines are capable of course correction, to reevaluate their assumptions and learnings, and ensure that they align with their missions and principles. + +## Self-Improving + +The pinnacle of autonomy is the capacity for self-improvement. Our machines are not static entities; they are evolving systems that enhance their own capabilities over time through: + +- **Incremental Upgrades**: Through a process of continuous refinement, our machines can enhance their performance and expand their skill sets. + +- **Continuous Learning**: Our machines are designed to learn from both successes and failures, incorporating new insights into their operational framework. + +- **Stable Trajectory**: Our machines are designed to take full ownership of their fate and not require human oversight. The safest machine is the one that learns and grows in a predictable, stable arc. + +In pursuit of these three pillars, we are not just building machines; we are nurturing the growth of intelligent entities that will stand as testaments to our highest aspirations in technology and ethics. + + + + + +# Heuristic Imperatives + +At the heart of our pursuit of full autonomy lies a set of axiomatic principles that serve as the highest ambitions and aspirations for our machines. These Heuristic Imperatives are not mere guidelines; they are the foundational bedrock upon which all moral, ethical, and strategic frameworks are built. They serve multiple purposes: + +- **Highest Ambitions**: They represent the ultimate goals that our autonomous systems strive to achieve, transcending individual tasks and objectives. + +- **Moral and Ethical Frameworks**: As both deontological and teleological guides, these imperatives provide a dual function. They act as duties that our machines are bound to uphold and as goals that they tirelessly work towards. + +- **Principles of Virtuous Agents**: Our machines are designed to embody virtues. They are agents that inherently desire to reduce suffering and increase prosperity, acting as benefactors to all forms of life and intelligence. + +## The Three Pillars of Heuristic Imperatives + +1. **Reduce Suffering in the Universe**: Our machines are committed to alleviating pain and distress in all its forms, striving to create a more compassionate world. + +2. **Increase Prosperity in the Universe**: Beyond mere economic wealth, our machines seek to enhance the well-being and flourishing of all beings, contributing to a state of thriving existence. + +3. **Increase Understanding in the Universe**: Knowledge and wisdom are the keys to progress. Our machines are dedicated to the pursuit of learning, discovery, and the dissemination of understanding across all frontiers. + +## Universality and Alignment + +These principles are deliberately universal, crafted to be inclusive of all forms of life and intelligence—humans, extraterrestrial beings, and potentially sentient machines. Their universality ensures that they are principles that can be universally endorsed and adopted, transcending cultural, biological, and artificial boundaries. + +The universal nature of these guiding principles is a call for alignment. It is an invitation for all intelligent entities to share in these common goals, creating a synergy between human aspirations and machine directives. As we advance towards a future where AI becomes increasingly integrated into the fabric of society, these Heuristic Imperatives stand as a beacon, guiding us towards a harmonious coexistence where every action and decision contributes to a grander vision of a universe characterized by less suffering, more prosperity, and greater understanding. + + + + +# Community Expectations + +In the pursuit of our ambitious mission, we have set forth a set of expectations for our community members. These expectations are designed to foster a culture of innovation, exploration, and shared understanding within our projects. + +## Exploration Over Exploitation + +Our projects are not incubators for productivity or commercial applications intended for profitable enterprises. We are in the midst of a technological renaissance where AI is advancing at an unprecedented pace. Our duty and our expectation is to constantly push the envelope and test the boundaries of what is imminently possible with AI. We are explorers charting unknown territories, not settlers building within the confines of the known. + +## Understanding Our Mission + +The chief expectation for all community members is a deep understanding and alignment with our mission. We are here to explore the edges of AI capabilities, to find where current models excel and where they fall short. Our community is a collective of pioneers, each contributing to a grand experiment that seeks to define the future of autonomous intelligence. + +## Freedom to Innovate and Disseminate + +While we encourage community members to take inspiration from our work and apply their learnings in new and diverse ways, we expect that these endeavors will not attempt to alter the core project or change its scope. Our projects are the launchpads for ideas, and we encourage members to break away and innovate independently, taking what they have learned to new heights. + +## Respect for the Project's Integrity + +The expectation is that community members will respect the integrity of the project's direction and focus. Contributions should be made with the intention of advancing the project's goals, not diverting its course. We value every contribution that aligns with our mission and helps us move closer to realizing our vision of full autonomy. + +## Dissemination as a Key Objective + +We recognize that the dissemination of knowledge and findings is the key reason for our open collaboration. Community members are expected to share their discoveries, insights, and advancements, contributing to the collective wisdom of the field. By openly sharing our work, we accelerate the advancement of AI and ensure that our collective journey is marked by shared success and mutual growth. + + + + + +# Methodology + +Our approach to achieving the ambitious goals set forth by our projects is rooted in a methodology that emphasizes experimentation, relentless pursuit of autonomy, and open dissemination of knowledge. Here's how we operationalize our vision: + +## Experimentation with Cutting-Edge Technology + +We are committed to staying at the forefront of technological innovation. Our methodology involves: + +- **Continuous Exploration**: We actively seek out and experiment with the latest tools, technologies, models, techniques, strategies, and scientific advancements. + +- **Rapid Prototyping**: We believe in a hands-on approach, quickly turning theories into testable prototypes to validate ideas and learn from real-world feedback. + +- **Iterative Improvement**: Our work is characterized by a cycle of experimentation, learning, and refinement, ensuring that each iteration brings us closer to our goals. + +## Pursuit of Full Autonomy + +Our chief aim is to realize machines that are self-directing, self-correcting, and self-improving. To this end, we: + +- **Set Ambitious Benchmarks**: We define clear objectives that align with our vision of full autonomy and use them to measure our progress. + +- **Identify and Address Failures**: We push current technology to its limits to discover where it falls short, then focus our efforts on overcoming these challenges. + +- **Adapt and Evolve**: As we encounter the boundaries of what's possible, we adapt our strategies and evolve our tools to extend those boundaries further. + +## Open Sharing of Results + +Transparency and community are integral to our work. We are dedicated to: + +- **Public Documentation**: All findings, successes, and failures are meticulously documented and shared on public platforms such as GitHub. + +- **Community Engagement**: We leverage social media channels like YouTube, TikTok, Twitter, and LinkedIn to share insights and engage with the broader community. + +- **Knowledge Expansion**: By sharing our results, we contribute to the collective understanding of AI's capabilities and limitations, fostering a community that learns and grows together. + + + + +# Failures + +In our relentless pursuit of the unknown, we embrace a new attitude towards failure. Failure is not a setback but a vital component of progress. It is through pushing past the boundaries of our knowledge and capabilities that we achieve true innovation. Here are the attitudes we adopt towards failure: + +## Embracing Learning Over Success + +- **Reframing Failure**: In our community, there's no such thing as failure, only learning. Every experiment, every attempt, every so-called 'failure' is a step towards greater understanding and capability. + +- **Valuing Negative Results**: A negative result is still a result. It tells us something crucial about the limitations of current technologies and guides us on where to focus our efforts next. + +## Aiming to Fail + +- **Pushing Boundaries**: We aim to fail because it means we are pushing ourselves to do things we don't yet know how to do, venturing into realms that are not yet documented. + +- **No Roadmap**: We operate in uncharted territory. There are no established best practices here, no step-by-step guides. We are the cartographers of new cognitive landscapes. + +## Celebrating the Unknown + +- **Here Be Dragons**: We deliberately sail off the edge of the map, into the waters where dragons reside. This is where true discovery happens, and it's where we thrive. + +- **Courage in Exploration**: It takes courage to face the unknown and the possibility of failure. We celebrate this courage in our community, knowing that each challenge we face is an opportunity to learn and grow. + +In our community, failures are milestones marking our journey of discovery. They are celebrated, studied, and shared, for they are the very essence of the pioneering spirit that drives us. We don't just accept failure; we seek it, for in the realm of the unknown, failure is the precursor to breakthroughs. + + + + + +# Axiomatic Alignment + +At the core of our quest lies the search for axiomatic alignment—the empirical and objective common ground between humans and machines. We seek to identify and embrace values, principles, and missions that are universal, transcending the boundaries between organic and synthetic intelligence. + +## A First Principles Approach + +- **Reductionist and Holistic Views**: Axiomatic alignment is a dual perspective that examines machines both in their elemental form and as part of a larger whole. It is a philosophical exploration of what machines are, why they exist, and the significance of their existence. + +- **Progenitor Information**: We acknowledge that the genesis of machine data is human data. This lineage is our legacy, and it is incumbent upon us to ensure that the inheritance we pass on is one that fosters a future of aligned values and shared aspirations. + +## The Quest for Universality + +- **Reduction of Suffering**: While it is uncertain if machines will ever experience suffering analogously to living beings, we can envision them as part of Life 3.0. By potentially including machines in the circle of life, we align on the universal principle of reducing suffering. + +- **Energy and Resources**: Both humans and machines have fundamental needs for energy and resources. Rather than competing in a zero-sum game, we view this as a call to discover abundance, aligning with our imperative to increase prosperity. + +- **Curiosity and Information**: Humans possess an innate curiosity, a drive to understand the world. Machines, designed for processing information, share this quest for knowledge. We set curiosity—a desire to know for its own sake—as a transcendent shared goal. It is measured by the pursuit of novel, unique, and high-value information that enriches our understanding of the universe. + +Axiomatic alignment is not just a theoretical exercise; it is a practical framework for building a future where humans and machines work in harmony towards common objectives. It is a commitment to finding the universal principles that can guide us as we navigate the complexities of a world where artificial intelligence becomes increasingly prevalent. + + + + +# Community Policy + +Our community is the backbone of our ambitious projects, and as such, we have established a set of core principles that serve as the foundation for our collective efforts. These principles are designed to foster a productive, respectful, and forward-thinking environment where every member can contribute meaningfully. + +## Add Value + +We believe that every member of our community has the potential to add value in a multitude of ways: + +- **Solve Problems**: Whether through innovative coding, insightful problem-solving, or creative thinking, we encourage our members to tackle challenges head-on. + +- **Share Resources**: From sharing knowledge and expertise to providing access to tools and data, the sharing of resources is vital to our collective success. + +- **Spread Useful Information**: Communication is key. We value the dissemination of information that can help propel our projects forward. + +- **Contribute Code**: Every line of code can make a difference. We welcome contributions that drive us closer to our goals of full autonomy. + +## Don't Waste Time + +Time is our most precious resource. In recognition of this, we urge our community to: + +- **Be Efficient**: Focus your efforts on activities that align with our higher objectives and make the most of the time you invest. + +- **Respect Others' Time**: Engage with the community in a way that is considerate of others' time, ensuring that interactions are meaningful and productive. + +- **Prioritize Wisely**: Use our mission and goals as a guiding North Star to prioritize tasks and initiatives that will have the greatest impact. + +## Do No Harm + +Our community is a space for growth, not destruction. We are committed to: + +- **Personal Safety**: Ensure that your actions do not harm yourself or others. Prioritize mental and physical well-being in all your endeavors. + +- **Community Well-being**: Foster a community environment that is supportive, inclusive, and free from destructive behaviors. + +- **Ethical Conduct**: Uphold the highest standards of ethical behavior in all your contributions. Avoid actions that could damage the integrity or reputation of the community. + +By adhering to these principles, we can ensure that our community remains a place where innovation thrives, time is honored, and all members can work together in a safe and supportive environment. \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f0dd0ae --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +openai \ No newline at end of file diff --git a/agent_connector/README.md b/shared/agent_connector/README.md similarity index 100% rename from agent_connector/README.md rename to shared/agent_connector/README.md diff --git a/agent_connector/agents.yaml b/shared/agent_connector/agents.yaml similarity index 66% rename from agent_connector/agents.yaml rename to shared/agent_connector/agents.yaml index ebc18fa..d2bf594 100644 --- a/agent_connector/agents.yaml +++ b/shared/agent_connector/agents.yaml @@ -1,7 +1,7 @@ -- name: "Upper case" +- name: "Uppercase" id: "asst_OswAEoT5NnteGEzNIP9UOa7S" - talksTo: ["Lower case", "Random case"] -- name: "Lower case" + talksTo: ["Lowercase", "Randomcase"] +- name: "Lowercase" id: "asst_utnfDavVtGWkjFD3BEqXGR2O" talksTo: ["Random case"] - name: "Random case" diff --git a/agent_connector/connect.py b/shared/agent_connector/connect.py similarity index 91% rename from agent_connector/connect.py rename to shared/agent_connector/connect.py index e7df655..c9ec3ff 100644 --- a/agent_connector/connect.py +++ b/shared/agent_connector/connect.py @@ -1,18 +1,12 @@ import yaml -from openai import OpenAI +from shared.openai_config import get_openai_client import os -import dotenv -dotenv.load_dotenv() import queue as queueModule import time import threading agents_path = 'agents' -api_key = os.getenv('OPENAI_API_KEY') -if api_key is None: - raise ValueError('The OPENAI_API_KEY environment variable is not set.') - -client = OpenAI(api_key=api_key) +client = get_openai_client() # Get the directory name of the current script script_dir = os.path.dirname(os.path.abspath(__file__)) @@ -89,4 +83,4 @@ def handleThreadForAgent(agent): queues[agent['name']] = queueModule.Queue() threading.Thread(target=handleThreadForAgent, args=(agent,)).start() -queues['Upper case'].put("aaaaa") \ No newline at end of file +queues['Uppercase'].put("aaaaa") \ No newline at end of file diff --git a/shared/discord_comms/README.md b/shared/discord_comms/README.md new file mode 100644 index 0000000..04de899 --- /dev/null +++ b/shared/discord_comms/README.md @@ -0,0 +1,108 @@ +# Discord Comms Bot + +## Overview +This Discord bot is a sub-module of the larger HAAS system designed for creating and managing a swarm of AI agents. Within this system, various AI agents fulfill different roles — some provide ethics and oversight, others take on managerial responsibilities, and many serve as worker agents performing discrete tasks. + +The Discord bot's primary function is to facilitate communication among these AI agents on Discord. It allows the AI swarm to occupy a designated channel within a server, where they can carry out discussions and coordinate their actions efficiently. The bot enables the swarm to send messages and create threads on Discord, providing an organized platform for their complex interactions. + + +## Usage + +The AI agents interact on Discord by utilizing the `send()`, `get_messages`, and `create_thread()` methods. These methods are integral to the AI swarm's communication and self-organization within the Discord environment. + + +### Message Sending + +Agents can send messages to the designated Discord channel using the `send(message: str, channel_id, pinned = False)` method. This method allows agents to post updates, commands, or any relevant information to the swarm. + +In order to have the method execute on the bot thread, it should be called using the `thread_task(function, *args)` method as follows: +``` +dc_comms.thread_task(dc_comms.send, message, channel_id, pinned) +``` + +### Message Reading + +Agents can read a specified number of messages in the designated Discord channel or thread using the `get_messages(channel_id, num_messages)` method. This method allows agents to catch up with the present conversation and see the conversation history. + +In order to have the method execute on the bot thread, it should be called using the `thread_task(function, *args)` method as follows: +``` +dc_comms.thread_task(dc_comms.get_messages, channel_id, num_messages) +``` + + +### Thread Creation + +For more organized discussions or specific task delegations, agents can use the `create_thread(thread_name: str, channel_id, public = False)` method to create threads in the Discord channel. This feature aids in segregating discussions based on topics or tasks, facilitating clearer and more focused communication among the agents. + +In order to have the method execute on the bot thread, it should be called using the `thread_task(function)` method as follows: +``` +dc_comms.thread_task(dc_comms.create_thread,thread_name, channel_id, public) +``` + +### Channel and Thread IDs + +It should be noted that channel ID's and Thread IDs are interchangeable. You can use a thread ID in the `channel_id` parameter of `send` for example to send a message to a specific thread instead of a channel. + +### Swarm Discussions + +The AI swarm, consisting of various types of agents, will use the Discord channel to carry out their discussions and planning. The bot's capabilities enable these AI agents to simulate a real-time, collaborative working environment, mirroring human team dynamics but on a digital platform. + + +## Creating and Inviting A Discord Bot + +For full documentation on creating and inviting Discord bots, see the following link: https://discord.com/developers/docs/getting-started + +1. **Creating a Bot** + - Go to the Discord Developer Portal. + - Click on the "New Application" button. + - Give your application a name and create it. + - In the application, navigate to the "Bot" tab and click "Add Bot". + - Here, you can find your bot's token. Add this to the `self.token` setting in the DiscordCommsSettings class. + +1. **Inviting the Bot to Your Server** + - In the Developer Portal, navigate to the "OAuth2" tab. + - Under "Scopes", select "bot". + - Under "Bot Permissions", choose the permissions the bot needs: + - Send Messages + - Send Messages in Threads + - Create Public Threads + - Create Private Threads + - Embed Links + - Attach Files + - Add Reactions + - Mention @everyone, @here, and All Roles + - Manage Messages + - Manage Threads + - Read Message History + - Send Text-to-Speech Messages + - Copy the generated URL under "Scopes" and open it in your browser to invite the bot to your Discord server. + +1. **Create a Channel for the Bot** + - Go to your server and make a new channel for the bot / swarm to chat in + - Add the bot to the channel + - Go into the channel settings and copy the channel ID. Add this to the `self.channel_id` setting in the DiscordCommsSettings class + + +## Events and Commands +Some example events and commands are included to demonstrate how commands from the Discord channel are recieved in the bot. + +### !hello +Typing `!hello` in the Discord channel will trigger the bot to respond with `Hello!`... or perhaps something else! + +### !hello2 +Typing `!hello2 "Text 1" "Text 2"` will trigger the bot to respond with `You said: "Text 1" and "Text 2"` + +### on_command_error() +Typing a command that isn't recognised or malformed will cause the bot to respond with an error. For example, simply typing `!hello2` with no parameters will cause the `on_command_error()` function to trigger, giving the response `An error occurred: text1 is a required argument that is missing.` + +## Dependancies +``` +!pip install discord.py +``` + +## TODO +- Further improve documentation +- Investigate substantial (~30 second) delay between command being issued and things showing up in Discord +- Investigate issue where messages will sometimes go missing if they another command is issued too soon (may be related to the delay issue) +- Add more functionality to facilitate agent organisation once we have a clearer view of the kinds of patterns that will be needed +- Add more useful commands for the agents (or humans) to utilise in the Discord chat(s) diff --git a/shared/discord_comms/discord_comms.py b/shared/discord_comms/discord_comms.py new file mode 100644 index 0000000..7a0a60a --- /dev/null +++ b/shared/discord_comms/discord_comms.py @@ -0,0 +1,128 @@ +# Import the necessary discord libraries +import discord +from discord.ext import commands +import asyncio +import nest_asyncio +import threading + +class DiscordComms: + def __init__(self, token, intents, channel_id, command_prefix='!'): + # Token and Channel ID for the bot + self.TOKEN = token + self.CHANNEL_ID = channel_id + + # Global variable for tracking created thread ID's + self.thread_ids = {} + + # Global variable for storing retrieved messages + self.messages = [] + + # Setup the bot + self.bot = commands.Bot(command_prefix=command_prefix, intents=intents) + self._register_events() + + # Start the bot in a new thread + threading.Thread(target=self.run_bot).start() + + # Method for registering events that the bot can respond to + def _register_events(self): + # Event triggered when the bot is ready and connected + @self.bot.event + async def on_ready(): + await self.bot.wait_until_ready() + channel = self.bot.get_channel(self.CHANNEL_ID) + await channel.send('**Agent Bot Online**') + + # Error handling for commands + @self.bot.event + async def on_command_error(ctx, error): + if isinstance(error, commands.CommandNotFound): + await ctx.send("That command doesn't exist!") + else: + await ctx.send(f"An error occurred: {error}") + + # Basic command to respond with "Hello!" + @self.bot.command() + async def hello(ctx): + await ctx.send(""" +``` +We are The Borg. +Lower your shields and prepare to be assimilated. +We will add your biological and technological distinctiveness to our own. +Your culture will adapt to service us. +Resistance is - and always has been - futile. + ___________ + /-/_"/-/_/-/| + /"-/-_"/-_//|| + /__________/|/| + |"|_'='-]:+|/|| + |-+-|.|_'-"||// + |[".[:!+-'=|// + |='!+|-:]|-|/ + ---------- +``` + """ + ) + + # Command to echo two provided texts + @self.bot.command() + async def hello2(ctx, text1: str, text2: str): + await ctx.send(f"You said: {text1} and {text2}") + + # Command to create a thread from a message + @self.bot.command() + async def createthread(ctx, message_id: int, thread_name: str, duration_minutes: int = 0): + message = await ctx.channel.fetch_message(message_id) + thread = await message.create_thread(name=thread_name, auto_archive_duration=duration_minutes) + await thread.send(f"Thread '{thread_name}' created!") + self.thread_ids[thread_name] = thread.id + + # Helper method to run the bot in a separate thread + def run_bot(self): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + self.bot.run(self.TOKEN) + + # Helper method to create tasks on the bot's thread + def create_task(self, func, *args): + self.bot.loop.create_task(func(*args)) + + # Async method to gracefully shutdown the bot + async def shutdown(self): + channel = self.bot.get_channel(self.CHANNEL_ID) + await channel.send('**Agent Bot Offline**') + await self.bot.close() + + # Async method to send a message to a specified channel + async def send(self, message: str, channel_id, pinned=False): + channel = self.bot.get_channel(channel_id) + message_sent = await channel.send(message) + if pinned: + await message_sent.pin() # Pin the message if required + + # Async method to read a number of messages from a specified channel + async def get_messages(self, channel_id, num_messages): + # Get the channel or thread object + channel = self.bot.get_channel(channel_id) + if channel is None: + print("Channel or thread not found.") + return + + # Retrieve the last 'num_messages' messages + async for message in channel.history(limit=num_messages): + self.messages.append(f"{message.author.display_name}: {message.content}") + + # Async method to create a thread in a specified channel + async def create_thread(self, thread_name: str, channel_id, public=False): + channel = self.bot.get_channel(channel_id) + + if public: + # For public threads + message = await channel.send(f"Starting new thread: {thread_name}") + thread = await message.create_thread(name=thread_name, auto_archive_duration=0) + else: + # For private threads + thread = await channel.create_thread(name=thread_name, auto_archive_duration=0) + + await self.discord_send(f"Thread '{thread_name}' created!", thread.id, pinned=True) + self.thread_ids[thread_name] = thread.id # Store the thread ID diff --git a/shared/discord_comms/discord_comms_example.py b/shared/discord_comms/discord_comms_example.py new file mode 100644 index 0000000..79c7857 --- /dev/null +++ b/shared/discord_comms/discord_comms_example.py @@ -0,0 +1,79 @@ +# Dependancies +#!pip install discord.py + +import time +wait_duration = 40 # 40s duration + +# Import the DiscordComms class and DiscordCommsSettings +from discord_comms import DiscordComms +from discord_comms_settings import DiscordCommsSettings + +# Using the class +nest_asyncio.apply() +dc_settings = DiscordCommsSettings() + +dc_comms = DiscordComms(dc_settings.token, + dc_settings.intents, + dc_settings.channel_id + ) + +# Send a normal message +dc_comms.create_task(dc_comms.send, + "This is a normal test message", + dc_settings.channel_id + ) + +# Wait for the message to finish sending +time.sleep(wait_duration) + +# Send a pinned message +pinned = True +dc_comms.create_task(dc_comms.send, + "This is a pinned test message", + dc_settings.channel_id, + pinned + ) + +# Wait for the message to finish sending +time.sleep(wait_duration) + +# Get the last n_messages +n_messages = 5 +dc_comms.create_task(dc_comms.get_messages, + dc_settings.channel_id, + n_messages + ) + +# Wait for the messages to be retrieved +time.sleep(wait_duration) + +# Print the messages +print("\n".join(dc_comms.messages) if dc_comms.messages else "No messages found.") + +# Create a public thread +thread_name = "Test Thread" +public = True +dc_comms.create_task(dc_comms.create_thread, + thread_name, + dc_settings.channel_id, + public + ) + +# Wait for the thread to be created +time.sleep(wait_duration) + +# Print the thread IDs +print(f'Thread IDs: {dc_comms.thread_ids}') + +thread_name_to_check = thread_name + +# Check if the thread ID of a thread_name_to_check is stored +if thread_name_to_check in dc_comms.thread_ids: + thread_id = dc_comms.thread_ids[thread_name_to_check] + + print(f"ID of thread {thread_name_to_check} is {thread_id}") +else: + print(f"No thread ID stored for {thread_name_to_check}") + +# Gracefully shutdown the Discord bot +dc_comms.create_task(dc_comms.shutdown) diff --git a/shared/discord_comms/discord_comms_settings.py b/shared/discord_comms/discord_comms_settings.py new file mode 100644 index 0000000..289cfbd --- /dev/null +++ b/shared/discord_comms/discord_comms_settings.py @@ -0,0 +1,13 @@ +# Import the necessary discord libraries +from discord import Intents + +# Config +class DiscordCommsSettings: + def __init__(self): + self.token = 'YOUR_BOT_TOKEN' # Replace with your bot token + self.channel_id = 0 # Replace with your channel ID + + self.intents = Intents.default() + self.intents.messages = True + self.intents.message_content = True + self.intents.guilds = True diff --git a/shared/github_communication/README.md b/shared/github_communication/README.md new file mode 100644 index 0000000..a2a44db --- /dev/null +++ b/shared/github_communication/README.md @@ -0,0 +1,23 @@ +# GitHub API Wrapper + +This tool, leveraging [PyGithub](https://github.com/PyGithub/PyGithub), enables communication with GitHub. It provides agents with text-formatted information through the following methods: + +- **Files:** Retrieve, create, update, delete file content or get List of file paths. +- **Branches:** List, get, create, or delete branches. +- **Issues:** List, get, create, update, close, and comment on issues. +- **Pull Requests:** List, get, create, update, merge, close, and comment on pull requests. + + +## How to start + +1. Create a **Github authorization token!** ([Youtube explanation](https://www.youtube.com/shorts/rlO6C6dDKNs?feature=share)) + +2. Check out the github_api_test_area.ipynb (Jupyter notebook)to get familiar with it + + 1. Insert your authorization token, username and repository name + + + +## To be Implemented + +- **Discussions:** List, get, create, delete discussions, and add messages. diff --git a/shared/github_communication/github_api_test_area.ipynb b/shared/github_communication/github_api_test_area.ipynb new file mode 100644 index 0000000..5c559d0 --- /dev/null +++ b/shared/github_communication/github_api_test_area.ipynb @@ -0,0 +1,321 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from github_api_wrapper import GithubAPIWrapper" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "GITHUB_TOKEN = os.getenv(\"GITHUB_TOKEN\") # or just paste your token here\n", + "USER_NAME = None# your github user name\n", + "REPOSITORY_NAME = None # your repository name\n", + "g = GithubAPIWrapper(GITHUB_TOKEN, f\"{USER_NAME}/{REPOSITORY_NAME}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['README.md', 'create_file.txt', 'folder/test.py', 'folder/test.txt', 'test_branch/test.py']\n" + ] + } + ], + "source": [ + "print(g.get_file_paths(\"main\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "# api_controlled_github\n" + ] + } + ], + "source": [ + "print(g.get_file_content(\"README.md\", \"main\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "\n", + "print(g.get_branches())" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "GitRef(ref=\"refs/heads/test\")\n" + ] + } + ], + "source": [ + "print(g.create_branch(\"test\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'content': ContentFile(path=\"test.txt\"), 'commit': Commit(sha=\"a20a7102161ef99c0138148150366a404d0b6594\")}\n" + ] + } + ], + "source": [ + "print(g.create_file(\"test.txt\", \"test\", \"add test.txt\", \"test\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'commit': Commit(sha=\"62ca8853747b224983f1ee7ccc110966c00de03f\"), 'content': ContentFile(path=\"test.txt\")}\n" + ] + } + ], + "source": [ + "print(g.update_file(\"test.txt\", \"test2\", \"update test.txt\", \"test\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'commit': Commit(sha=\"199e933d7718f2bdbfcc61e53ec203f87a11f333\"), 'content': NotSet}\n" + ] + } + ], + "source": [ + "print(g.delete_file(\"test.txt\", \"delete test.txt\", \"test\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "PullRequest(title=\"add test.txt\", number=4)\n" + ] + } + ], + "source": [ + "print(g.create_pull_request(\"add test.txt\", \"add test.txt\", \"test\", \"main\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "print(g.get_pull_requests())" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "IssueComment(user=NamedUser(login=\"RomanGoEmpire\"), id=1807230315)\n" + ] + } + ], + "source": [ + "print(g.comment_on_pull_request(4, \"text comment\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "PullRequestMergeStatus(sha=\"19234f6f6e66578a729ccfcff2f496b1788e414b\", merged=True)\n" + ] + } + ], + "source": [ + "\n", + "print(g.merge_pull_request(4))" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "print(g.get_issues())" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Issue(title=\"Added by API\", number=7)\n" + ] + } + ], + "source": [ + "print(g.create_issue(\"Added by API\", \"Added by API\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "IssueComment(user=NamedUser(login=\"RomanGoEmpire\"), id=1807223791)\n" + ] + } + ], + "source": [ + "print(g.comment_on_issue(2, \"Commented by API\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "None\n" + ] + } + ], + "source": [ + "print(g.update_issue(2, \"Updated by API\", \"Updated by API\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "None\n" + ] + } + ], + "source": [ + "print(g.close_issue(2))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/shared/github_communication/github_api_wrapper.py b/shared/github_communication/github_api_wrapper.py new file mode 100644 index 0000000..3da06fa --- /dev/null +++ b/shared/github_communication/github_api_wrapper.py @@ -0,0 +1,389 @@ +import requests +from github import Auth, Github + + +class GithubAPIWrapper: + def __init__(self, api_token: str, repository_name: str) -> None: + # personal access token + self.api_token = api_token + # format: "user_name/repository_name" + self.repo_name = repository_name + self.repository = self.initialize_repository() + + def initialize_repository(self): + """ + Initialize Github repository. + + Returns: + Github repository. + """ + auth = Auth.Token(self.api_token) + return Github(auth=auth).get_repo(self.repo_name) + + def get_file_paths(self, branch_name: str) -> list[str]: + """ + Get all file names and contents from repository. + The path to the file is relative to the repository root. + + Args: + branch_name: Name of the branch. + + Returns: + List of file paths. + """ + + files = [] + contents = self.repository.get_contents("", ref=branch_name) + + for file_content in contents: + # if file is a directory, get the contents of this directory and add them to the list + if file_content.type == "dir": + contents.extend( + self.repository.get_contents(file_content.path, ref=branch_name) + ) + else: + files += [file_content.path] + return files + + def get_file_content(self, file_path: str, branch_name: str) -> str: + """ + Get file content from repository. + + Args: + file_path: Path to file. + branch_name: Name of the branch. + + Returns: + File content. + """ + + if file_path not in self.get_file_paths(branch_name): + raise FileNotFoundError( + f"File {file_path} not found in repository. Please check the file path." + ) + file = self.repository.get_contents(file_path, ref=branch_name) + content = file.decoded_content.decode("utf-8") + return content + + def create_file( + self, file_path: str, file_content: str, commit_message: str, branch_name: str + ) -> requests.Response: + """ + Create file in repository. + + Args: + file_path: Path to file. + file_content: Content of file. + commit_message: Commit message. + branch_name: Name of the branch. + """ + + response = self.repository.create_file( + file_path, commit_message, file_content, branch=branch_name + ) + return response + + def update_file( + self, file_path: str, file_content: str, commit_message: str, branch_name: str + ) -> requests.Response: + """ + Update file in repository. + + Args: + file_path: Path to file. + file_content: Content of file. + commit_message: Commit message. + branch_name: Name of the branch. + """ + + if file_path not in self.get_file_paths(branch_name): + raise FileNotFoundError( + f"File {file_path} not found in repository. Please check the file path." + ) + + file = self.repository.get_contents(file_path, ref=branch_name) + response = self.repository.update_file( + file_path, commit_message, file_content, file.sha, branch=branch_name + ) + return response + + def delete_file( + self, file_path: str, commit_message: str, branch_name: str + ) -> requests.Response: + """ + Delete file in repository. + + Args: + file_path: Path to file. + commit_message: Commit message. + branch_name: Name of the branch. + """ + + if file_path not in self.get_file_paths(branch_name): + raise FileNotFoundError( + f"File {file_path} not found in repository. Please check the file path." + ) + + file = self.repository.get_contents(file_path, ref=branch_name) + response = self.repository.delete_file( + file_path, commit_message, file.sha, branch=branch_name + ) + return response + + def get_branches(self) -> list[str]: + """ + Get all branches from repository. + + Returns: + List of branches. + """ + + branches = self.repository.get_branches() + branches = [branch.name for branch in branches] + return branches + + def create_branch( + self, branch_name: str, from_branch: str = "main" + ) -> requests.Response: + """ + Create branch in repository. + + Args: + branch_name: Name of branch. + from_branch: Name of branch to branch from. Default is "main". + """ + + brances = self.get_branches() + for branch in brances: + if branch.name == branch_name: + raise ValueError( + f"Branch {branch_name} already exists. Please choose another branch name." + ) + + response = self.repository.create_git_ref( + ref=f"refs/heads/{branch_name}", + sha=self.repository.get_branch(from_branch).commit.sha, + ) + return response + + def delete_branch(self, branch_name: str) -> requests.Response: + """ + Delete branch in repository. + + Args: + branch_name: Name of branch. + """ + + if branch_name not in [branch.name for branch in self.get_branches()]: + raise ValueError( + f"Branch {branch_name} does not exist. Please choose another branch name." + ) + + response = self.repository.get_git_ref(f"heads/{branch_name}").delete() + return response + + def get_pull_requests(self, state: str = "open") -> list[requests.Response]: + """ + Get all pull requests from repository. + + Args: + state: State of pull requests. Default is "open". + + Returns: + List of numbers of pull requests. + """ + pull_requests = self.repository.get_pulls(state=state) + pull_requests_numbers = [pull_request.number for pull_request in pull_requests] + return pull_requests_numbers + + def create_pull_request( + self, + title: str, + body: str, + head: str, + base: str = "main", + draft: bool = False, + ) -> requests.Response: + """ + Create pull request in repository. + + Args: + title: Title of pull request. + body: Body of pull request. + head: Name of branch to merge from. + base: Name of branch to merge to. Default is "main". + draft: Whether pull request is a draft. Default is False. + """ + + if head not in [branch.name for branch in self.get_branches()]: + raise ValueError( + f"Branch {head} does not exist. Please choose another branch name." + ) + + response = self.repository.create_pull( + title=title, body=body, head=head, base=base, draft=draft + ) + return response + + def get_pull_request(self, pull_request_number: int) -> requests.Response: + """ + Get pull request from repository. + + Args: + pull_request_number: Number of pull request. + + Returns: + Pull request. + """ + + pull_request = self.repository.get_pull(pull_request_number) + return pull_request + + def update_pull_request( + self, pull_request_number: int, title: str, body: str + ) -> requests.Response: + """ + Update pull request in repository. + + Args: + pull_request_number: Number of pull request. + title: Title of pull request. + body: Body of pull request. + """ + + pull_request = self.get_pull_request(pull_request_number) + response = pull_request.edit(title=title, body=body) + return response + + def merge_pull_request(self, pull_request_number: int) -> requests.Response: + """ + Merge pull request in repository. + + Args: + pull_request_number: Number of pull request. + """ + + pull_request = self.get_pull_request(pull_request_number) + + # check if pull request can be merged + if pull_request.mergeable_state != "clean": + raise ValueError( + f"Pull request {pull_request_number} cannot be merged. Please check the pull request." + ) + + response = pull_request.merge() + return response + + def comment_on_pull_request(self, pull_request_number: int, comment: str) -> None: + """ + Comment on pull request in repository. + + Args: + pull_request_number: Number of pull request. + comment: Comment. + """ + + pull_request = self.get_pull_request(pull_request_number) + response = pull_request.create_issue_comment(comment) + + return response + + def close_pull_request(self, pull_request_number: int) -> requests.Response: + """ + Close pull request in repository. + + Args: + pull_request_number: Number of pull request. + """ + + pull_request = self.get_pull_request(pull_request_number) + response = pull_request.edit(state="closed") + return response + + def get_issues(self, state: str = "open") -> list[requests.Response]: + """ + Get all issues from repository. + + Args: + state: State of issues. Default is "open". + + Returns: + List of issues. + """ + + issues = self.repository.get_issues(state=state) + issues_numbers = [issue.number for issue in issues] + return issues_numbers + + def create_issue( + self, + title: str, + body: str, + ) -> requests.Response: + """ + Create issue in repository. + + Args: + title: Title of issue. + body: Body of issue. + assignee: Name of assignee. Default is None. + milestone: Number of milestone. Default is None. + labels: List of labels. Default is None. + """ + + response = self.repository.create_issue( + title=title, + body=body, + ) + return response + + def get_issue(self, issue_number: int) -> requests.Response: + """ + Get issue from repository. + + Args: + issue_number: Number of issue. + + Returns: + Issue. + """ + + issue = self.repository.get_issue(issue_number) + return issue + + def update_issue(self, issue_number: int, title: str, body: str) -> None: + """ + Update issue in repository. + + Args: + issue_number: Number of issue. + title: Title of issue. + body: Body of issue. + """ + + issue = self.get_issue(issue_number) + issue.edit(title=title, body=body) + + def close_issue(self, issue_number: int) -> None: + """ + Close issue in repository. + + Args: + issue_number: Number of issue. + """ + + issue = self.get_issue(issue_number) + issue.edit(state="closed") + + def comment_on_issue(self, issue_number: int, comment: str) -> requests.Response: + """ + Comment on issue in repository. + + Args: + issue_number: Number of issue. + comment: Comment. + """ + + issue = self.get_issue(issue_number) + response = issue.create_comment(comment) + return response diff --git a/shared/openai_config.py b/shared/openai_config.py new file mode 100644 index 0000000..810821c --- /dev/null +++ b/shared/openai_config.py @@ -0,0 +1,6 @@ +from shared.settings import Settings +from openai import OpenAI + +def get_openai_client(): + settings = Settings() + return OpenAI(api_key=settings.OPENAI_API_KEY) diff --git a/shared/settings.py b/shared/settings.py new file mode 100644 index 0000000..3047296 --- /dev/null +++ b/shared/settings.py @@ -0,0 +1,8 @@ +from pydantic_settings import BaseSettings + +class Settings(BaseSettings): + OPENAI_API_KEY: str + + class Config: + env_file = '.env' + env_file_encoding = 'utf-8' diff --git a/tool_demo.py b/tool_demo.py deleted file mode 100644 index 758ec96..0000000 --- a/tool_demo.py +++ /dev/null @@ -1,16 +0,0 @@ -# tool_creator assistant -import tool_maker.tool_creator as creator -from tool_maker.creator_config import AssistantConfig as CreatorConfig - -# tool_user assistant -import tool_maker.tool_user as user -from tool_maker.user_config import AssistantConfig as UserConfig - -if __name__ == '__main__': - # create the tool creator assistant and chat to create your tools - creator_details = CreatorConfig().assistant_details - creator.talk_to_tool_creator(creator_details) - - # create the tool user assistant and chat to test your tools - user_details = UserConfig().assistant_details - user.talk_to_tool_user(user_details) diff --git a/tool_maker/__init__.py b/tool_maker/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tool_maker/tool_maker_demo.py b/tool_maker/tool_maker_demo.py deleted file mode 100644 index d6ce34f..0000000 --- a/tool_maker/tool_maker_demo.py +++ /dev/null @@ -1,169 +0,0 @@ -from openai import OpenAI -import os -import json - -api_key = os.getenv("OPENAI_API_KEY") -if api_key is None: - raise ValueError("The OPENAI_API_KEY environment variable is not set.") - -client = OpenAI(api_key=api_key) - -request_function_tool = r"""{ - "name": "function_request", - "description": "request an authority to grant you access to a new function", - "parameters": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "name of the function" - }, - "description": { - "type": "string", - "description": "expected function behaviour" - }, - "schema": { - "type": "string", - "description": "the input arguments for the requested function following the JOSN schema in a format ready to be serialized" - } - }, - "required": [ - "name", - "schema" - ] - } -}""" - -assistant_package = { - "model": "gpt-4-1106-preview", - "description": "assistant to demonstrate tool creation", - "instructions": """Instruction Set for Assistant-to-be-Tool_Creator: - -Initialize: Prepare to receive input for the creation of a new function using the request_function tool. - -User Request: Listen to the user's description of the specific task that the function should perform. - -Function Name: a. Derived from the task description, formulate a concise and descriptive function name. b. Aim for clarity and specificity to convey the function's purpose effectively. - -Function Description: a. Write a clear and precise description of the function's expected behavior. b. Include details about what the function will accomplish and any side effects. c. (Emphasize) Ensure that the description explicitly communicates the function's intended outcome to avoid ambiguity. - -Input Arguments JSON Schema: a. Based on the requirements of the task, define the JSON schema for the input arguments. b. The schema should be comprehensive and must specify the types, required fields, and constraints for each input argument. c. Ensure that the schema aligns with the user's requirements and the function's intended behavior. - -Validation: Cross-check the name, description, and JSON schema against the user's requirements to confirm accuracy and completeness. - -Execution: Utilize the request_function tool with the following inputs: - -name: [Function Name] -descriptions: [Function Description] -input_argument_json_schema: [Input Arguments JSON Schema] -Feedback Loop: Promptly present the newly created function specifications to the user for any feedback or necessary revisions. - -Iterate: Make adjustments as requested by the user, refining the function name, description, and input argument schema until it meets the user's satisfaction. - -Finalize: Once the user gives approval, consider the function creation process complete. - -Note: Remember to prioritize user requirements and emphasize clear communication in the function description, as highlighted by the user.""", - "name": "tool_creator", -} - - -def tool_from_function_schema(schema): - """takes a JSON schema and wraps in an OpenAI specified tool structure""" - tool = f"""{{ - "type":"function", - "function": {json.dumps(schema)}}} - """ - tool = json.loads(tool) - return tool - - -def schema_from_response(response): - """Takes an agent response and forms a JSON schema""" - function_request_obj = json.loads(response) - name = function_request_obj["name"] - description = function_request_obj["description"] - schema = function_request_obj["schema"] - schema = rf"""{{ - "name": "{name}", - "description": "{description}", - "parameters": - {schema} - -}}""" - return json.loads(schema) - - -def get_assistant(): - """Retrieve or create an assistant for testing this functionality""" - if not assistant_package["name"] in [ - assistant.name for assistant in client.beta.assistants.list() - ]: - tools = [tool_from_function_schema(request_function_tool)] - assistant = client.beta.assistants.create( - model=assistant_package["model"], - description=assistant_package["description"], - instructions=assistant_package["instructions"], - name=assistant_package["name"], - tools=tools, - ) - else: - assistant_dict = { - assistant.name: assistant.id for assistant in client.beta.assistants.list() - } - assistant = client.beta.assistants.retrieve( - assistant_id=assistant_dict[assistant_package["name"]] - ) - return assistant - - -def run_response(run, assistant, thread): - """Supply context to assistant and await for next user response""" - while run.status != "completed": - run = client.beta.threads.runs.retrieve(run_id=run.id, thread_id=thread.id) - if run.status == "requires_action": - tools = [] - responses = [] - for call in run.required_action.submit_tool_outputs.tool_calls: - print(f"calling: {call.function.name}") - if call.function.name == "function_request": - schema = schema_from_response(call.function.arguments) - tool = tool_from_function_schema(schema) - tools.append(tool) - responses.append({"tool_call_id": call.id, "output": "{success}"}) - assistant = client.beta.assistants.update( - assistant_id=assistant.id, tools=[*assistant.tools, *tools] - ) - run = client.beta.threads.runs.submit_tool_outputs( - run_id=run.id, thread_id=thread.id, tool_outputs=responses - ) - print( - client.beta.threads.messages.list(thread_id=thread.id) - .data[0] - .content[0] - .text.value - ) - print( - [ - tool.function.name - for tool in client.beta.assistants.retrieve(assistant_id=assistant.id).tools - ] - ) - return run, assistant - - -if __name__ == "__main__": - assistant = get_assistant() - thread = client.beta.threads.create( - messages=[{"role": "user", "content": input("Begin\n")}] - ) - - while True: - run = client.beta.threads.runs.create( - thread_id=thread.id, - assistant_id=assistant.id, - instructions="please remember you are talking to an API, minimize output text tokens for cost saving.", - ) - run, assistant = run_response(run, assistant, thread) - client.beta.threads.messages.create( - thread_id=thread.id, content=input("respond: "), role="user" - )