diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..fbe2d68 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index 44cf959..9e77d01 100644 --- a/.gitignore +++ b/.gitignore @@ -148,7 +148,6 @@ crazy_functions/test_project/pdf_and_word crazy_fun ctions/test_samples # auto -autogpt/ diff --git a/__main__.py b/__main__.py index c29cd1a..0676a9c 100644 --- a/__main__.py +++ b/__main__.py @@ -1,6 +1,4 @@ import os - - import gradio as gr from request_llm.bridge_chatgpt import predict from toolbox import format_io, find_free_port, on_file_uploaded, on_report_generated, get_user_upload, \ @@ -135,36 +133,39 @@ class ChatBot(ChatBotFrame): self.max_length_sl = gr.Slider(minimum=256, maximum=4096, value=512, step=1, interactive=True, label="MaxLength", ) self.models_box = gr.CheckboxGroup(["input加密"], value=["input加密"], label="对话模式") self.md_dropdown = gr.Dropdown(AVAIL_LLM_MODELS, value=LLM_MODEL, label="更换LLM模型/请求源").style(container=False) - gr.Markdown(self.description) + # temp = gr.Markdown(self.description) def draw_goals_auto(self): - with gr.Box(): + with gr.Tab('Ai Prompt'): with gr.Row(): - self.ai_name = gr.Textbox(show_label=False, placeholder="Give AI a name.").style(container=False) + self.ai_name = gr.Textbox(show_label=False, placeholder="给Ai一个名字").style(container=False) with gr.Row(): - self.user_input = gr.Textbox(lines=5, show_label=False, placeholder="Describe your AI's role.").style(container=False) + self.ai_role = gr.Textbox(lines=5, show_label=False, placeholder="请输入你的需求").style(container=False) with gr.Row(): - self.goal_list = gr.Dataframe(headers=['Goals'], interactive=True, - row_count=4, col_count=(1, 'fixed'), type='array') - self.goal_list.style() + self.ai_goal_list = gr.Dataframe(headers=['Goals'], interactive=True, row_count=4, col_count=(1, 'fixed'), type='array') with gr.Row(): - __l = [str(i) for i in range(10, 101, 10)] - __l.insert(0, '1') - self.submit_numer = gr.Dropdown(__l, value='1', interactive=True, label='Number of Next').style( - container=False) + self.ai_budget = gr.Number(show_label=False, value=0.0, info="关于本次项目的预算,超过预算自动停止,默认无限").style(container=False) + # self.ai_goal_list.style() + + with gr.Tab('Ai Settings'): + pass def draw_next_auto(self): with gr.Row(): - self.submit_next = gr.Button("Next", variant="primary") - self.submit_auto = gr.Button("Continuous", variant="secondary") + self.text_continue = gr.Textbox(visible=False, show_label=False, placeholder="请根据提示输入执行命令").style(container=False) + with gr.Row(): + self.submit_start = gr.Button("Start", variant='primary') + self.submit_next = gr.Button("Next", visible=False, variant='primary') self.submit_stop = gr.Button("Stop", variant="stop") + self.agent_obj = gr.State({'obj': None, "start": self.submit_start, + "next": self.submit_next, "text": self.text_continue}) def signals_input_setting(self): # 注册input self.input_combo = [self.cookies, self.max_length_sl, self.md_dropdown, self.txt, self.top_p, self.temperature, self.chatbot, self.history, self.system_prompt, self.models_box] - self.output_combo = [self.cookies, self.chatbot, self.history, self.status] + self.output_combo = [self.cookies, self.chatbot, self.history, self.status, self.txt] self.predict_args = dict(fn=ArgsGeneralWrapper(predict), inputs=self.input_combo, outputs=self.output_combo) # 提交按钮、重置按钮 self.cancel_handles.append(self.txt.submit(**self.predict_args)) @@ -208,6 +209,17 @@ class ChatBot(ChatBotFrame): # 终止按钮的回调函数注册 self.stopBtn.click(fn=None, inputs=None, outputs=None, cancels=self.cancel_handles) + + def signals_auto_input(self): + from autogpt.cli import agent_main + self.auto_input_combo = [self.ai_name, self.ai_role, self.ai_goal_list, self.ai_budget, + self.cookies, self.chatbot, self.history, + self.agent_obj] + self.auto_output_combo = [self.cookies, self.chatbot, self.history, self.status, + self.agent_obj, self.submit_start, self.submit_next, self.text_continue] + self.submit_start.click(fn=agent_main, inputs=self.auto_input_combo, outputs=self.auto_output_combo) + + # gradio的inbrowser触发不太稳定,回滚代码到原始的浏览器打开函数 def auto_opentab_delay(self): import threading, webbrowser, time @@ -224,16 +236,17 @@ class ChatBot(ChatBotFrame): threading.Thread(target=auto_update, name="self-upgrade", daemon=True).start() # threading.Thread(target=warm_up_modules, name="warm-up", daemon=True).start() + def main(self): with gr.Blocks(title="ChatGPT For Tester", theme=set_theme, analytics_enabled=False, css=advanced_css) as demo: # 绘制页面title self.draw_title() # 绘制一个ROW,row会让底下的元素自动排部 with gr.Row(): - # 绘制列2 - with gr.Column(scale=100): - self.draw_chatbot() # 绘制列1 + with gr.Column(scale=100) as chat: + pass + # 绘制列2 with gr.Column(scale=51): # 绘制对话模组 with gr.Tab('对话模式'): @@ -243,18 +256,19 @@ class ChatBot(ChatBotFrame): self.draw_setting_chat() # 绘制autogpt模组 with gr.Tab('Auto-GPT'): - self.draw_goals_auto() self.draw_next_auto() + self.draw_goals_auto() + with chat: + self.draw_chatbot() # 函数注册,需要在Blocks下进行 self.signals_input_setting() self.signals_function() self.signals_public() + self.signals_auto_input() # Start self.auto_opentab_delay() demo.queue(concurrency_count=CONCURRENT_COUNT).launch(server_name="0.0.0.0", server_port=PORT, auth=AUTHENTICATION) - if __name__ == '__main__': - tester = ChatBot() - tester.main() + ChatBot().main() diff --git a/ai_settings.yaml b/ai_settings.yaml new file mode 100644 index 0000000..9a56d7a --- /dev/null +++ b/ai_settings.yaml @@ -0,0 +1,8 @@ +ai_goals: +- '' +- '' +- '' +- '' +ai_name: Entrepreneur-GPT +ai_role: CCzcccCZCC +api_budget: 0.0 diff --git a/autogpt/.DS_Store b/autogpt/.DS_Store new file mode 100644 index 0000000..eccc1ae Binary files /dev/null and b/autogpt/.DS_Store differ diff --git a/autogpt/CURRENT_BULLETIN.md b/autogpt/CURRENT_BULLETIN.md new file mode 100644 index 0000000..735048d --- /dev/null +++ b/autogpt/CURRENT_BULLETIN.md @@ -0,0 +1,2 @@ +Welcome to Auto-GPT! We'll keep you informed of the latest news and features by printing messages here. +If you don't wish to see this message, you can run Auto-GPT with the --skip-news flag \ No newline at end of file diff --git a/autogpt/__init__.py b/autogpt/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/autogpt/__main__.py b/autogpt/__main__.py new file mode 100644 index 0000000..128f9ee --- /dev/null +++ b/autogpt/__main__.py @@ -0,0 +1,5 @@ +"""Auto-GPT: A GPT powered AI Assistant""" +import autogpt.cli + +if __name__ == "__main__": + autogpt.cli.main() diff --git a/autogpt/agent/__init__.py b/autogpt/agent/__init__.py new file mode 100644 index 0000000..e928af2 --- /dev/null +++ b/autogpt/agent/__init__.py @@ -0,0 +1,4 @@ +from autogpt.agent.agent import Agent +from autogpt.agent.agent_manager import AgentManager + +__all__ = ["Agent", "AgentManager"] diff --git a/autogpt/agent/agent.py b/autogpt/agent/agent.py new file mode 100644 index 0000000..2b6538d --- /dev/null +++ b/autogpt/agent/agent.py @@ -0,0 +1,241 @@ +from colorama import Fore, Style + +from autogpt.app import execute_command, get_command +from autogpt.chat import chat_with_ai, create_chat_message +from autogpt.config import Config +from autogpt.json_utils.json_fix_llm import fix_json_using_multiple_techniques +from autogpt.json_utils.utilities import validate_json +from autogpt.logs import logger, print_assistant_thoughts +from autogpt.speech import say_text +from autogpt.spinner import Spinner +from autogpt.utils import clean_input +from autogpt.workspace import Workspace + + +class Agent: + """Agent class for interacting with Auto-GPT. + + Attributes: + ai_name: The name of the agent. + memory: The memory object to use. + full_message_history: The full message history. + next_action_count: The number of actions to execute. + system_prompt: The system prompt is the initial prompt that defines everything + the AI needs to know to achieve its task successfully. + Currently, the dynamic and customizable information in the system prompt are + ai_name, description and goals. + + triggering_prompt: The last sentence the AI will see before answering. + For Auto-GPT, this prompt is: + Determine which next command to use, and respond using the format specified + above: + The triggering prompt is not part of the system prompt because between the + system prompt and the triggering + prompt we have contextual information that can distract the AI and make it + forget that its goal is to find the next task to achieve. + SYSTEM PROMPT + CONTEXTUAL INFORMATION (memory, previous conversations, anything relevant) + TRIGGERING PROMPT + + The triggering prompt reminds the AI about its short term meta task + (defining the next task) + """ + + def __init__( + self, + ai_name, + memory, + full_message_history, + next_action_count, + command_registry, + config, + system_prompt, + triggering_prompt, + workspace_directory, + ): + self.cfg = Config() + self.ai_name = ai_name + self.memory = memory + self.full_message_history = full_message_history + self.next_action_count = next_action_count + self.command_registry = command_registry + self.config = config + self.system_prompt = system_prompt + self.triggering_prompt = triggering_prompt + self.workspace = Workspace(workspace_directory, self.cfg.restrict_to_workspace) + self.loop_count = 0 + self.command_name = None + self.sarguments = None + self.user_input = "" + self.cfg = Config() + + def start_interaction_loop(self): + # Discontinue if continuous limit is reached + self.loop_count += 1 + if ( + self.cfg.continuous_mode + and self.cfg.continuous_limit > 0 + and self.loop_count > self.cfg.continuous_limit + ): + logger.typewriter_log( + "Continuous Limit Reached: ", Fore.YELLOW, f"{self.cfg.continuous_limit}" + ) + # break + + # Send message to AI, get response + with Spinner("Thinking... "): + self.assistant_reply = chat_with_ai( + self, + self.system_prompt, + self.triggering_prompt, + self.full_message_history, + self.memory, + self.cfg.fast_token_limit, + ) # TODO: This hardcodes the model to use GPT3.5. Make this an argument + + self.assistant_reply_json = fix_json_using_multiple_techniques(self.assistant_reply) + for plugin in self.cfg.plugins: + if not plugin.can_handle_post_planning(): + continue + self.assistant_reply_json = plugin.post_planning(self, self.assistant_reply_json) + + # Print Assistant thoughts + if self.assistant_reply_json != {}: + validate_json(self.assistant_reply_json, "llm_response_format_1") + # Get command name and self.arguments + try: + print_assistant_thoughts(self.ai_name, self.assistant_reply_json) + self.command_name, self.arguments = get_command(self.assistant_reply_json) + if self.cfg.speak_mode: + say_text(f"I want to execute {self.command_name}") + self.arguments = self._resolve_pathlike_command_args(self.arguments) + + except Exception as e: + logger.error("Error: \n", str(e)) + + if not self.cfg.continuous_mode and self.next_action_count == 0: + # ### GET USER AUTHORIZATION TO EXECUTE COMMAND ### + # Get key press: Prompt the user to press enter to continue or escape + # to exit + logger.typewriter_log( + "NEXT ACTION: ", + Fore.CYAN, + f"COMMAND = {self.command_name}" + f"ARGUMENTS = {self.arguments}", + ) + logger.typewriter_log( + "", + "", + "Enter 'y' to authorise command, 'y -N' to run N continuous " + "commands, 'n' to exit program, or enter feedback for " + f"{self.ai_name}...", + ) + + def start_interaction_next(self, cookie, chatbot, history, msg, _input, obj): + console_input = _input + if console_input.lower().strip() == "y": + self.user_input = "GENERATE NEXT COMMAND JSON" + elif console_input.lower().strip() == "": + print("Invalid input format.") + return + elif console_input.lower().startswith("y -"): + try: + self.next_action_count = abs( + int(console_input.split(" ")[1]) + ) + self.user_input = "GENERATE NEXT COMMAND JSON" + except ValueError: + print( + "Invalid input format. Please enter 'y -n' where n is" + " the number of continuous tasks." + ) + + return + elif console_input.lower() == "n": + self.user_input = "EXIT" + return + else: + self.user_input = console_input + self.command_name = "human_feedback" + return + + if self.user_input == "GENERATE NEXT COMMAND JSON": + logger.typewriter_log( + "-=-=-=-=-=-=-= COMMAND AUTHORISED BY USER -=-=-=-=-=-=-=", + Fore.MAGENTA, + "", + ) + elif self.user_input == "EXIT": + print("Exiting...", flush=True) + # break 这里需要注意 + else: + # Print command + logger.typewriter_log( + "NEXT ACTION: ", + Fore.CYAN, + f"COMMAND = {Fore.CYAN}{self.command_name}{Style.RESET_ALL}" + f" ARGUMENTS = {Fore.CYAN}{self.arguments}{Style.RESET_ALL}", + ) + + # Execute command + if self.command_name is not None and self.command_name.lower().startswith("error"): + result = ( + f"Command {self.command_name} threw the following error: {self.arguments}" + ) + elif self.command_name == "human_feedback": + result = f"Human feedback: {self.user_input}" + else: + for plugin in self.cfg.plugins: + if not plugin.can_handle_pre_command(): + continue + self.command_name, self.arguments = plugin.pre_command( + self.command_name, self.arguments + ) + command_result = execute_command( + self.command_registry, + self.command_name, + self.arguments, + self.config.prompt_generator, + ) + result = f"Command {self.command_name} returned: " f"{command_result}" + + for plugin in self.cfg.plugins: + if not plugin.can_handle_post_command(): + continue + result = plugin.post_command(self.command_name, result) + if self.next_action_count > 0: + self.next_action_count -= 1 + if self.command_name != "do_nothing": + memory_to_add = ( + f"Assistant Reply: {self.assistant_reply} " + f"\nResult: {result} " + f"\nHuman Feedback: {self.user_input} " + ) + + self.memory.add(memory_to_add) + + # Check if there's a result from the command append it to the message + # history + if result is not None: + self.full_message_history.append( + create_chat_message("system", result) + ) + logger.typewriter_log("SYSTEM: ", Fore.YELLOW, result) + else: + self.full_message_history.append( + create_chat_message("system", "Unable to execute command") + ) + logger.typewriter_log( + "SYSTEM: ", Fore.YELLOW, "Unable to execute command" + ) + + def _resolve_pathlike_command_args(self, command_args): + if "directory" in command_args and command_args["directory"] in {"", "/"}: + command_args["directory"] = str(self.workspace.root) + else: + for pathlike in ["filename", "directory", "clone_path"]: + if pathlike in command_args: + command_args[pathlike] = str( + self.workspace.get_path(command_args[pathlike]) + ) + return command_args diff --git a/autogpt/agent/agent_manager.py b/autogpt/agent/agent_manager.py new file mode 100644 index 0000000..9a62ef6 --- /dev/null +++ b/autogpt/agent/agent_manager.py @@ -0,0 +1,145 @@ +"""Agent manager for managing GPT agents""" +from __future__ import annotations + +from typing import List, Union + +from autogpt.config.config import Config, Singleton +from autogpt.llm_utils import create_chat_completion +from autogpt.types.openai import Message + + +class AgentManager(metaclass=Singleton): + """Agent manager for managing GPT agents""" + + def __init__(self): + self.next_key = 0 + self.agents = {} # key, (task, full_message_history, model) + self.cfg = Config() + + # Create new GPT agent + # TODO: Centralise use of create_chat_completion() to globally enforce token limit + + def create_agent(self, task: str, prompt: str, model: str) -> tuple[int, str]: + """Create a new agent and return its key + + Args: + task: The task to perform + prompt: The prompt to use + model: The model to use + + Returns: + The key of the new agent + """ + messages: List[Message] = [ + {"role": "user", "content": prompt}, + ] + for plugin in self.cfg.plugins: + if not plugin.can_handle_pre_instruction(): + continue + if plugin_messages := plugin.pre_instruction(messages): + messages.extend(iter(plugin_messages)) + # Start GPT instance + agent_reply = create_chat_completion( + model=model, + messages=messages, + ) + + messages.append({"role": "assistant", "content": agent_reply}) + + plugins_reply = "" + for i, plugin in enumerate(self.cfg.plugins): + if not plugin.can_handle_on_instruction(): + continue + if plugin_result := plugin.on_instruction(messages): + sep = "\n" if i else "" + plugins_reply = f"{plugins_reply}{sep}{plugin_result}" + + if plugins_reply and plugins_reply != "": + messages.append({"role": "assistant", "content": plugins_reply}) + key = self.next_key + # This is done instead of len(agents) to make keys unique even if agents + # are deleted + self.next_key += 1 + + self.agents[key] = (task, messages, model) + + for plugin in self.cfg.plugins: + if not plugin.can_handle_post_instruction(): + continue + agent_reply = plugin.post_instruction(agent_reply) + + return key, agent_reply + + def message_agent(self, key: str | int, message: str) -> str: + """Send a message to an agent and return its response + + Args: + key: The key of the agent to message + message: The message to send to the agent + + Returns: + The agent's response + """ + task, messages, model = self.agents[int(key)] + + # Add user message to message history before sending to agent + messages.append({"role": "user", "content": message}) + + for plugin in self.cfg.plugins: + if not plugin.can_handle_pre_instruction(): + continue + if plugin_messages := plugin.pre_instruction(messages): + for plugin_message in plugin_messages: + messages.append(plugin_message) + + # Start GPT instance + agent_reply = create_chat_completion( + model=model, + messages=messages, + ) + + messages.append({"role": "assistant", "content": agent_reply}) + + plugins_reply = agent_reply + for i, plugin in enumerate(self.cfg.plugins): + if not plugin.can_handle_on_instruction(): + continue + if plugin_result := plugin.on_instruction(messages): + sep = "\n" if i else "" + plugins_reply = f"{plugins_reply}{sep}{plugin_result}" + # Update full message history + if plugins_reply and plugins_reply != "": + messages.append({"role": "assistant", "content": plugins_reply}) + + for plugin in self.cfg.plugins: + if not plugin.can_handle_post_instruction(): + continue + agent_reply = plugin.post_instruction(agent_reply) + + return agent_reply + + def list_agents(self) -> list[tuple[str | int, str]]: + """Return a list of all agents + + Returns: + A list of tuples of the form (key, task) + """ + + # Return a list of agent keys and their tasks + return [(key, task) for key, (task, _, _) in self.agents.items()] + + def delete_agent(self, key: str | int) -> bool: + """Delete an agent from the agent manager + + Args: + key: The key of the agent to delete + + Returns: + True if successful, False otherwise + """ + + try: + del self.agents[int(key)] + return True + except KeyError: + return False diff --git a/autogpt/api_manager.py b/autogpt/api_manager.py new file mode 100644 index 0000000..882e026 --- /dev/null +++ b/autogpt/api_manager.py @@ -0,0 +1,158 @@ +from typing import List + +import openai + +from autogpt.config import Config +from autogpt.logs import logger +from autogpt.modelsinfo import COSTS + +cfg = Config() +openai.api_key = cfg.openai_api_key +print_total_cost = cfg.debug_mode + + +class ApiManager: + def __init__(self, debug=False): + self.total_prompt_tokens = 0 + self.total_completion_tokens = 0 + self.total_cost = 0 + self.total_budget = 0 + self.debug = debug + + def reset(self): + self.total_prompt_tokens = 0 + self.total_completion_tokens = 0 + self.total_cost = 0 + self.total_budget = 0.0 + + def create_chat_completion( + self, + messages: list, # type: ignore + model: str = None, + temperature: float = cfg.temperature, + max_tokens: int = None, + deployment_id=None, + ) -> str: + """ + Create a chat completion and update the cost. + Args: + messages (list): The list of messages to send to the API. + model (str): The model to use for the API call. + temperature (float): The temperature to use for the API call. + max_tokens (int): The maximum number of tokens for the API call. + Returns: + str: The AI's response. + """ + if deployment_id is not None: + response = openai.ChatCompletion.create( + deployment_id=deployment_id, + model=model, + messages=messages, + temperature=temperature, + max_tokens=max_tokens, + ) + else: + response = openai.ChatCompletion.create( + model=model, + messages=messages, + temperature=temperature, + max_tokens=max_tokens, + ) + if self.debug: + logger.debug(f"Response: {response}") + prompt_tokens = response.usage.prompt_tokens + completion_tokens = response.usage.completion_tokens + self.update_cost(prompt_tokens, completion_tokens, model) + return response + + def embedding_create( + self, + text_list: List[str], + model: str = "text-embedding-ada-002", + ) -> List[float]: + """ + Create an embedding for the given input text using the specified model. + + Args: + text_list (List[str]): Input text for which the embedding is to be created. + model (str, optional): The model to use for generating the embedding. + + Returns: + List[float]: The generated embedding as a list of float values. + """ + if cfg.use_azure: + response = openai.Embedding.create( + input=text_list, + engine=cfg.get_azure_deployment_id_for_model(model), + ) + else: + response = openai.Embedding.create(input=text_list, model=model) + + self.update_cost(response.usage.prompt_tokens, 0, model) + return response["data"][0]["embedding"] + + def update_cost(self, prompt_tokens, completion_tokens, model): + """ + Update the total cost, prompt tokens, and completion tokens. + + Args: + prompt_tokens (int): The number of tokens used in the prompt. + completion_tokens (int): The number of tokens used in the completion. + model (str): The model used for the API call. + """ + self.total_prompt_tokens += prompt_tokens + self.total_completion_tokens += completion_tokens + self.total_cost += ( + prompt_tokens * COSTS[model]["prompt"] + + completion_tokens * COSTS[model]["completion"] + ) / 1000 + if print_total_cost: + print(f"Total running cost: ${self.total_cost:.3f}") + + def set_total_budget(self, total_budget): + """ + Sets the total user-defined budget for API calls. + + Args: + prompt_tokens (int): The number of tokens used in the prompt. + """ + self.total_budget = total_budget + + def get_total_prompt_tokens(self): + """ + Get the total number of prompt tokens. + + Returns: + int: The total number of prompt tokens. + """ + return self.total_prompt_tokens + + def get_total_completion_tokens(self): + """ + Get the total number of completion tokens. + + Returns: + int: The total number of completion tokens. + """ + return self.total_completion_tokens + + def get_total_cost(self): + """ + Get the total cost of API calls. + + Returns: + float: The total cost of API calls. + """ + return self.total_cost + + def get_total_budget(self): + """ + Get the total user-defined budget for API calls. + + Returns: + float: The total budget for API calls. + """ + return self.total_budget + + +api_manager = ApiManager(cfg.debug_mode) diff --git a/autogpt/app.py b/autogpt/app.py new file mode 100644 index 0000000..237feae --- /dev/null +++ b/autogpt/app.py @@ -0,0 +1,253 @@ +""" Command and Control """ +import json +from typing import Dict, List, NoReturn, Union + +from autogpt.agent.agent_manager import AgentManager +from autogpt.commands.command import CommandRegistry, command +from autogpt.commands.web_requests import scrape_links, scrape_text +from autogpt.config import Config +from autogpt.memory import get_memory +from autogpt.processing.text import summarize_text +from autogpt.prompts.generator import PromptGenerator +from autogpt.speech import say_text + +CFG = Config() +AGENT_MANAGER = AgentManager() + + +def is_valid_int(value: str) -> bool: + """Check if the value is a valid integer + + Args: + value (str): The value to check + + Returns: + bool: True if the value is a valid integer, False otherwise + """ + try: + int(value) + return True + except ValueError: + return False + + +def get_command(response_json: Dict): + """Parse the response and return the command name and arguments + + Args: + response_json (json): The response from the AI + + Returns: + tuple: The command name and arguments + + Raises: + json.decoder.JSONDecodeError: If the response is not valid JSON + + Exception: If any other error occurs + """ + try: + if "command" not in response_json: + return "Error:", "Missing 'command' object in JSON" + + if not isinstance(response_json, dict): + return "Error:", f"'response_json' object is not dictionary {response_json}" + + command = response_json["command"] + if not isinstance(command, dict): + return "Error:", "'command' object is not a dictionary" + + if "name" not in command: + return "Error:", "Missing 'name' field in 'command' object" + + command_name = command["name"] + + # Use an empty dictionary if 'args' field is not present in 'command' object + arguments = command.get("args", {}) + + return command_name, arguments + except json.decoder.JSONDecodeError: + return "Error:", "Invalid JSON" + # All other errors, return "Error: + error message" + except Exception as e: + return "Error:", str(e) + + +def map_command_synonyms(command_name: str): + """Takes the original command name given by the AI, and checks if the + string matches a list of common/known hallucinations + """ + synonyms = [ + ("write_file", "write_to_file"), + ("create_file", "write_to_file"), + ("search", "google"), + ] + for seen_command, actual_command_name in synonyms: + if command_name == seen_command: + return actual_command_name + return command_name + + +def execute_command( + command_registry: CommandRegistry, + command_name: str, + arguments, + prompt: PromptGenerator, +): + """Execute the command and return the result + + Args: + command_name (str): The name of the command to execute + arguments (dict): The arguments for the command + + Returns: + str: The result of the command + """ + try: + cmd = command_registry.commands.get(command_name) + + # If the command is found, call it with the provided arguments + if cmd: + return cmd(**arguments) + + # TODO: Remove commands below after they are moved to the command registry. + command_name = map_command_synonyms(command_name.lower()) + + if command_name == "memory_add": + return get_memory(CFG).add(arguments["string"]) + + # TODO: Change these to take in a file rather than pasted code, if + # non-file is given, return instructions "Input should be a python + # filepath, write your code to file and try again + elif command_name == "do_nothing": + return "No action performed." + elif command_name == "task_complete": + shutdown() + else: + for command in prompt.commands: + if ( + command_name == command["label"].lower() + or command_name == command["name"].lower() + ): + return command["function"](**arguments) + return ( + f"Unknown command '{command_name}'. Please refer to the 'COMMANDS'" + " list for available commands and only respond in the specified JSON" + " format." + ) + except Exception as e: + return f"Error: {str(e)}" + + +@command( + "get_text_summary", "Get text summary", '"url": "", "question": ""' +) +def get_text_summary(url: str, question: str) -> str: + """Return the results of a Google search + + Args: + url (str): The url to scrape + question (str): The question to summarize the text for + + Returns: + str: The summary of the text + """ + text = scrape_text(url) + summary = summarize_text(url, text, question) + return f""" "Result" : {summary}""" + + +@command("get_hyperlinks", "Get text summary", '"url": ""') +def get_hyperlinks(url: str) -> Union[str, List[str]]: + """Return the results of a Google search + + Args: + url (str): The url to scrape + + Returns: + str or list: The hyperlinks on the page + """ + return scrape_links(url) + + +def shutdown() -> NoReturn: + """Shut down the program""" + print("Shutting down...") + quit() + + +@command( + "start_agent", + "Start GPT Agent", + '"name": "", "task": "", "prompt": ""', +) +def start_agent(name: str, task: str, prompt: str, model=CFG.fast_llm_model) -> str: + """Start an agent with a given name, task, and prompt + + Args: + name (str): The name of the agent + task (str): The task of the agent + prompt (str): The prompt for the agent + model (str): The model to use for the agent + + Returns: + str: The response of the agent + """ + # Remove underscores from name + voice_name = name.replace("_", " ") + + first_message = f"""You are {name}. Respond with: "Acknowledged".""" + agent_intro = f"{voice_name} here, Reporting for duty!" + + # Create agent + if CFG.speak_mode: + say_text(agent_intro, 1) + key, ack = AGENT_MANAGER.create_agent(task, first_message, model) + + if CFG.speak_mode: + say_text(f"Hello {voice_name}. Your task is as follows. {task}.") + + # Assign task (prompt), get response + agent_response = AGENT_MANAGER.message_agent(key, prompt) + + return f"Agent {name} created with key {key}. First response: {agent_response}" + + +@command("message_agent", "Message GPT Agent", '"key": "", "message": ""') +def message_agent(key: str, message: str) -> str: + """Message an agent with a given key and message""" + # Check if the key is a valid integer + if is_valid_int(key): + agent_response = AGENT_MANAGER.message_agent(int(key), message) + else: + return "Invalid key, must be an integer." + + # Speak response + if CFG.speak_mode: + say_text(agent_response, 1) + return agent_response + + +@command("list_agents", "List GPT Agents", "") +def list_agents() -> str: + """List all agents + + Returns: + str: A list of all agents + """ + return "List of agents:\n" + "\n".join( + [str(x[0]) + ": " + x[1] for x in AGENT_MANAGER.list_agents()] + ) + + +@command("delete_agent", "Delete GPT Agent", '"key": ""') +def delete_agent(key: str) -> str: + """Delete an agent with a given key + + Args: + key (str): The key of the agent to delete + + Returns: + str: A message indicating whether the agent was deleted or not + """ + result = AGENT_MANAGER.delete_agent(key) + return f"Agent {key} deleted." if result else f"Agent {key} does not exist." diff --git a/autogpt/auto-gpt.json b/autogpt/auto-gpt.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/autogpt/auto-gpt.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/autogpt/auto_gpt_workspace/.DS_Store b/autogpt/auto_gpt_workspace/.DS_Store new file mode 100644 index 0000000..b5f4c97 Binary files /dev/null and b/autogpt/auto_gpt_workspace/.DS_Store differ diff --git a/autogpt/auto_gpt_workspace/127.0.0.1/auto-gpt.json b/autogpt/auto_gpt_workspace/127.0.0.1/auto-gpt.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/autogpt/auto_gpt_workspace/127.0.0.1/auto-gpt.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/autogpt/auto_gpt_workspace/127.0.0.1/file_logger.txt b/autogpt/auto_gpt_workspace/127.0.0.1/file_logger.txt new file mode 100644 index 0000000..67f4589 --- /dev/null +++ b/autogpt/auto_gpt_workspace/127.0.0.1/file_logger.txt @@ -0,0 +1 @@ +File Operation Logger \ No newline at end of file diff --git a/autogpt/chat.py b/autogpt/chat.py new file mode 100644 index 0000000..21eab6a --- /dev/null +++ b/autogpt/chat.py @@ -0,0 +1,218 @@ +import time + +from openai.error import RateLimitError + +from autogpt import token_counter +from autogpt.api_manager import api_manager +from autogpt.config import Config +from autogpt.llm_utils import create_chat_completion +from autogpt.logs import logger +from autogpt.types.openai import Message + +cfg = Config() + + +def create_chat_message(role, content) -> Message: + """ + Create a chat message with the given role and content. + + Args: + role (str): The role of the message sender, e.g., "system", "user", or "assistant". + content (str): The content of the message. + + Returns: + dict: A dictionary containing the role and content of the message. + """ + return {"role": role, "content": content} + + +def generate_context(prompt, relevant_memory, full_message_history, model): + current_context = [ + create_chat_message("system", prompt), + create_chat_message( + "system", f"The current time and date is {time.strftime('%c')}" + ), + create_chat_message( + "system", + f"This reminds you of these events from your past:\n{relevant_memory}\n\n", + ), + ] + + # Add messages from the full message history until we reach the token limit + next_message_to_add_index = len(full_message_history) - 1 + insertion_index = len(current_context) + # Count the currently used tokens + current_tokens_used = token_counter.count_message_tokens(current_context, model) + return ( + next_message_to_add_index, + current_tokens_used, + insertion_index, + current_context, + ) + + +# TODO: Change debug from hardcode to argument +def chat_with_ai( + agent, prompt, user_input, full_message_history, permanent_memory, token_limit +): + """Interact with the OpenAI API, sending the prompt, user input, message history, + and permanent memory.""" + while True: + try: + """ + Interact with the OpenAI API, sending the prompt, user input, + message history, and permanent memory. + + Args: + prompt (str): The prompt explaining the rules to the AI. + user_input (str): The input from the user. + full_message_history (list): The list of all messages sent between the + user and the AI. + permanent_memory (Obj): The memory object containing the permanent + memory. + token_limit (int): The maximum number of tokens allowed in the API call. + + Returns: + str: The AI's response. + """ + model = cfg.fast_llm_model # TODO: Change model from hardcode to argument + # Reserve 1000 tokens for the response + + logger.debug(f"Token limit: {token_limit}") + send_token_limit = token_limit - 1000 + + relevant_memory = ( + "" + if len(full_message_history) == 0 + else permanent_memory.get_relevant(str(full_message_history[-9:]), 10) + ) + + logger.debug(f"Memory Stats: {permanent_memory.get_stats()}") + + ( + next_message_to_add_index, + current_tokens_used, + insertion_index, + current_context, + ) = generate_context(prompt, relevant_memory, full_message_history, model) + + while current_tokens_used > 2500: + # remove memories until we are under 2500 tokens + relevant_memory = relevant_memory[:-1] + ( + next_message_to_add_index, + current_tokens_used, + insertion_index, + current_context, + ) = generate_context( + prompt, relevant_memory, full_message_history, model + ) + + current_tokens_used += token_counter.count_message_tokens( + [create_chat_message("user", user_input)], model + ) # Account for user input (appended later) + + while next_message_to_add_index >= 0: + # print (f"CURRENT TOKENS USED: {current_tokens_used}") + message_to_add = full_message_history[next_message_to_add_index] + + tokens_to_add = token_counter.count_message_tokens( + [message_to_add], model + ) + if current_tokens_used + tokens_to_add > send_token_limit: + break + + # Add the most recent message to the start of the current context, + # after the two system prompts. + current_context.insert( + insertion_index, full_message_history[next_message_to_add_index] + ) + + # Count the currently used tokens + current_tokens_used += tokens_to_add + + # Move to the next most recent message in the full message history + next_message_to_add_index -= 1 + + # inform the AI about its remaining budget (if it has one) + if api_manager.get_total_budget() > 0.0: + remaining_budget = ( + api_manager.get_total_budget() - api_manager.get_total_cost() + ) + if remaining_budget < 0: + remaining_budget = 0 + system_message = ( + f"Your remaining API budget is ${remaining_budget:.3f}" + + ( + " BUDGET EXCEEDED! SHUT DOWN!\n\n" + if remaining_budget == 0 + else " Budget very nearly exceeded! Shut down gracefully!\n\n" + if remaining_budget < 0.005 + else " Budget nearly exceeded. Finish up.\n\n" + if remaining_budget < 0.01 + else "\n\n" + ) + ) + logger.debug(system_message) + current_context.append(create_chat_message("system", system_message)) + + # Append user input, the length of this is accounted for above + current_context.extend([create_chat_message("user", user_input)]) + + plugin_count = len(cfg.plugins) + for i, plugin in enumerate(cfg.plugins): + if not plugin.can_handle_on_planning(): + continue + plugin_response = plugin.on_planning( + agent.prompt_generator, current_context + ) + if not plugin_response or plugin_response == "": + continue + tokens_to_add = token_counter.count_message_tokens( + [create_chat_message("system", plugin_response)], model + ) + if current_tokens_used + tokens_to_add > send_token_limit: + if cfg.debug_mode: + print("Plugin response too long, skipping:", plugin_response) + print("Plugins remaining at stop:", plugin_count - i) + break + current_context.append(create_chat_message("system", plugin_response)) + + # Calculate remaining tokens + tokens_remaining = token_limit - current_tokens_used + # assert tokens_remaining >= 0, "Tokens remaining is negative. + # This should never happen, please submit a bug report at + # https://www.github.com/Torantulino/Auto-GPT" + + # Debug print the current context + logger.debug(f"Token limit: {token_limit}") + logger.debug(f"Send Token Count: {current_tokens_used}") + logger.debug(f"Tokens remaining for response: {tokens_remaining}") + logger.debug("------------ CONTEXT SENT TO AI ---------------") + for message in current_context: + # Skip printing the prompt + if message["role"] == "system" and message["content"] == prompt: + continue + logger.debug(f"{message['role'].capitalize()}: {message['content']}") + logger.debug("") + logger.debug("----------- END OF CONTEXT ----------------") + + # TODO: use a model defined elsewhere, so that model can contain + # temperature and other settings we care about + assistant_reply = create_chat_completion( + model=model, + messages=current_context, + max_tokens=tokens_remaining, + ) + + # Update full message history + full_message_history.append(create_chat_message("user", user_input)) + full_message_history.append( + create_chat_message("assistant", assistant_reply) + ) + + return assistant_reply + except RateLimitError: + # TODO: When we switch to langchain, this is built in + print("Error: ", "API Rate Limit Reached. Waiting 10 seconds...") + time.sleep(10) diff --git a/autogpt/cli.py b/autogpt/cli.py new file mode 100644 index 0000000..3951788 --- /dev/null +++ b/autogpt/cli.py @@ -0,0 +1,230 @@ +"""Main script for the autogpt package.""" +# Put imports inside function to avoid importing everything when starting the CLI +import logging +import os.path +import sys +from pathlib import Path + +import gradio +from colorama import Fore +from autogpt.agent.agent import Agent +from autogpt.commands.command import CommandRegistry +from autogpt.config import Config, check_openai_api_key +from autogpt.configurator import create_config +from autogpt.logs import logger +from autogpt.memory import get_memory +from autogpt.plugins import scan_plugins +from autogpt.prompts.prompt import construct_main_ai_config +from autogpt.utils import get_current_git_branch, get_latest_bulletin +from autogpt.workspace import Workspace +import func_box +from toolbox import update_ui +from toolbox import ChatBotWithCookies +def handle_config(kwargs_settings): + kwargs_settings = { + 'continuous': False, # Enable Continuous Mode + 'continuous_limit': None, # Defines the number of times to run in continuous mode + 'ai_settings': None, # Specifies which ai_settings.yaml file to use, will also automatically skip the re-prompt. + 'skip_reprompt': False, # Skips the re-prompting messages at the beginning of the scrip + 'speak': False, # Enable speak Mode + 'debug': False, # Enable Debug Mode + 'gpt3only': False, # Enable GPT3.5 Only Mode + 'gpt4only': False, # Enable GPT4 Only Mode + 'memory_type': None, # Defines which Memory backend to use + 'browser_name': None, # Specifies which web-browser to use when using selenium to scrape the web. + 'allow_downloads': False, # Dangerous: Allows Auto-GPT to download files natively. + 'skip_news': True, # Specifies whether to suppress the output of latest news on startup. + 'workspace_directory': None # TODO: this is a hidden option for now, necessary for integration testing. We should make this public once we're ready to roll out agent specific workspaces. + } + """ + Welcome to AutoGPT an experimental open-source application showcasing the capabilities of the GPT-4 pushing the boundaries of AI. + Start an Auto-GPT assistant. + """ + if kwargs_settings['workspace_directory']: + kwargs_settings['ai_settings'] = os.path.join(kwargs_settings['workspace_directory'], 'ai_settings.yaml') + # if ctx.invoked_subcommand is None: + cfg = Config() + # TODO: fill in llm values here + check_openai_api_key() + create_config( + kwargs_settings['continuous'], + kwargs_settings['continuous_limit'], + kwargs_settings['ai_settings'], + kwargs_settings['skip_reprompt'], + kwargs_settings['speak'], + kwargs_settings['debug'], + kwargs_settings['gpt3only'], + kwargs_settings['gpt4only'], + kwargs_settings['memory_type'], + kwargs_settings['browser_name'], + kwargs_settings['allow_downloads'], + kwargs_settings['skip_news'], + ) + return cfg + + +def handle_news(): + motd = get_latest_bulletin() + if motd: + logger.typewriter_log("NEWS: ", Fore.GREEN, motd) + git_branch = get_current_git_branch() + if git_branch and git_branch != "stable": + logger.typewriter_log( + "WARNING: ", + Fore.RED, + f"You are running on `{git_branch}` branch " + "- this is not a supported branch.", + ) + if sys.version_info < (3, 10): + logger.typewriter_log( + "WARNING: ", + Fore.RED, + "You are running on an older version of Python. " + "Some people have observed problems with certain " + "parts of Auto-GPT with this version. " + "Please consider upgrading to Python 3.10 or higher.", + ) + + +def handle_registry(): + # Create a CommandRegistry instance and scan default folder + command_registry = CommandRegistry() + command_registry.import_commands("autogpt.commands.analyze_code") + command_registry.import_commands("autogpt.commands.audio_text") + command_registry.import_commands("autogpt.commands.execute_code") + command_registry.import_commands("autogpt.commands.file_operations") + command_registry.import_commands("autogpt.commands.git_operations") + command_registry.import_commands("autogpt.commands.google_search") + command_registry.import_commands("autogpt.commands.image_gen") + command_registry.import_commands("autogpt.commands.improve_code") + command_registry.import_commands("autogpt.commands.twitter") + command_registry.import_commands("autogpt.commands.web_selenium") + command_registry.import_commands("autogpt.commands.write_tests") + command_registry.import_commands("autogpt.app") + return command_registry + + +def handle_workspace(user): + # TODO: have this directory live outside the repository (e.g. in a user's + # home directory) and have it come in as a command line argument or part of + # the env file. + if user is None: + workspace_directory = Path(__file__).parent / "auto_gpt_workspace" + else: + workspace_directory = Path(__file__).parent / "auto_gpt_workspace" / user + # TODO: pass in the ai_settings file and the env file and have them cloned into + # the workspace directory so we can bind them to the agent. + workspace_directory = Workspace.make_workspace(workspace_directory) + # HACK: doing this here to collect some globals that depend on the workspace. + file_logger_path = workspace_directory / "file_logger.txt" + if not file_logger_path.exists(): + with file_logger_path.open(mode="w", encoding="utf-8") as f: + f.write("File Operation Logger ") + + return workspace_directory, file_logger_path + + +def update_obj(plugin_kwargs, _is=True): + obj = plugin_kwargs['obj'] + start = plugin_kwargs['start'] + next_ = plugin_kwargs['next'] + text = plugin_kwargs['txt'] + if _is: + start.update(visible=True) + next_.update(visible=False) + text.update(visible=False) + else: + start.update(visible=False) + next_.update(visible=True) + text.update(visible=True) + return obj, start, next_, text + + +def agent_main(name, role, goals, budget, + cookies, chatbot, history, obj, + ipaddr: gradio.Request): + # ai setup + input_kwargs = { + 'name': name, + 'role': role, + 'goals': goals, + 'budget': budget + } + # chat setup + logger.output_content = [] + chatbot_with_cookie = ChatBotWithCookies(cookies) + chatbot_with_cookie.write_list(chatbot) + history = [] + cfg = handle_config(None) + logger.set_level(logging.DEBUG if cfg.debug_mode else logging.INFO) + workspace_directory = ipaddr.client.host + if not cfg.skip_news: + handle_news() + cfg.set_plugins(scan_plugins(cfg, cfg.debug_mode)) + command_registry = handle_registry() + ai_config = construct_main_ai_config(input_kwargs) + def update_stream_ui(user='', gpt='', msg='Done', + _start=obj['start'].update(), _next=obj['next'].update(), _text=obj['text'].update()): + if user or gpt: + temp = [user, gpt] + if not chatbot_with_cookie: + chatbot_with_cookie.append(temp) + else: + chatbot_with_cookie[-1] = [chatbot_with_cookie[-1][i] + temp[i] for i in range(len(chatbot_with_cookie[-1]))] + yield chatbot_with_cookie.get_cookies(), chatbot_with_cookie, history, msg, obj, _start, _next, _text + if not ai_config: + msg = '### ROLE 不能为空' + # yield chatbot_with_cookie.get_cookies(), chatbot_with_cookie, history, msg, obj, None, None, None + yield from update_stream_ui(msg=msg) + return + ai_config.command_registry = command_registry + next_action_count = 0 + # Make a constant: + triggering_prompt = ( + "Determine which next command to use, and respond using the" + " format specified above:" + ) + workspace_directory, file_logger_path = handle_workspace(workspace_directory) + cfg.workspace_path = str(workspace_directory) + cfg.file_logger_path = str(file_logger_path) + # Initialize memory and make sure it is empty. + # this is particularly important for indexing and referencing pinecone memory + memory = get_memory(cfg, init=True) + logger.typewriter_log( + "Using memory of type:", Fore.GREEN, f"{memory.__class__.__name__}" + ) + logger.typewriter_log("Using Browser:", Fore.GREEN, cfg.selenium_web_browser) + system_prompt = ai_config.construct_full_prompt() + if cfg.debug_mode: + logger.typewriter_log("Prompt:", Fore.GREEN, system_prompt) + agent = Agent( + ai_name=input_kwargs['name'], + memory=memory, + full_message_history=history, + next_action_count=next_action_count, + command_registry=command_registry, + config=ai_config, + system_prompt=system_prompt, + triggering_prompt=triggering_prompt, + workspace_directory=workspace_directory, + ) + obj['obj'] = agent + _start = obj['start'].update(visible=False) + _next = obj['next'].update(visible=True) + _text = obj['text'].update(visible=True, interactive=True) + chat, his = func_box.chat_history(logger.output_content) + yield from update_stream_ui(user='Auto-GPT Start!', gpt=chat, _start=_start, _next=_next, _text=_text) + agent.start_interaction_loop() + chat, his = func_box.chat_history(logger.output_content) + yield from update_stream_ui(user='Auto-GPT Start!', gpt=chat, _start=_start, _next=_next, _text=_text) + + + + +def agent_start(cookie, chatbot, history, msg, obj): + yield from obj['obj'].start_interaction_loop(cookie, chatbot, history, msg, obj) + + +if __name__ == "__main__": + pass + diff --git a/autogpt/cli_private.py b/autogpt/cli_private.py new file mode 100644 index 0000000..75908a1 --- /dev/null +++ b/autogpt/cli_private.py @@ -0,0 +1,213 @@ +"""Main script for the autogpt package.""" +import click + + +@click.group(invoke_without_command=True) +@click.option("-c", "--continuous", is_flag=True, help="Enable Continuous Mode") +@click.option( + "--skip-reprompt", + "-y", + is_flag=True, + help="Skips the re-prompting messages at the beginning of the script", +) +@click.option( + "--ai-settings", + "-C", + help="Specifies which ai_settings.yaml file to use, will also automatically skip the re-prompt.", +) +@click.option( + "-l", + "--continuous-limit", + type=int, + help="Defines the number of times to run in continuous mode", +) +@click.option("--speak", is_flag=True, help="Enable Speak Mode") +@click.option("--debug", is_flag=True, help="Enable Debug Mode") +@click.option("--gpt3only", is_flag=True, help="Enable GPT3.5 Only Mode") +@click.option("--gpt4only", is_flag=True, help="Enable GPT4 Only Mode") +@click.option( + "--use-memory", + "-m", + "memory_type", + type=str, + help="Defines which Memory backend to use", +) +@click.option( + "-b", + "--browser-name", + help="Specifies which web-browser to use when using selenium to scrape the web.", +) +@click.option( + "--allow-downloads", + is_flag=True, + help="Dangerous: Allows Auto-GPT to download files natively.", +) +@click.option( + "--skip-news", + is_flag=True, + help="Specifies whether to suppress the output of latest news on startup.", +) +@click.option( + # TODO: this is a hidden option for now, necessary for integration testing. + # We should make this public once we're ready to roll out agent specific workspaces. + "--workspace-directory", + "-w", + type=click.Path(), + hidden=True, +) +@click.pass_context +def main( + ctx: click.Context, + continuous: bool, + continuous_limit: int, + ai_settings: str, + skip_reprompt: bool, + speak: bool, + debug: bool, + gpt3only: bool, + gpt4only: bool, + memory_type: str, + browser_name: str, + allow_downloads: bool, + skip_news: bool, + workspace_directory: str, +) -> None: + """ + Welcome to AutoGPT an experimental open-source application showcasing the capabilities of the GPT-4 pushing the boundaries of AI. + + Start an Auto-GPT assistant. + """ + # Put imports inside function to avoid importing everything when starting the CLI + import logging + import sys + from pathlib import Path + + from colorama import Fore + + from autogpt.agent.agent import Agent + from autogpt.commands.command import CommandRegistry + from autogpt.config import Config, check_openai_api_key + from autogpt.configurator import create_config + from autogpt.logs import logger + from autogpt.memory import get_memory + from autogpt.plugins import scan_plugins + from autogpt.prompts.prompt import construct_main_ai_config + from autogpt.utils import get_current_git_branch, get_latest_bulletin + from autogpt.workspace import Workspace + + if ctx.invoked_subcommand is None: + cfg = Config() + # TODO: fill in llm values here + check_openai_api_key() + create_config( + continuous, + continuous_limit, + ai_settings, + skip_reprompt, + speak, + debug, + gpt3only, + gpt4only, + memory_type, + browser_name, + allow_downloads, + skip_news, + ) + logger.set_level(logging.DEBUG if cfg.debug_mode else logging.INFO) + if not cfg.skip_news: + motd = get_latest_bulletin() + if motd: + logger.typewriter_log("NEWS: ", Fore.GREEN, motd) + git_branch = get_current_git_branch() + if git_branch and git_branch != "stable": + logger.typewriter_log( + "WARNING: ", + Fore.RED, + f"You are running on `{git_branch}` branch " + "- this is not a supported branch.", + ) + if sys.version_info < (3, 10): + logger.typewriter_log( + "WARNING: ", + Fore.RED, + "You are running on an older version of Python. " + "Some people have observed problems with certain " + "parts of Auto-GPT with this version. " + "Please consider upgrading to Python 3.10 or higher.", + ) + + cfg.set_plugins(scan_plugins(cfg, cfg.debug_mode)) + # Create a CommandRegistry instance and scan default folder + command_registry = CommandRegistry() + command_registry.import_commands("autogpt.commands.analyze_code") + command_registry.import_commands("autogpt.commands.audio_text") + command_registry.import_commands("autogpt.commands.execute_code") + command_registry.import_commands("autogpt.commands.file_operations") + command_registry.import_commands("autogpt.commands.git_operations") + command_registry.import_commands("autogpt.commands.google_search") + command_registry.import_commands("autogpt.commands.image_gen") + command_registry.import_commands("autogpt.commands.improve_code") + command_registry.import_commands("autogpt.commands.twitter") + command_registry.import_commands("autogpt.commands.web_selenium") + command_registry.import_commands("autogpt.commands.write_tests") + command_registry.import_commands("autogpt.app") + + ai_name = "" + ai_config = construct_main_ai_config() + ai_config.command_registry = command_registry + # print(prompt) + # Initialize variables + full_message_history = [] + next_action_count = 0 + # Make a constant: + triggering_prompt = ( + "Determine which next command to use, and respond using the" + " format specified above:" + ) + # Initialize memory and make sure it is empty. + # this is particularly important for indexing and referencing pinecone memory + memory = get_memory(cfg, init=True) + logger.typewriter_log( + "Using memory of type:", Fore.GREEN, f"{memory.__class__.__name__}" + ) + logger.typewriter_log("Using Browser:", Fore.GREEN, cfg.selenium_web_browser) + system_prompt = ai_config.construct_full_prompt() + if cfg.debug_mode: + logger.typewriter_log("Prompt:", Fore.GREEN, system_prompt) + + # TODO: have this directory live outside the repository (e.g. in a user's + # home directory) and have it come in as a command line argument or part of + # the env file. + if workspace_directory is None: + workspace_directory = Path(__file__).parent / "auto_gpt_workspace" + else: + workspace_directory = Path(workspace_directory) + # TODO: pass in the ai_settings file and the env file and have them cloned into + # the workspace directory so we can bind them to the agent. + workspace_directory = Workspace.make_workspace(workspace_directory) + cfg.workspace_path = str(workspace_directory) + + # HACK: doing this here to collect some globals that depend on the workspace. + file_logger_path = workspace_directory / "file_logger.txt" + if not file_logger_path.exists(): + with file_logger_path.open(mode="w", encoding="utf-8") as f: + f.write("File Operation Logger ") + + cfg.file_logger_path = str(file_logger_path) + + agent = Agent( + ai_name=ai_name, + memory=memory, + full_message_history=full_message_history, + next_action_count=next_action_count, + command_registry=command_registry, + config=ai_config, + system_prompt=system_prompt, + triggering_prompt=triggering_prompt, + workspace_directory=workspace_directory, + ) + agent.start_interaction_loop() + + +if __name__ == "__main__": + main() diff --git a/autogpt/commands/__init__.py b/autogpt/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/autogpt/commands/analyze_code.py b/autogpt/commands/analyze_code.py new file mode 100644 index 0000000..47cfc1e --- /dev/null +++ b/autogpt/commands/analyze_code.py @@ -0,0 +1,31 @@ +"""Code evaluation module.""" +from __future__ import annotations + +from autogpt.commands.command import command +from autogpt.llm_utils import call_ai_function + + +@command( + "analyze_code", + "Analyze Code", + '"code": ""', +) +def analyze_code(code: str) -> list[str]: + """ + A function that takes in a string and returns a response from create chat + completion api call. + + Parameters: + code (str): Code to be evaluated. + Returns: + A result string from create chat completion. A list of suggestions to + improve the code. + """ + + function_string = "def analyze_code(code: str) -> list[str]:" + args = [code] + description_string = ( + "Analyzes the given code and returns a list of suggestions for improvements." + ) + + return call_ai_function(function_string, args, description_string) diff --git a/autogpt/commands/audio_text.py b/autogpt/commands/audio_text.py new file mode 100644 index 0000000..0a8640c --- /dev/null +++ b/autogpt/commands/audio_text.py @@ -0,0 +1,61 @@ +"""Commands for converting audio to text.""" +import json + +import requests + +from autogpt.commands.command import command +from autogpt.config import Config + +CFG = Config() + + +@command( + "read_audio_from_file", + "Convert Audio to text", + '"filename": ""', + CFG.huggingface_audio_to_text_model, + "Configure huggingface_audio_to_text_model.", +) +def read_audio_from_file(filename: str) -> str: + """ + Convert audio to text. + + Args: + filename (str): The path to the audio file + + Returns: + str: The text from the audio + """ + with open(filename, "rb") as audio_file: + audio = audio_file.read() + return read_audio(audio) + + +def read_audio(audio: bytes) -> str: + """ + Convert audio to text. + + Args: + audio (bytes): The audio to convert + + Returns: + str: The text from the audio + """ + model = CFG.huggingface_audio_to_text_model + api_url = f"https://api-inference.huggingface.co/models/{model}" + api_token = CFG.huggingface_api_token + headers = {"Authorization": f"Bearer {api_token}"} + + if api_token is None: + raise ValueError( + "You need to set your Hugging Face API token in the config file." + ) + + response = requests.post( + api_url, + headers=headers, + data=audio, + ) + + text = json.loads(response.content.decode("utf-8"))["text"] + return f"The audio says: {text}" diff --git a/autogpt/commands/command.py b/autogpt/commands/command.py new file mode 100644 index 0000000..22ebace --- /dev/null +++ b/autogpt/commands/command.py @@ -0,0 +1,156 @@ +import functools +import importlib +import inspect +from typing import Any, Callable, Optional + +# Unique identifier for auto-gpt commands +AUTO_GPT_COMMAND_IDENTIFIER = "auto_gpt_command" + + +class Command: + """A class representing a command. + + Attributes: + name (str): The name of the command. + description (str): A brief description of what the command does. + signature (str): The signature of the function that the command executes. Defaults to None. + """ + + def __init__( + self, + name: str, + description: str, + method: Callable[..., Any], + signature: str = "", + enabled: bool = True, + disabled_reason: Optional[str] = None, + ): + self.name = name + self.description = description + self.method = method + self.signature = signature if signature else str(inspect.signature(self.method)) + self.enabled = enabled + self.disabled_reason = disabled_reason + + def __call__(self, *args, **kwargs) -> Any: + if not self.enabled: + return f"Command '{self.name}' is disabled: {self.disabled_reason}" + return self.method(*args, **kwargs) + + def __str__(self) -> str: + return f"{self.name}: {self.description}, args: {self.signature}" + + +class CommandRegistry: + """ + The CommandRegistry class is a manager for a collection of Command objects. + It allows the registration, modification, and retrieval of Command objects, + as well as the scanning and loading of command plugins from a specified + directory. + """ + + def __init__(self): + self.commands = {} + + def _import_module(self, module_name: str) -> Any: + return importlib.import_module(module_name) + + def _reload_module(self, module: Any) -> Any: + return importlib.reload(module) + + def register(self, cmd: Command) -> None: + self.commands[cmd.name] = cmd + + def unregister(self, command_name: str): + if command_name in self.commands: + del self.commands[command_name] + else: + raise KeyError(f"Command '{command_name}' not found in registry.") + + def reload_commands(self) -> None: + """Reloads all loaded command plugins.""" + for cmd_name in self.commands: + cmd = self.commands[cmd_name] + module = self._import_module(cmd.__module__) + reloaded_module = self._reload_module(module) + if hasattr(reloaded_module, "register"): + reloaded_module.register(self) + + def get_command(self, name: str) -> Callable[..., Any]: + return self.commands[name] + + def call(self, command_name: str, **kwargs) -> Any: + if command_name not in self.commands: + raise KeyError(f"Command '{command_name}' not found in registry.") + command = self.commands[command_name] + return command(**kwargs) + + def command_prompt(self) -> str: + """ + Returns a string representation of all registered `Command` objects for use in a prompt + """ + commands_list = [ + f"{idx + 1}. {str(cmd)}" for idx, cmd in enumerate(self.commands.values()) + ] + return "\n".join(commands_list) + + def import_commands(self, module_name: str) -> None: + """ + Imports the specified Python module containing command plugins. + + This method imports the associated module and registers any functions or + classes that are decorated with the `AUTO_GPT_COMMAND_IDENTIFIER` attribute + as `Command` objects. The registered `Command` objects are then added to the + `commands` dictionary of the `CommandRegistry` object. + + Args: + module_name (str): The name of the module to import for command plugins. + """ + + module = importlib.import_module(module_name) + + for attr_name in dir(module): + attr = getattr(module, attr_name) + # Register decorated functions + if hasattr(attr, AUTO_GPT_COMMAND_IDENTIFIER) and getattr( + attr, AUTO_GPT_COMMAND_IDENTIFIER + ): + self.register(attr.command) + # Register command classes + elif ( + inspect.isclass(attr) and issubclass(attr, Command) and attr != Command + ): + cmd_instance = attr() + self.register(cmd_instance) + + +def command( + name: str, + description: str, + signature: str = "", + enabled: bool = True, + disabled_reason: Optional[str] = None, +) -> Callable[..., Any]: + """The command decorator is used to create Command objects from ordinary functions.""" + + def decorator(func: Callable[..., Any]) -> Command: + cmd = Command( + name=name, + description=description, + method=func, + signature=signature, + enabled=enabled, + disabled_reason=disabled_reason, + ) + + @functools.wraps(func) + def wrapper(*args, **kwargs) -> Any: + return func(*args, **kwargs) + + wrapper.command = cmd + + setattr(wrapper, AUTO_GPT_COMMAND_IDENTIFIER, True) + + return wrapper + + return decorator diff --git a/autogpt/commands/execute_code.py b/autogpt/commands/execute_code.py new file mode 100644 index 0000000..71c1bd2 --- /dev/null +++ b/autogpt/commands/execute_code.py @@ -0,0 +1,182 @@ +"""Execute code in a Docker container""" +import os +import subprocess + +import docker +from docker.errors import ImageNotFound + +from autogpt.commands.command import command +from autogpt.config import Config + +CFG = Config() + + +@command("execute_python_file", "Execute Python File", '"filename": ""') +def execute_python_file(filename: str) -> str: + """Execute a Python file in a Docker container and return the output + + Args: + filename (str): The name of the file to execute + + Returns: + str: The output of the file + """ + print(f"Executing file '{filename}'") + + if not filename.endswith(".py"): + return "Error: Invalid file type. Only .py files are allowed." + + if not os.path.isfile(filename): + return f"Error: File '{filename}' does not exist." + + if we_are_running_in_a_docker_container(): + result = subprocess.run( + f"python {filename}", capture_output=True, encoding="utf8", shell=True + ) + if result.returncode == 0: + return result.stdout + else: + return f"Error: {result.stderr}" + + try: + client = docker.from_env() + + # You can replace this with the desired Python image/version + # You can find available Python images on Docker Hub: + # https://hub.docker.com/_/python + image_name = "python:3-alpine" + try: + client.images.get(image_name) + print(f"Image '{image_name}' found locally") + except ImageNotFound: + print(f"Image '{image_name}' not found locally, pulling from Docker Hub") + # Use the low-level API to stream the pull response + low_level_client = docker.APIClient() + for line in low_level_client.pull(image_name, stream=True, decode=True): + # Print the status and progress, if available + status = line.get("status") + progress = line.get("progress") + if status and progress: + print(f"{status}: {progress}") + elif status: + print(status) + + container = client.containers.run( + image_name, + f"python {filename}", + volumes={ + CFG.workspace_path: { + "bind": "/workspace", + "mode": "ro", + } + }, + working_dir="/workspace", + stderr=True, + stdout=True, + detach=True, + ) + + container.wait() + logs = container.logs().decode("utf-8") + container.remove() + + # print(f"Execution complete. Output: {output}") + # print(f"Logs: {logs}") + + return logs + + except docker.errors.DockerException as e: + print( + "Could not run the script in a container. If you haven't already, please install Docker https://docs.docker.com/get-docker/" + ) + return f"Error: {str(e)}" + + except Exception as e: + return f"Error: {str(e)}" + + +@command( + "execute_shell", + "Execute Shell Command, non-interactive commands only", + '"command_line": ""', + CFG.execute_local_commands, + "You are not allowed to run local shell commands. To execute" + " shell commands, EXECUTE_LOCAL_COMMANDS must be set to 'True' " + "in your config. Do not attempt to bypass the restriction.", +) +def execute_shell(command_line: str) -> str: + """Execute a shell command and return the output + + Args: + command_line (str): The command line to execute + + Returns: + str: The output of the command + """ + + if not CFG.execute_local_commands: + return ( + "You are not allowed to run local shell commands. To execute" + " shell commands, EXECUTE_LOCAL_COMMANDS must be set to 'True' " + "in your config. Do not attempt to bypass the restriction." + ) + current_dir = os.getcwd() + # Change dir into workspace if necessary + if CFG.workspace_path not in current_dir: + os.chdir(CFG.workspace_path) + + print(f"Executing command '{command_line}' in working directory '{os.getcwd()}'") + + result = subprocess.run(command_line, capture_output=True, shell=True) + output = f"STDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}" + + # Change back to whatever the prior working dir was + + os.chdir(current_dir) + + +@command( + "execute_shell_popen", + "Execute Shell Command, non-interactive commands only", + '"command_line": ""', + CFG.execute_local_commands, + "You are not allowed to run local shell commands. To execute" + " shell commands, EXECUTE_LOCAL_COMMANDS must be set to 'True' " + "in your config. Do not attempt to bypass the restriction.", +) +def execute_shell_popen(command_line) -> str: + """Execute a shell command with Popen and returns an english description + of the event and the process id + + Args: + command_line (str): The command line to execute + + Returns: + str: Description of the fact that the process started and its id + """ + current_dir = os.getcwd() + # Change dir into workspace if necessary + if CFG.workspace_path not in current_dir: + os.chdir(CFG.workspace_path) + + print(f"Executing command '{command_line}' in working directory '{os.getcwd()}'") + + do_not_show_output = subprocess.DEVNULL + process = subprocess.Popen( + command_line, shell=True, stdout=do_not_show_output, stderr=do_not_show_output + ) + + # Change back to whatever the prior working dir was + + os.chdir(current_dir) + + return f"Subprocess started with PID:'{str(process.pid)}'" + + +def we_are_running_in_a_docker_container() -> bool: + """Check if we are running in a Docker container + + Returns: + bool: True if we are running in a Docker container, False otherwise + """ + return os.path.exists("/.dockerenv") diff --git a/autogpt/commands/file_operations.py b/autogpt/commands/file_operations.py new file mode 100644 index 0000000..0735c06 --- /dev/null +++ b/autogpt/commands/file_operations.py @@ -0,0 +1,268 @@ +"""File operations for AutoGPT""" +from __future__ import annotations + +import os +import os.path +from typing import Generator + +import requests +from colorama import Back, Fore +from requests.adapters import HTTPAdapter, Retry + +from autogpt.commands.command import command +from autogpt.config import Config +from autogpt.spinner import Spinner +from autogpt.utils import readable_file_size + +CFG = Config() + + +def check_duplicate_operation(operation: str, filename: str) -> bool: + """Check if the operation has already been performed on the given file + + Args: + operation (str): The operation to check for + filename (str): The name of the file to check for + + Returns: + bool: True if the operation has already been performed on the file + """ + log_content = read_file(CFG.file_logger_path) + log_entry = f"{operation}: {filename}\n" + return log_entry in log_content + + +def log_operation(operation: str, filename: str) -> None: + """Log the file operation to the file_logger.txt + + Args: + operation (str): The operation to log + filename (str): The name of the file the operation was performed on + """ + log_entry = f"{operation}: {filename}\n" + append_to_file(CFG.file_logger_path, log_entry, should_log=False) + + +def split_file( + content: str, max_length: int = 4000, overlap: int = 0 +) -> Generator[str, None, None]: + """ + Split text into chunks of a specified maximum length with a specified overlap + between chunks. + + :param content: The input text to be split into chunks + :param max_length: The maximum length of each chunk, + default is 4000 (about 1k token) + :param overlap: The number of overlapping characters between chunks, + default is no overlap + :return: A generator yielding chunks of text + """ + start = 0 + content_length = len(content) + + while start < content_length: + end = start + max_length + if end + overlap < content_length: + chunk = content[start : end + overlap - 1] + else: + chunk = content[start:content_length] + + # Account for the case where the last chunk is shorter than the overlap, so it has already been consumed + if len(chunk) <= overlap: + break + + yield chunk + start += max_length - overlap + + +@command("read_file", "Read file", '"filename": ""') +def read_file(filename: str) -> str: + """Read a file and return the contents + + Args: + filename (str): The name of the file to read + + Returns: + str: The contents of the file + """ + try: + with open(filename, "r", encoding="utf-8") as f: + content = f.read() + return content + except Exception as e: + return f"Error: {str(e)}" + + +def ingest_file( + filename: str, memory, max_length: int = 4000, overlap: int = 200 +) -> None: + """ + Ingest a file by reading its content, splitting it into chunks with a specified + maximum length and overlap, and adding the chunks to the memory storage. + + :param filename: The name of the file to ingest + :param memory: An object with an add() method to store the chunks in memory + :param max_length: The maximum length of each chunk, default is 4000 + :param overlap: The number of overlapping characters between chunks, default is 200 + """ + try: + print(f"Working with file {filename}") + content = read_file(filename) + content_length = len(content) + print(f"File length: {content_length} characters") + + chunks = list(split_file(content, max_length=max_length, overlap=overlap)) + + num_chunks = len(chunks) + for i, chunk in enumerate(chunks): + print(f"Ingesting chunk {i + 1} / {num_chunks} into memory") + memory_to_add = ( + f"Filename: {filename}\n" f"Content part#{i + 1}/{num_chunks}: {chunk}" + ) + + memory.add(memory_to_add) + + print(f"Done ingesting {num_chunks} chunks from {filename}.") + except Exception as e: + print(f"Error while ingesting file '{filename}': {str(e)}") + + +@command("write_to_file", "Write to file", '"filename": "", "text": ""') +def write_to_file(filename: str, text: str) -> str: + """Write text to a file + + Args: + filename (str): The name of the file to write to + text (str): The text to write to the file + + Returns: + str: A message indicating success or failure + """ + if check_duplicate_operation("write", filename): + return "Error: File has already been updated." + try: + directory = os.path.dirname(filename) + if not os.path.exists(directory): + os.makedirs(directory) + with open(filename, "w", encoding="utf-8") as f: + f.write(text) + log_operation("write", filename) + return "File written to successfully." + except Exception as e: + return f"Error: {str(e)}" + + +@command( + "append_to_file", "Append to file", '"filename": "", "text": ""' +) +def append_to_file(filename: str, text: str, should_log: bool = True) -> str: + """Append text to a file + + Args: + filename (str): The name of the file to append to + text (str): The text to append to the file + should_log (bool): Should log output + + Returns: + str: A message indicating success or failure + """ + try: + with open(filename, "a") as f: + f.write(text) + + if should_log: + log_operation("append", filename) + + return "Text appended successfully." + except Exception as e: + return f"Error: {str(e)}" + + +@command("delete_file", "Delete file", '"filename": ""') +def delete_file(filename: str) -> str: + """Delete a file + + Args: + filename (str): The name of the file to delete + + Returns: + str: A message indicating success or failure + """ + if check_duplicate_operation("delete", filename): + return "Error: File has already been deleted." + try: + os.remove(filename) + log_operation("delete", filename) + return "File deleted successfully." + except Exception as e: + return f"Error: {str(e)}" + + +@command("search_files", "Search Files", '"directory": ""') +def search_files(directory: str) -> list[str]: + """Search for files in a directory + + Args: + directory (str): The directory to search in + + Returns: + list[str]: A list of files found in the directory + """ + found_files = [] + + for root, _, files in os.walk(directory): + for file in files: + if file.startswith("."): + continue + relative_path = os.path.relpath( + os.path.join(root, file), CFG.workspace_path + ) + found_files.append(relative_path) + + return found_files + + +@command( + "download_file", + "Download File", + '"url": "", "filename": ""', + CFG.allow_downloads, + "Error: You do not have user authorization to download files locally.", +) +def download_file(url, filename): + """Downloads a file + Args: + url (str): URL of the file to download + filename (str): Filename to save the file as + """ + try: + message = f"{Fore.YELLOW}Downloading file from {Back.MAGENTA}{url}{Back.RESET}{Fore.RESET}" + with Spinner(message) as spinner: + session = requests.Session() + retry = Retry(total=3, backoff_factor=1, status_forcelist=[502, 503, 504]) + adapter = HTTPAdapter(max_retries=retry) + session.mount("http://", adapter) + session.mount("https://", adapter) + + total_size = 0 + downloaded_size = 0 + + with session.get(url, allow_redirects=True, stream=True) as r: + r.raise_for_status() + total_size = int(r.headers.get("Content-Length", 0)) + downloaded_size = 0 + + with open(filename, "wb") as f: + for chunk in r.iter_content(chunk_size=8192): + f.write(chunk) + downloaded_size += len(chunk) + + # Update the progress message + progress = f"{readable_file_size(downloaded_size)} / {readable_file_size(total_size)}" + spinner.update_message(f"{message} {progress}") + + return f'Successfully downloaded and locally stored file: "{filename}"! (Size: {readable_file_size(total_size)})' + except requests.HTTPError as e: + return f"Got an HTTP Error whilst trying to download file: {e}" + except Exception as e: + return "Error: " + str(e) diff --git a/autogpt/commands/git_operations.py b/autogpt/commands/git_operations.py new file mode 100644 index 0000000..c373b8c --- /dev/null +++ b/autogpt/commands/git_operations.py @@ -0,0 +1,33 @@ +"""Git operations for autogpt""" +from git.repo import Repo + +from autogpt.commands.command import command +from autogpt.config import Config + +CFG = Config() + + +@command( + "clone_repository", + "Clone Repository", + '"repository_url": "", "clone_path": ""', + CFG.github_username and CFG.github_api_key, + "Configure github_username and github_api_key.", +) +def clone_repository(repository_url: str, clone_path: str) -> str: + """Clone a GitHub repository locally. + + Args: + repository_url (str): The URL of the repository to clone. + clone_path (str): The path to clone the repository to. + + Returns: + str: The result of the clone operation. + """ + split_url = repository_url.split("//") + auth_repo_url = f"//{CFG.github_username}:{CFG.github_api_key}@".join(split_url) + try: + Repo.clone_from(auth_repo_url, clone_path) + return f"""Cloned {repository_url} to {clone_path}""" + except Exception as e: + return f"Error: {str(e)}" diff --git a/autogpt/commands/google_search.py b/autogpt/commands/google_search.py new file mode 100644 index 0000000..264daaf --- /dev/null +++ b/autogpt/commands/google_search.py @@ -0,0 +1,117 @@ +"""Google search command for Autogpt.""" +from __future__ import annotations + +import json + +from duckduckgo_search import ddg + +from autogpt.commands.command import command +from autogpt.config import Config + +CFG = Config() + + +@command("google", "Google Search", '"query": ""', not CFG.google_api_key) +def google_search(query: str, num_results: int = 8) -> str: + """Return the results of a Google search + + Args: + query (str): The search query. + num_results (int): The number of results to return. + + Returns: + str: The results of the search. + """ + search_results = [] + if not query: + return json.dumps(search_results) + + results = ddg(query, max_results=num_results) + if not results: + return json.dumps(search_results) + + for j in results: + search_results.append(j) + + results = json.dumps(search_results, ensure_ascii=False, indent=4) + return safe_google_results(results) + + +@command( + "google", + "Google Search", + '"query": ""', + bool(CFG.google_api_key), + "Configure google_api_key.", +) +def google_official_search(query: str, num_results: int = 8) -> str | list[str]: + """Return the results of a Google search using the official Google API + + Args: + query (str): The search query. + num_results (int): The number of results to return. + + Returns: + str: The results of the search. + """ + + from googleapiclient.discovery import build + from googleapiclient.errors import HttpError + + try: + # Get the Google API key and Custom Search Engine ID from the config file + api_key = CFG.google_api_key + custom_search_engine_id = CFG.custom_search_engine_id + + # Initialize the Custom Search API service + service = build("customsearch", "v1", developerKey=api_key) + + # Send the search query and retrieve the results + result = ( + service.cse() + .list(q=query, cx=custom_search_engine_id, num=num_results) + .execute() + ) + + # Extract the search result items from the response + search_results = result.get("items", []) + + # Create a list of only the URLs from the search results + search_results_links = [item["link"] for item in search_results] + + except HttpError as e: + # Handle errors in the API call + error_details = json.loads(e.content.decode()) + + # Check if the error is related to an invalid or missing API key + if error_details.get("error", {}).get( + "code" + ) == 403 and "invalid API key" in error_details.get("error", {}).get( + "message", "" + ): + return "Error: The provided Google API key is invalid or missing." + else: + return f"Error: {e}" + # google_result can be a list or a string depending on the search results + + # Return the list of search result URLs + return safe_google_results(search_results_links) + + +def safe_google_results(results: str | list) -> str: + """ + Return the results of a google search in a safe format. + + Args: + results (str | list): The search results. + + Returns: + str: The results of the search. + """ + if isinstance(results, list): + safe_message = json.dumps( + [result.encode("utf-8", "ignore") for result in results] + ) + else: + safe_message = results.encode("utf-8", "ignore").decode("utf-8") + return safe_message diff --git a/autogpt/commands/image_gen.py b/autogpt/commands/image_gen.py new file mode 100644 index 0000000..834432c --- /dev/null +++ b/autogpt/commands/image_gen.py @@ -0,0 +1,164 @@ +""" Image Generation Module for AutoGPT.""" +import io +import uuid +from base64 import b64decode + +import openai +import requests +from PIL import Image + +from autogpt.commands.command import command +from autogpt.config import Config + +CFG = Config() + + +@command("generate_image", "Generate Image", '"prompt": ""', CFG.image_provider) +def generate_image(prompt: str, size: int = 256) -> str: + """Generate an image from a prompt. + + Args: + prompt (str): The prompt to use + size (int, optional): The size of the image. Defaults to 256. (Not supported by HuggingFace) + + Returns: + str: The filename of the image + """ + filename = f"{CFG.workspace_path}/{str(uuid.uuid4())}.jpg" + + # DALL-E + if CFG.image_provider == "dalle": + return generate_image_with_dalle(prompt, filename, size) + # HuggingFace + elif CFG.image_provider == "huggingface": + return generate_image_with_hf(prompt, filename) + # SD WebUI + elif CFG.image_provider == "sdwebui": + return generate_image_with_sd_webui(prompt, filename, size) + return "No Image Provider Set" + + +def generate_image_with_hf(prompt: str, filename: str) -> str: + """Generate an image with HuggingFace's API. + + Args: + prompt (str): The prompt to use + filename (str): The filename to save the image to + + Returns: + str: The filename of the image + """ + API_URL = ( + f"https://api-inference.huggingface.co/models/{CFG.huggingface_image_model}" + ) + if CFG.huggingface_api_token is None: + raise ValueError( + "You need to set your Hugging Face API token in the config file." + ) + headers = { + "Authorization": f"Bearer {CFG.huggingface_api_token}", + "X-Use-Cache": "false", + } + + response = requests.post( + API_URL, + headers=headers, + json={ + "inputs": prompt, + }, + ) + + image = Image.open(io.BytesIO(response.content)) + print(f"Image Generated for prompt:{prompt}") + + image.save(filename) + + return f"Saved to disk:{filename}" + + +def generate_image_with_dalle(prompt: str, filename: str, size: int) -> str: + """Generate an image with DALL-E. + + Args: + prompt (str): The prompt to use + filename (str): The filename to save the image to + size (int): The size of the image + + Returns: + str: The filename of the image + """ + openai.api_key = CFG.openai_api_key + + # Check for supported image sizes + if size not in [256, 512, 1024]: + closest = min([256, 512, 1024], key=lambda x: abs(x - size)) + print( + f"DALL-E only supports image sizes of 256x256, 512x512, or 1024x1024. Setting to {closest}, was {size}." + ) + size = closest + + response = openai.Image.create( + prompt=prompt, + n=1, + size=f"{size}x{size}", + response_format="b64_json", + ) + + print(f"Image Generated for prompt:{prompt}") + + image_data = b64decode(response["data"][0]["b64_json"]) + + with open(filename, mode="wb") as png: + png.write(image_data) + + return f"Saved to disk:{filename}" + + +def generate_image_with_sd_webui( + prompt: str, + filename: str, + size: int = 512, + negative_prompt: str = "", + extra: dict = {}, +) -> str: + """Generate an image with Stable Diffusion webui. + Args: + prompt (str): The prompt to use + filename (str): The filename to save the image to + size (int, optional): The size of the image. Defaults to 256. + negative_prompt (str, optional): The negative prompt to use. Defaults to "". + extra (dict, optional): Extra parameters to pass to the API. Defaults to {}. + Returns: + str: The filename of the image + """ + # Create a session and set the basic auth if needed + s = requests.Session() + if CFG.sd_webui_auth: + username, password = CFG.sd_webui_auth.split(":") + s.auth = (username, password or "") + + # Generate the images + response = requests.post( + f"{CFG.sd_webui_url}/sdapi/v1/txt2img", + json={ + "prompt": prompt, + "negative_prompt": negative_prompt, + "sampler_index": "DDIM", + "steps": 20, + "cfg_scale": 7.0, + "width": size, + "height": size, + "n_iter": 1, + **extra, + }, + ) + + print(f"Image Generated for prompt:{prompt}") + + # Save the image to disk + response = response.json() + b64 = b64decode(response["images"][0].split(",", 1)[0]) + image = Image.open(io.BytesIO(b64)) + image.save(filename) + + return f"Saved to disk:{filename}" diff --git a/autogpt/commands/improve_code.py b/autogpt/commands/improve_code.py new file mode 100644 index 0000000..f953cf2 --- /dev/null +++ b/autogpt/commands/improve_code.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +import json + +from autogpt.commands.command import command +from autogpt.llm_utils import call_ai_function + + +@command( + "improve_code", + "Get Improved Code", + '"suggestions": "", "code": ""', +) +def improve_code(suggestions: list[str], code: str) -> str: + """ + A function that takes in code and suggestions and returns a response from create + chat completion api call. + + Parameters: + suggestions (list): A list of suggestions around what needs to be improved. + code (str): Code to be improved. + Returns: + A result string from create chat completion. Improved code in response. + """ + + function_string = ( + "def generate_improved_code(suggestions: list[str], code: str) -> str:" + ) + args = [json.dumps(suggestions), code] + description_string = ( + "Improves the provided code based on the suggestions" + " provided, making no other changes." + ) + + return call_ai_function(function_string, args, description_string) diff --git a/autogpt/commands/times.py b/autogpt/commands/times.py new file mode 100644 index 0000000..3c9b8a4 --- /dev/null +++ b/autogpt/commands/times.py @@ -0,0 +1,10 @@ +from datetime import datetime + + +def get_datetime() -> str: + """Return the current date and time + + Returns: + str: The current date and time + """ + return "Current date and time: " + datetime.now().strftime("%Y-%m-%d %H:%M:%S") diff --git a/autogpt/commands/twitter.py b/autogpt/commands/twitter.py new file mode 100644 index 0000000..f050227 --- /dev/null +++ b/autogpt/commands/twitter.py @@ -0,0 +1,44 @@ +"""A module that contains a command to send a tweet.""" +import os + +import tweepy +from dotenv import load_dotenv + +from autogpt.commands.command import command + +load_dotenv() + + +@command( + "send_tweet", + "Send Tweet", + '"tweet_text": ""', +) +def send_tweet(tweet_text: str) -> str: + """ + A function that takes in a string and returns a response from create chat + completion api call. + + Args: + tweet_text (str): Text to be tweeted. + + Returns: + A result from sending the tweet. + """ + consumer_key = os.environ.get("TW_CONSUMER_KEY") + consumer_secret = os.environ.get("TW_CONSUMER_SECRET") + access_token = os.environ.get("TW_ACCESS_TOKEN") + access_token_secret = os.environ.get("TW_ACCESS_TOKEN_SECRET") + # Authenticate to Twitter + auth = tweepy.OAuthHandler(consumer_key, consumer_secret) + auth.set_access_token(access_token, access_token_secret) + + # Create API object + api = tweepy.API(auth) + + # Send tweet + try: + api.update_status(tweet_text) + return "Tweet sent successfully!" + except tweepy.TweepyException as e: + return f"Error sending tweet: {e.reason}" diff --git a/autogpt/commands/web_playwright.py b/autogpt/commands/web_playwright.py new file mode 100644 index 0000000..4e388de --- /dev/null +++ b/autogpt/commands/web_playwright.py @@ -0,0 +1,80 @@ +"""Web scraping commands using Playwright""" +from __future__ import annotations + +try: + from playwright.sync_api import sync_playwright +except ImportError: + print( + "Playwright not installed. Please install it with 'pip install playwright' to use." + ) +from bs4 import BeautifulSoup + +from autogpt.processing.html import extract_hyperlinks, format_hyperlinks + + +def scrape_text(url: str) -> str: + """Scrape text from a webpage + + Args: + url (str): The URL to scrape text from + + Returns: + str: The scraped text + """ + with sync_playwright() as p: + browser = p.chromium.launch() + page = browser.new_page() + + try: + page.goto(url) + html_content = page.content() + soup = BeautifulSoup(html_content, "html.parser") + + for script in soup(["script", "style"]): + script.extract() + + text = soup.get_text() + lines = (line.strip() for line in text.splitlines()) + chunks = (phrase.strip() for line in lines for phrase in line.split(" ")) + text = "\n".join(chunk for chunk in chunks if chunk) + + except Exception as e: + text = f"Error: {str(e)}" + + finally: + browser.close() + + return text + + +def scrape_links(url: str) -> str | list[str]: + """Scrape links from a webpage + + Args: + url (str): The URL to scrape links from + + Returns: + Union[str, List[str]]: The scraped links + """ + with sync_playwright() as p: + browser = p.chromium.launch() + page = browser.new_page() + + try: + page.goto(url) + html_content = page.content() + soup = BeautifulSoup(html_content, "html.parser") + + for script in soup(["script", "style"]): + script.extract() + + hyperlinks = extract_hyperlinks(soup, url) + formatted_links = format_hyperlinks(hyperlinks) + + except Exception as e: + formatted_links = f"Error: {str(e)}" + + finally: + browser.close() + + return formatted_links diff --git a/autogpt/commands/web_requests.py b/autogpt/commands/web_requests.py new file mode 100644 index 0000000..f3ad019 --- /dev/null +++ b/autogpt/commands/web_requests.py @@ -0,0 +1,188 @@ +"""Browse a webpage and summarize it using the LLM model""" +from __future__ import annotations + +from urllib.parse import urljoin, urlparse + +import requests +from bs4 import BeautifulSoup +from requests import Response +from requests.compat import urljoin + +from autogpt.config import Config +from autogpt.processing.html import extract_hyperlinks, format_hyperlinks + +CFG = Config() + +session = requests.Session() +session.headers.update({"User-Agent": CFG.user_agent}) + + +def is_valid_url(url: str) -> bool: + """Check if the URL is valid + + Args: + url (str): The URL to check + + Returns: + bool: True if the URL is valid, False otherwise + """ + try: + result = urlparse(url) + return all([result.scheme, result.netloc]) + except ValueError: + return False + + +def sanitize_url(url: str) -> str: + """Sanitize the URL + + Args: + url (str): The URL to sanitize + + Returns: + str: The sanitized URL + """ + return urljoin(url, urlparse(url).path) + + +def check_local_file_access(url: str) -> bool: + """Check if the URL is a local file + + Args: + url (str): The URL to check + + Returns: + bool: True if the URL is a local file, False otherwise + """ + local_prefixes = [ + "file:///", + "file://localhost/", + "file://localhost", + "http://localhost", + "http://localhost/", + "https://localhost", + "https://localhost/", + "http://2130706433", + "http://2130706433/", + "https://2130706433", + "https://2130706433/", + "http://127.0.0.1/", + "http://127.0.0.1", + "https://127.0.0.1/", + "https://127.0.0.1", + "https://0.0.0.0/", + "https://0.0.0.0", + "http://0.0.0.0/", + "http://0.0.0.0", + "http://0000", + "http://0000/", + "https://0000", + "https://0000/", + ] + return any(url.startswith(prefix) for prefix in local_prefixes) + + +def get_response( + url: str, timeout: int = 10 +) -> tuple[None, str] | tuple[Response, None]: + """Get the response from a URL + + Args: + url (str): The URL to get the response from + timeout (int): The timeout for the HTTP request + + Returns: + tuple[None, str] | tuple[Response, None]: The response and error message + + Raises: + ValueError: If the URL is invalid + requests.exceptions.RequestException: If the HTTP request fails + """ + try: + # Restrict access to local files + if check_local_file_access(url): + raise ValueError("Access to local files is restricted") + + # Most basic check if the URL is valid: + if not url.startswith("http://") and not url.startswith("https://"): + raise ValueError("Invalid URL format") + + sanitized_url = sanitize_url(url) + + response = session.get(sanitized_url, timeout=timeout) + + # Check if the response contains an HTTP error + if response.status_code >= 400: + return None, f"Error: HTTP {str(response.status_code)} error" + + return response, None + except ValueError as ve: + # Handle invalid URL format + return None, f"Error: {str(ve)}" + + except requests.exceptions.RequestException as re: + # Handle exceptions related to the HTTP request + # (e.g., connection errors, timeouts, etc.) + return None, f"Error: {str(re)}" + + +def scrape_text(url: str) -> str: + """Scrape text from a webpage + + Args: + url (str): The URL to scrape text from + + Returns: + str: The scraped text + """ + response, error_message = get_response(url) + if error_message: + return error_message + if not response: + return "Error: Could not get response" + + soup = BeautifulSoup(response.text, "html.parser") + + for script in soup(["script", "style"]): + script.extract() + + text = soup.get_text() + lines = (line.strip() for line in text.splitlines()) + chunks = (phrase.strip() for line in lines for phrase in line.split(" ")) + text = "\n".join(chunk for chunk in chunks if chunk) + + return text + + +def scrape_links(url: str) -> str | list[str]: + """Scrape links from a webpage + + Args: + url (str): The URL to scrape links from + + Returns: + str | list[str]: The scraped links + """ + response, error_message = get_response(url) + if error_message: + return error_message + if not response: + return "Error: Could not get response" + soup = BeautifulSoup(response.text, "html.parser") + + for script in soup(["script", "style"]): + script.extract() + + hyperlinks = extract_hyperlinks(soup, url) + + return format_hyperlinks(hyperlinks) + + +def create_message(chunk, question): + """Create a message for the user to summarize a chunk of text""" + return { + "role": "user", + "content": f'"""{chunk}""" Using the above text, answer the following' + f' question: "{question}" -- if the question cannot be answered using the' + " text, summarize the text.", + } diff --git a/autogpt/commands/web_selenium.py b/autogpt/commands/web_selenium.py new file mode 100644 index 0000000..e0e0d70 --- /dev/null +++ b/autogpt/commands/web_selenium.py @@ -0,0 +1,160 @@ +"""Selenium web scraping module.""" +from __future__ import annotations + +import logging +from pathlib import Path +from sys import platform + +from bs4 import BeautifulSoup +from selenium import webdriver +from selenium.webdriver.chrome.options import Options as ChromeOptions +from selenium.webdriver.common.by import By +from selenium.webdriver.firefox.options import Options as FirefoxOptions +from selenium.webdriver.remote.webdriver import WebDriver +from selenium.webdriver.safari.options import Options as SafariOptions +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.wait import WebDriverWait +from webdriver_manager.chrome import ChromeDriverManager +from webdriver_manager.firefox import GeckoDriverManager + +import autogpt.processing.text as summary +from autogpt.commands.command import command +from autogpt.config import Config +from autogpt.processing.html import extract_hyperlinks, format_hyperlinks + +FILE_DIR = Path(__file__).parent.parent +CFG = Config() + + +@command( + "browse_website", + "Browse Website", + '"url": "", "question": ""', +) +def browse_website(url: str, question: str) -> tuple[str, WebDriver]: + """Browse a website and return the answer and links to the user + + Args: + url (str): The url of the website to browse + question (str): The question asked by the user + + Returns: + Tuple[str, WebDriver]: The answer and links to the user and the webdriver + """ + driver, text = scrape_text_with_selenium(url) + add_header(driver) + summary_text = summary.summarize_text(url, text, question, driver) + links = scrape_links_with_selenium(driver, url) + + # Limit links to 5 + if len(links) > 5: + links = links[:5] + close_browser(driver) + return f"Answer gathered from website: {summary_text} \n \n Links: {links}", driver + + +def scrape_text_with_selenium(url: str) -> tuple[WebDriver, str]: + """Scrape text from a website using selenium + + Args: + url (str): The url of the website to scrape + + Returns: + Tuple[WebDriver, str]: The webdriver and the text scraped from the website + """ + logging.getLogger("selenium").setLevel(logging.CRITICAL) + + options_available = { + "chrome": ChromeOptions, + "safari": SafariOptions, + "firefox": FirefoxOptions, + } + + options = options_available[CFG.selenium_web_browser]() + options.add_argument( + "user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.5615.49 Safari/537.36" + ) + + if CFG.selenium_web_browser == "firefox": + driver = webdriver.Firefox( + executable_path=GeckoDriverManager().install(), options=options + ) + elif CFG.selenium_web_browser == "safari": + # Requires a bit more setup on the users end + # See https://developer.apple.com/documentation/webkit/testing_with_webdriver_in_safari + driver = webdriver.Safari(options=options) + else: + if platform == "linux" or platform == "linux2": + options.add_argument("--disable-dev-shm-usage") + options.add_argument("--remote-debugging-port=9222") + + options.add_argument("--no-sandbox") + if CFG.selenium_headless: + options.add_argument("--headless") + options.add_argument("--disable-gpu") + + driver = webdriver.Chrome( + executable_path=ChromeDriverManager().install(), options=options + ) + driver.get(url) + + WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.TAG_NAME, "body")) + ) + + # Get the HTML content directly from the browser's DOM + page_source = driver.execute_script("return document.body.outerHTML;") + soup = BeautifulSoup(page_source, "html.parser") + + for script in soup(["script", "style"]): + script.extract() + + text = soup.get_text() + lines = (line.strip() for line in text.splitlines()) + chunks = (phrase.strip() for line in lines for phrase in line.split(" ")) + text = "\n".join(chunk for chunk in chunks if chunk) + return driver, text + + +def scrape_links_with_selenium(driver: WebDriver, url: str) -> list[str]: + """Scrape links from a website using selenium + + Args: + driver (WebDriver): The webdriver to use to scrape the links + + Returns: + List[str]: The links scraped from the website + """ + page_source = driver.page_source + soup = BeautifulSoup(page_source, "html.parser") + + for script in soup(["script", "style"]): + script.extract() + + hyperlinks = extract_hyperlinks(soup, url) + + return format_hyperlinks(hyperlinks) + + +def close_browser(driver: WebDriver) -> None: + """Close the browser + + Args: + driver (WebDriver): The webdriver to close + + Returns: + None + """ + driver.quit() + + +def add_header(driver: WebDriver) -> None: + """Add a header to the website + + Args: + driver (WebDriver): The webdriver to use to add the header + + Returns: + None + """ + driver.execute_script(open(f"{FILE_DIR}/js/overlay.js", "r").read()) diff --git a/autogpt/commands/write_tests.py b/autogpt/commands/write_tests.py new file mode 100644 index 0000000..91cd930 --- /dev/null +++ b/autogpt/commands/write_tests.py @@ -0,0 +1,37 @@ +"""A module that contains a function to generate test cases for the submitted code.""" +from __future__ import annotations + +import json + +from autogpt.commands.command import command +from autogpt.llm_utils import call_ai_function + + +@command( + "write_tests", + "Write Tests", + '"code": "", "focus": ""', +) +def write_tests(code: str, focus: list[str]) -> str: + """ + A function that takes in code and focus topics and returns a response from create + chat completion api call. + + Parameters: + focus (list): A list of suggestions around what needs to be improved. + code (str): Code for test cases to be generated against. + Returns: + A result string from create chat completion. Test cases for the submitted code + in response. + """ + + function_string = ( + "def create_test_cases(code: str, focus: Optional[str] = None) -> str:" + ) + args = [code, json.dumps(focus)] + description_string = ( + "Generates test cases for the existing code, focusing on" + " specific areas if required." + ) + + return call_ai_function(function_string, args, description_string) diff --git a/autogpt/config/__init__.py b/autogpt/config/__init__.py new file mode 100644 index 0000000..726b6dc --- /dev/null +++ b/autogpt/config/__init__.py @@ -0,0 +1,14 @@ +""" +This module contains the configuration classes for AutoGPT. +""" +from autogpt.config.ai_config import AIConfig +from autogpt.config.config import Config, check_openai_api_key +from autogpt.config.singleton import AbstractSingleton, Singleton + +__all__ = [ + "check_openai_api_key", + "AbstractSingleton", + "AIConfig", + "Config", + "Singleton", +] diff --git a/autogpt/config/ai_config.py b/autogpt/config/ai_config.py new file mode 100644 index 0000000..d662429 --- /dev/null +++ b/autogpt/config/ai_config.py @@ -0,0 +1,163 @@ +# sourcery skip: do-not-use-staticmethod +""" +A module that contains the AIConfig class object that contains the configuration +""" +from __future__ import annotations + +import os +import platform +from pathlib import Path +from typing import Optional, Type + +import distro +import yaml + +from autogpt.prompts.generator import PromptGenerator + +# Soon this will go in a folder where it remembers more stuff about the run(s) +SAVE_FILE = str(Path(os.getcwd()) / "ai_settings.yaml") + + +class AIConfig: + """ + A class object that contains the configuration information for the AI + + Attributes: + ai_name (str): The name of the AI. + ai_role (str): The description of the AI's role. + ai_goals (list): The list of objectives the AI is supposed to complete. + api_budget (float): The maximum dollar value for API calls (0.0 means infinite) + """ + + def __init__( + self, + ai_name: str = "", + ai_role: str = "", + ai_goals: list | None = None, + api_budget: float = 0.0, + ) -> None: + """ + Initialize a class instance + + Parameters: + ai_name (str): The name of the AI. + ai_role (str): The description of the AI's role. + ai_goals (list): The list of objectives the AI is supposed to complete. + api_budget (float): The maximum dollar value for API calls (0.0 means infinite) + Returns: + None + """ + if ai_goals is None: + ai_goals = [] + self.ai_name = ai_name + self.ai_role = ai_role + self.ai_goals = ai_goals + self.api_budget = api_budget + self.prompt_generator = None + self.command_registry = None + + @staticmethod + def load(config_file: str = SAVE_FILE) -> "AIConfig": + """ + Returns class object with parameters (ai_name, ai_role, ai_goals, api_budget) loaded from + yaml file if yaml file exists, + else returns class with no parameters. + + Parameters: + config_file (int): The path to the config yaml file. + DEFAULT: "../ai_settings.yaml" + + Returns: + cls (object): An instance of given cls object + """ + + try: + with open(config_file, encoding="utf-8") as file: + config_params = yaml.load(file, Loader=yaml.FullLoader) + except FileNotFoundError: + config_params = {} + + ai_name = config_params.get("ai_name", "") + ai_role = config_params.get("ai_role", "") + ai_goals = config_params.get("ai_goals", []) + api_budget = config_params.get("api_budget", 0.0) + # type: Type[AIConfig] + return AIConfig(ai_name, ai_role, ai_goals, api_budget) + + def save(self, config_file: str = SAVE_FILE) -> None: + """ + Saves the class parameters to the specified file yaml file path as a yaml file. + + Parameters: + config_file(str): The path to the config yaml file. + DEFAULT: "../ai_settings.yaml" + + Returns: + None + """ + + config = { + "ai_name": self.ai_name, + "ai_role": self.ai_role, + "ai_goals": self.ai_goals, + "api_budget": self.api_budget, + } + with open(config_file, "w", encoding="utf-8") as file: + yaml.dump(config, file, allow_unicode=True) + + def construct_full_prompt( + self, prompt_generator: Optional[PromptGenerator] = None + ) -> str: + """ + Returns a prompt to the user with the class information in an organized fashion. + + Parameters: + None + + Returns: + full_prompt (str): A string containing the initial prompt for the user + including the ai_name, ai_role, ai_goals, and api_budget. + """ + + prompt_start = ( + "Your decisions must always be made independently without" + " seeking user assistance. Play to your strengths as an LLM and pursue" + " simple strategies with no legal complications." + "" + ) + + from autogpt.config import Config + from autogpt.prompts.prompt import build_default_prompt_generator + + cfg = Config() + if prompt_generator is None: + prompt_generator = build_default_prompt_generator() + prompt_generator.goals = self.ai_goals + prompt_generator.name = self.ai_name + prompt_generator.role = self.ai_role + prompt_generator.command_registry = self.command_registry + for plugin in cfg.plugins: + if not plugin.can_handle_post_prompt(): + continue + prompt_generator = plugin.post_prompt(prompt_generator) + + if cfg.execute_local_commands: + # add OS info to prompt + os_name = platform.system() + os_info = ( + platform.platform(terse=True) + if os_name != "Linux" + else distro.name(pretty=True) + ) + + prompt_start += f"\nThe OS you are running on is: {os_info}" + + # Construct full prompt + full_prompt = f"You are {prompt_generator.name}, {prompt_generator.role}\n{prompt_start}\n\nGOALS:\n\n" + for i, goal in enumerate(self.ai_goals): + full_prompt += f"{i+1}. {goal}\n" + if self.api_budget > 0.0: + full_prompt += f"\nIt takes money to let you run. Your API budget is ${self.api_budget:.3f}" + self.prompt_generator = prompt_generator + full_prompt += f"\n\n{prompt_generator.generate_prompt_string()}" + return full_prompt diff --git a/autogpt/config/config.py b/autogpt/config/config.py new file mode 100644 index 0000000..7fa849e --- /dev/null +++ b/autogpt/config/config.py @@ -0,0 +1,282 @@ +"""Configuration class to store the state of bools for different scripts access.""" +import os +from typing import List + +import openai +import yaml +from auto_gpt_plugin_template import AutoGPTPluginTemplate +from colorama import Fore +from dotenv import load_dotenv + +from autogpt.config.singleton import Singleton + +load_dotenv(verbose=True, override=True) + + +class Config(metaclass=Singleton): + """ + Configuration class to store the state of bools for different scripts access. + """ + + def __init__(self) -> None: + """Initialize the Config class""" + self.workspace_path = None + self.file_logger_path = None + + self.debug_mode = False + self.continuous_mode = False + self.continuous_limit = 0 + self.speak_mode = False + self.skip_reprompt = False + self.allow_downloads = False + self.skip_news = False + + self.ai_settings_file = os.getenv("AI_SETTINGS_FILE", "ai_settings.yaml") + self.fast_llm_model = os.getenv("FAST_LLM_MODEL", "gpt-3.5-turbo") + self.smart_llm_model = os.getenv("SMART_LLM_MODEL", "gpt-4") + self.fast_token_limit = int(os.getenv("FAST_TOKEN_LIMIT", 4000)) + self.smart_token_limit = int(os.getenv("SMART_TOKEN_LIMIT", 8000)) + self.browse_chunk_max_length = int(os.getenv("BROWSE_CHUNK_MAX_LENGTH", 3000)) + self.browse_spacy_language_model = os.getenv( + "BROWSE_SPACY_LANGUAGE_MODEL", "en_core_web_sm" + ) + + self.openai_api_key = os.getenv("OPENAI_API_KEY") + self.temperature = float(os.getenv("TEMPERATURE", "0")) + self.use_azure = os.getenv("USE_AZURE") == "True" + self.execute_local_commands = ( + os.getenv("EXECUTE_LOCAL_COMMANDS", "False") == "True" + ) + self.restrict_to_workspace = ( + os.getenv("RESTRICT_TO_WORKSPACE", "True") == "True" + ) + + if self.use_azure: + self.load_azure_config() + openai.api_type = self.openai_api_type + openai.api_base = self.openai_api_base + openai.api_version = self.openai_api_version + + self.elevenlabs_api_key = os.getenv("ELEVENLABS_API_KEY") + self.elevenlabs_voice_1_id = os.getenv("ELEVENLABS_VOICE_1_ID") + self.elevenlabs_voice_2_id = os.getenv("ELEVENLABS_VOICE_2_ID") + + self.use_mac_os_tts = False + self.use_mac_os_tts = os.getenv("USE_MAC_OS_TTS") + + self.use_brian_tts = False + self.use_brian_tts = os.getenv("USE_BRIAN_TTS") + + self.github_api_key = os.getenv("GITHUB_API_KEY") + self.github_username = os.getenv("GITHUB_USERNAME") + + self.google_api_key = os.getenv("GOOGLE_API_KEY") + self.custom_search_engine_id = os.getenv("CUSTOM_SEARCH_ENGINE_ID") + + self.pinecone_api_key = os.getenv("PINECONE_API_KEY") + self.pinecone_region = os.getenv("PINECONE_ENV") + + self.weaviate_host = os.getenv("WEAVIATE_HOST") + self.weaviate_port = os.getenv("WEAVIATE_PORT") + self.weaviate_protocol = os.getenv("WEAVIATE_PROTOCOL", "http") + self.weaviate_username = os.getenv("WEAVIATE_USERNAME", None) + self.weaviate_password = os.getenv("WEAVIATE_PASSWORD", None) + self.weaviate_scopes = os.getenv("WEAVIATE_SCOPES", None) + self.weaviate_embedded_path = os.getenv("WEAVIATE_EMBEDDED_PATH") + self.weaviate_api_key = os.getenv("WEAVIATE_API_KEY", None) + self.use_weaviate_embedded = ( + os.getenv("USE_WEAVIATE_EMBEDDED", "False") == "True" + ) + + # milvus or zilliz cloud configuration. + self.milvus_addr = os.getenv("MILVUS_ADDR", "localhost:19530") + self.milvus_username = os.getenv("MILVUS_USERNAME") + self.milvus_password = os.getenv("MILVUS_PASSWORD") + self.milvus_collection = os.getenv("MILVUS_COLLECTION", "autogpt") + self.milvus_secure = os.getenv("MILVUS_SECURE") == "True" + + self.image_provider = os.getenv("IMAGE_PROVIDER") + self.image_size = int(os.getenv("IMAGE_SIZE", 256)) + self.huggingface_api_token = os.getenv("HUGGINGFACE_API_TOKEN") + self.huggingface_image_model = os.getenv( + "HUGGINGFACE_IMAGE_MODEL", "CompVis/stable-diffusion-v1-4" + ) + self.huggingface_audio_to_text_model = os.getenv( + "HUGGINGFACE_AUDIO_TO_TEXT_MODEL" + ) + self.sd_webui_url = os.getenv("SD_WEBUI_URL", "http://localhost:7860") + self.sd_webui_auth = os.getenv("SD_WEBUI_AUTH") + + # Selenium browser settings + self.selenium_web_browser = os.getenv("USE_WEB_BROWSER", "chrome") + self.selenium_headless = os.getenv("HEADLESS_BROWSER", "True") == "True" + + # User agent header to use when making HTTP requests + # Some websites might just completely deny request with an error code if + # no user agent was found. + self.user_agent = os.getenv( + "USER_AGENT", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36" + " (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36", + ) + + self.redis_host = os.getenv("REDIS_HOST", "localhost") + self.redis_port = os.getenv("REDIS_PORT", "6379") + self.redis_password = os.getenv("REDIS_PASSWORD", "") + self.wipe_redis_on_start = os.getenv("WIPE_REDIS_ON_START", "True") == "True" + self.memory_index = os.getenv("MEMORY_INDEX", "auto-gpt") + # Note that indexes must be created on db 0 in redis, this is not configurable. + + self.memory_backend = os.getenv("MEMORY_BACKEND", "local") + # Initialize the OpenAI API client + openai.api_key = self.openai_api_key + + self.plugins_dir = os.getenv("PLUGINS_DIR", "plugins") + self.plugins: List[AutoGPTPluginTemplate] = [] + self.plugins_openai = [] + + plugins_allowlist = os.getenv("ALLOWLISTED_PLUGINS") + if plugins_allowlist: + self.plugins_allowlist = plugins_allowlist.split(",") + else: + self.plugins_allowlist = [] + self.plugins_denylist = [] + + def get_azure_deployment_id_for_model(self, model: str) -> str: + """ + Returns the relevant deployment id for the model specified. + + Parameters: + model(str): The model to map to the deployment id. + + Returns: + The matching deployment id if found, otherwise an empty string. + """ + if model == self.fast_llm_model: + return self.azure_model_to_deployment_id_map[ + "fast_llm_model_deployment_id" + ] # type: ignore + elif model == self.smart_llm_model: + return self.azure_model_to_deployment_id_map[ + "smart_llm_model_deployment_id" + ] # type: ignore + elif model == "text-embedding-ada-002": + return self.azure_model_to_deployment_id_map[ + "embedding_model_deployment_id" + ] # type: ignore + else: + return "" + + AZURE_CONFIG_FILE = os.path.join(os.path.dirname(__file__), "../..", "azure.yaml") + + def load_azure_config(self, config_file: str = AZURE_CONFIG_FILE) -> None: + """ + Loads the configuration parameters for Azure hosting from the specified file + path as a yaml file. + + Parameters: + config_file(str): The path to the config yaml file. DEFAULT: "../azure.yaml" + + Returns: + None + """ + with open(config_file) as file: + config_params = yaml.load(file, Loader=yaml.FullLoader) + self.openai_api_type = config_params.get("azure_api_type") or "azure" + self.openai_api_base = config_params.get("azure_api_base") or "" + self.openai_api_version = ( + config_params.get("azure_api_version") or "2023-03-15-preview" + ) + self.azure_model_to_deployment_id_map = config_params.get("azure_model_map", {}) + + def set_continuous_mode(self, value: bool) -> None: + """Set the continuous mode value.""" + self.continuous_mode = value + + def set_continuous_limit(self, value: int) -> None: + """Set the continuous limit value.""" + self.continuous_limit = value + + def set_speak_mode(self, value: bool) -> None: + """Set the speak mode value.""" + self.speak_mode = value + + def set_fast_llm_model(self, value: str) -> None: + """Set the fast LLM model value.""" + self.fast_llm_model = value + + def set_smart_llm_model(self, value: str) -> None: + """Set the smart LLM model value.""" + self.smart_llm_model = value + + def set_fast_token_limit(self, value: int) -> None: + """Set the fast token limit value.""" + self.fast_token_limit = value + + def set_smart_token_limit(self, value: int) -> None: + """Set the smart token limit value.""" + self.smart_token_limit = value + + def set_browse_chunk_max_length(self, value: int) -> None: + """Set the browse_website command chunk max length value.""" + self.browse_chunk_max_length = value + + def set_openai_api_key(self, value: str) -> None: + """Set the OpenAI API key value.""" + self.openai_api_key = value + + def set_elevenlabs_api_key(self, value: str) -> None: + """Set the ElevenLabs API key value.""" + self.elevenlabs_api_key = value + + def set_elevenlabs_voice_1_id(self, value: str) -> None: + """Set the ElevenLabs Voice 1 ID value.""" + self.elevenlabs_voice_1_id = value + + def set_elevenlabs_voice_2_id(self, value: str) -> None: + """Set the ElevenLabs Voice 2 ID value.""" + self.elevenlabs_voice_2_id = value + + def set_google_api_key(self, value: str) -> None: + """Set the Google API key value.""" + self.google_api_key = value + + def set_custom_search_engine_id(self, value: str) -> None: + """Set the custom search engine id value.""" + self.custom_search_engine_id = value + + def set_pinecone_api_key(self, value: str) -> None: + """Set the Pinecone API key value.""" + self.pinecone_api_key = value + + def set_pinecone_region(self, value: str) -> None: + """Set the Pinecone region value.""" + self.pinecone_region = value + + def set_debug_mode(self, value: bool) -> None: + """Set the debug mode value.""" + self.debug_mode = value + + def set_plugins(self, value: list) -> None: + """Set the plugins value.""" + self.plugins = value + + def set_temperature(self, value: int) -> None: + """Set the temperature value.""" + self.temperature = value + + def set_memory_backend(self, value: int) -> None: + """Set the temperature value.""" + self.memory_backend = value + + +def check_openai_api_key() -> None: + """Check if the OpenAI API key is set in config.py or as an environment variable.""" + cfg = Config() + if not cfg.openai_api_key: + print( + Fore.RED + + "Please set your OpenAI API key in .env or as an environment variable." + ) + print("You can get your key from https://platform.openai.com/account/api-keys") + exit(1) diff --git a/autogpt/config/singleton.py b/autogpt/config/singleton.py new file mode 100644 index 0000000..55b2aee --- /dev/null +++ b/autogpt/config/singleton.py @@ -0,0 +1,24 @@ +"""The singleton metaclass for ensuring only one instance of a class.""" +import abc + + +class Singleton(abc.ABCMeta, type): + """ + Singleton metaclass for ensuring only one instance of a class. + """ + + _instances = {} + + def __call__(cls, *args, **kwargs): + """Call method for the singleton metaclass.""" + if cls not in cls._instances: + cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) + return cls._instances[cls] + + +class AbstractSingleton(abc.ABC, metaclass=Singleton): + """ + Abstract singleton class for ensuring only one instance of a class. + """ + + pass diff --git a/autogpt/configurator.py b/autogpt/configurator.py new file mode 100644 index 0000000..84000e5 --- /dev/null +++ b/autogpt/configurator.py @@ -0,0 +1,134 @@ +"""Configurator module.""" +import click +from colorama import Back, Fore, Style + +from autogpt import utils +from autogpt.config import Config +from autogpt.logs import logger +from autogpt.memory import get_supported_memory_backends + +CFG = Config() + + +def create_config( + continuous: bool, + continuous_limit: int, + ai_settings_file: str, + skip_reprompt: bool, + speak: bool, + debug: bool, + gpt3only: bool, + gpt4only: bool, + memory_type: str, + browser_name: str, + allow_downloads: bool, + skip_news: bool, +) -> None: + """Updates the config object with the given arguments. + + Args: + continuous (bool): Whether to run in continuous mode + continuous_limit (int): The number of times to run in continuous mode + ai_settings_file (str): The path to the ai_settings.yaml file + skip_reprompt (bool): Whether to skip the re-prompting messages at the beginning of the script + speak (bool): Whether to enable speak mode + debug (bool): Whether to enable debug mode + gpt3only (bool): Whether to enable GPT3.5 only mode + gpt4only (bool): Whether to enable GPT4 only mode + memory_type (str): The type of memory backend to use + browser_name (str): The name of the browser to use when using selenium to scrape the web + allow_downloads (bool): Whether to allow Auto-GPT to download files natively + skips_news (bool): Whether to suppress the output of latest news on startup + """ + CFG.set_debug_mode(False) + CFG.set_continuous_mode(False) + CFG.set_speak_mode(False) + + if debug: + logger.typewriter_log("Debug Mode: ", Fore.GREEN, "ENABLED") + CFG.set_debug_mode(True) + + if continuous: + logger.typewriter_log("Continuous Mode: ", Fore.RED, "ENABLED") + logger.typewriter_log( + "WARNING: ", + Fore.RED, + "Continuous mode is not recommended. It is potentially dangerous and may" + " cause your AI to run forever or carry out actions you would not usually" + " authorise. Use at your own risk.", + ) + CFG.set_continuous_mode(True) + + if continuous_limit: + logger.typewriter_log( + "Continuous Limit: ", Fore.GREEN, f"{continuous_limit}" + ) + CFG.set_continuous_limit(continuous_limit) + + # Check if continuous limit is used without continuous mode + if continuous_limit and not continuous: + raise click.UsageError("--continuous-limit can only be used with --continuous") + + if speak: + logger.typewriter_log("Speak Mode: ", Fore.GREEN, "ENABLED") + CFG.set_speak_mode(True) + + if gpt3only: + logger.typewriter_log("GPT3.5 Only Mode: ", Fore.GREEN, "ENABLED") + CFG.set_smart_llm_model(CFG.fast_llm_model) + + if gpt4only: + logger.typewriter_log("GPT4 Only Mode: ", Fore.GREEN, "ENABLED") + CFG.set_fast_llm_model(CFG.smart_llm_model) + + if memory_type: + supported_memory = get_supported_memory_backends() + chosen = memory_type + if chosen not in supported_memory: + logger.typewriter_log( + "ONLY THE FOLLOWING MEMORY BACKENDS ARE SUPPORTED: ", + Fore.RED, + f"{supported_memory}", + ) + logger.typewriter_log("Defaulting to: ", Fore.YELLOW, CFG.memory_backend) + else: + CFG.memory_backend = chosen + + if skip_reprompt: + logger.typewriter_log("Skip Re-prompt: ", Fore.GREEN, "ENABLED") + CFG.skip_reprompt = True + + if ai_settings_file: + file = ai_settings_file + + # Validate file + (validated, message) = utils.validate_yaml_file(file) + if not validated: + logger.typewriter_log("FAILED FILE VALIDATION", Fore.RED, message) + logger.double_check() + exit(1) + + logger.typewriter_log("Using AI Settings File:", Fore.GREEN, file) + CFG.ai_settings_file = file + CFG.skip_reprompt = True + + if browser_name: + CFG.selenium_web_browser = browser_name + + if allow_downloads: + logger.typewriter_log("Native Downloading:", Fore.GREEN, "ENABLED") + logger.typewriter_log( + "WARNING: ", + Fore.YELLOW, + f"{Back.LIGHTYELLOW_EX}Auto-GPT will now be able to download and save files to your machine.{Back.RESET} " + + "It is recommended that you monitor any files it downloads carefully.", + ) + logger.typewriter_log( + "WARNING: ", + Fore.YELLOW, + f"{Back.RED + Style.BRIGHT}ALWAYS REMEMBER TO NEVER OPEN FILES YOU AREN'T SURE OF!{Style.RESET_ALL}", + ) + CFG.allow_downloads = True + + if skip_news: + CFG.skip_news = True diff --git a/autogpt/js/overlay.js b/autogpt/js/overlay.js new file mode 100644 index 0000000..1c99c72 --- /dev/null +++ b/autogpt/js/overlay.js @@ -0,0 +1,29 @@ +const overlay = document.createElement('div'); +Object.assign(overlay.style, { + position: 'fixed', + zIndex: 999999, + top: 0, + left: 0, + width: '100%', + height: '100%', + background: 'rgba(0, 0, 0, 0.7)', + color: '#fff', + fontSize: '24px', + fontWeight: 'bold', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', +}); +const textContent = document.createElement('div'); +Object.assign(textContent.style, { + textAlign: 'center', +}); +textContent.textContent = 'AutoGPT Analyzing Page'; +overlay.appendChild(textContent); +document.body.append(overlay); +document.body.style.overflow = 'hidden'; +let dotCount = 0; +setInterval(() => { + textContent.textContent = 'AutoGPT Analyzing Page' + '.'.repeat(dotCount); + dotCount = (dotCount + 1) % 4; +}, 1000); diff --git a/autogpt/json_utils/__init__.py b/autogpt/json_utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/autogpt/json_utils/json_fix_general.py b/autogpt/json_utils/json_fix_general.py new file mode 100644 index 0000000..7010fa3 --- /dev/null +++ b/autogpt/json_utils/json_fix_general.py @@ -0,0 +1,124 @@ +"""This module contains functions to fix JSON strings using general programmatic approaches, suitable for addressing +common JSON formatting issues.""" +from __future__ import annotations + +import contextlib +import json +import re +from typing import Optional + +from autogpt.config import Config +from autogpt.json_utils.utilities import extract_char_position + +CFG = Config() + + +def fix_invalid_escape(json_to_load: str, error_message: str) -> str: + """Fix invalid escape sequences in JSON strings. + + Args: + json_to_load (str): The JSON string. + error_message (str): The error message from the JSONDecodeError + exception. + + Returns: + str: The JSON string with invalid escape sequences fixed. + """ + while error_message.startswith("Invalid \\escape"): + bad_escape_location = extract_char_position(error_message) + json_to_load = ( + json_to_load[:bad_escape_location] + json_to_load[bad_escape_location + 1 :] + ) + try: + json.loads(json_to_load) + return json_to_load + except json.JSONDecodeError as e: + if CFG.debug_mode: + print("json loads error - fix invalid escape", e) + error_message = str(e) + return json_to_load + + +def balance_braces(json_string: str) -> Optional[str]: + """ + Balance the braces in a JSON string. + + Args: + json_string (str): The JSON string. + + Returns: + str: The JSON string with braces balanced. + """ + + open_braces_count = json_string.count("{") + close_braces_count = json_string.count("}") + + while open_braces_count > close_braces_count: + json_string += "}" + close_braces_count += 1 + + while close_braces_count > open_braces_count: + json_string = json_string.rstrip("}") + close_braces_count -= 1 + + with contextlib.suppress(json.JSONDecodeError): + json.loads(json_string) + return json_string + + +def add_quotes_to_property_names(json_string: str) -> str: + """ + Add quotes to property names in a JSON string. + + Args: + json_string (str): The JSON string. + + Returns: + str: The JSON string with quotes added to property names. + """ + + def replace_func(match: re.Match) -> str: + return f'"{match[1]}":' + + property_name_pattern = re.compile(r"(\w+):") + corrected_json_string = property_name_pattern.sub(replace_func, json_string) + + try: + json.loads(corrected_json_string) + return corrected_json_string + except json.JSONDecodeError as e: + raise e + + +def correct_json(json_to_load: str) -> str: + """ + Correct common JSON errors. + Args: + json_to_load (str): The JSON string. + """ + + try: + if CFG.debug_mode: + print("json", json_to_load) + json.loads(json_to_load) + return json_to_load + except json.JSONDecodeError as e: + if CFG.debug_mode: + print("json loads error", e) + error_message = str(e) + if error_message.startswith("Invalid \\escape"): + json_to_load = fix_invalid_escape(json_to_load, error_message) + if error_message.startswith( + "Expecting property name enclosed in double quotes" + ): + json_to_load = add_quotes_to_property_names(json_to_load) + try: + json.loads(json_to_load) + return json_to_load + except json.JSONDecodeError as e: + if CFG.debug_mode: + print("json loads error - add quotes", e) + error_message = str(e) + if balanced_str := balance_braces(json_to_load): + return balanced_str + return json_to_load diff --git a/autogpt/json_utils/json_fix_llm.py b/autogpt/json_utils/json_fix_llm.py new file mode 100644 index 0000000..869aed1 --- /dev/null +++ b/autogpt/json_utils/json_fix_llm.py @@ -0,0 +1,220 @@ +"""This module contains functions to fix JSON strings generated by LLM models, such as ChatGPT, using the assistance +of the ChatGPT API or LLM models.""" +from __future__ import annotations + +import contextlib +import json +from typing import Any, Dict + +from colorama import Fore +from regex import regex + +from autogpt.config import Config +from autogpt.json_utils.json_fix_general import correct_json +from autogpt.llm_utils import call_ai_function +from autogpt.logs import logger +from autogpt.speech import say_text + +JSON_SCHEMA = """ +{ + "command": { + "name": "command name", + "args": { + "arg name": "value" + } + }, + "thoughts": + { + "text": "thought", + "reasoning": "reasoning", + "plan": "- short bulleted\n- list that conveys\n- long-term plan", + "criticism": "constructive self-criticism", + "speak": "thoughts summary to say to user" + } +} +""" + +CFG = Config() + + +def auto_fix_json(json_string: str, schema: str) -> str: + """Fix the given JSON string to make it parseable and fully compliant with + the provided schema using GPT-3. + + Args: + json_string (str): The JSON string to fix. + schema (str): The schema to use to fix the JSON. + Returns: + str: The fixed JSON string. + """ + # Try to fix the JSON using GPT: + function_string = "def fix_json(json_string: str, schema:str=None) -> str:" + args = [f"'''{json_string}'''", f"'''{schema}'''"] + description_string = ( + "This function takes a JSON string and ensures that it" + " is parseable and fully compliant with the provided schema. If an object" + " or field specified in the schema isn't contained within the correct JSON," + " it is omitted. The function also escapes any double quotes within JSON" + " string values to ensure that they are valid. If the JSON string contains" + " any None or NaN values, they are replaced with null before being parsed." + ) + + # If it doesn't already start with a "`", add one: + if not json_string.startswith("`"): + json_string = "```json\n" + json_string + "\n```" + result_string = call_ai_function( + function_string, args, description_string, model=CFG.fast_llm_model + ) + logger.debug("------------ JSON FIX ATTEMPT ---------------") + logger.debug(f"Original JSON: {json_string}") + logger.debug("-----------") + logger.debug(f"Fixed JSON: {result_string}") + logger.debug("----------- END OF FIX ATTEMPT ----------------") + + try: + json.loads(result_string) # just check the validity + return result_string + except json.JSONDecodeError: # noqa: E722 + # Get the call stack: + # import traceback + # call_stack = traceback.format_exc() + # print(f"Failed to fix JSON: '{json_string}' "+call_stack) + return "failed" + + +def fix_json_using_multiple_techniques(assistant_reply: str) -> Dict[Any, Any]: + """Fix the given JSON string to make it parseable and fully compliant with two techniques. + + Args: + json_string (str): The JSON string to fix. + + Returns: + str: The fixed JSON string. + """ + + # Parse and print Assistant response + assistant_reply_json = fix_and_parse_json(assistant_reply) + if assistant_reply_json == {}: + assistant_reply_json = attempt_to_fix_json_by_finding_outermost_brackets( + assistant_reply + ) + + if assistant_reply_json != {}: + return assistant_reply_json + + logger.error( + "Error: The following AI output couldn't be converted to a JSON:\n", + assistant_reply, + ) + if CFG.speak_mode: + say_text("I have received an invalid JSON response from the OpenAI API.") + + return {} + + +def fix_and_parse_json( + json_to_load: str, try_to_fix_with_gpt: bool = True +) -> Dict[Any, Any]: + """Fix and parse JSON string + + Args: + json_to_load (str): The JSON string. + try_to_fix_with_gpt (bool, optional): Try to fix the JSON with GPT. + Defaults to True. + + Returns: + str or dict[Any, Any]: The parsed JSON. + """ + + with contextlib.suppress(json.JSONDecodeError): + json_to_load = json_to_load.replace("\t", "") + return json.loads(json_to_load) + + with contextlib.suppress(json.JSONDecodeError): + json_to_load = correct_json(json_to_load) + return json.loads(json_to_load) + # Let's do something manually: + # sometimes GPT responds with something BEFORE the braces: + # "I'm sorry, I don't understand. Please try again." + # {"text": "I'm sorry, I don't understand. Please try again.", + # "confidence": 0.0} + # So let's try to find the first brace and then parse the rest + # of the string + try: + brace_index = json_to_load.index("{") + maybe_fixed_json = json_to_load[brace_index:] + last_brace_index = maybe_fixed_json.rindex("}") + maybe_fixed_json = maybe_fixed_json[: last_brace_index + 1] + return json.loads(maybe_fixed_json) + except (json.JSONDecodeError, ValueError) as e: + return try_ai_fix(try_to_fix_with_gpt, e, json_to_load) + + +def try_ai_fix( + try_to_fix_with_gpt: bool, exception: Exception, json_to_load: str +) -> Dict[Any, Any]: + """Try to fix the JSON with the AI + + Args: + try_to_fix_with_gpt (bool): Whether to try to fix the JSON with the AI. + exception (Exception): The exception that was raised. + json_to_load (str): The JSON string to load. + + Raises: + exception: If try_to_fix_with_gpt is False. + + Returns: + str or dict[Any, Any]: The JSON string or dictionary. + """ + if not try_to_fix_with_gpt: + raise exception + if CFG.debug_mode: + logger.warn( + "Warning: Failed to parse AI output, attempting to fix." + "\n If you see this warning frequently, it's likely that" + " your prompt is confusing the AI. Try changing it up" + " slightly." + ) + # Now try to fix this up using the ai_functions + ai_fixed_json = auto_fix_json(json_to_load, JSON_SCHEMA) + + if ai_fixed_json != "failed": + return json.loads(ai_fixed_json) + # This allows the AI to react to the error message, + # which usually results in it correcting its ways. + # logger.error("Failed to fix AI output, telling the AI.") + return {} + + +def attempt_to_fix_json_by_finding_outermost_brackets(json_string: str): + if CFG.speak_mode and CFG.debug_mode: + say_text( + "I have received an invalid JSON response from the OpenAI API. " + "Trying to fix it now." + ) + logger.error("Attempting to fix JSON by finding outermost brackets\n") + + try: + json_pattern = regex.compile(r"\{(?:[^{}]|(?R))*\}") + json_match = json_pattern.search(json_string) + + if json_match: + # Extract the valid JSON object from the string + json_string = json_match.group(0) + logger.typewriter_log( + title="Apparently json was fixed.", title_color=Fore.GREEN + ) + if CFG.speak_mode and CFG.debug_mode: + say_text("Apparently json was fixed.") + else: + return {} + + except (json.JSONDecodeError, ValueError): + if CFG.debug_mode: + logger.error(f"Error: Invalid JSON: {json_string}\n") + if CFG.speak_mode: + say_text("Didn't work. I will have to ignore this response then.") + logger.error("Error: Invalid JSON, setting it to empty JSON now.\n") + json_string = {} + + return fix_and_parse_json(json_string) diff --git a/autogpt/json_utils/llm_response_format_1.json b/autogpt/json_utils/llm_response_format_1.json new file mode 100644 index 0000000..9aa3335 --- /dev/null +++ b/autogpt/json_utils/llm_response_format_1.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "thoughts": { + "type": "object", + "properties": { + "text": {"type": "string"}, + "reasoning": {"type": "string"}, + "plan": {"type": "string"}, + "criticism": {"type": "string"}, + "speak": {"type": "string"} + }, + "required": ["text", "reasoning", "plan", "criticism", "speak"], + "additionalProperties": false + }, + "command": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "args": { + "type": "object" + } + }, + "required": ["name", "args"], + "additionalProperties": false + } + }, + "required": ["thoughts", "command"], + "additionalProperties": false +} diff --git a/autogpt/json_utils/utilities.py b/autogpt/json_utils/utilities.py new file mode 100644 index 0000000..c8cb5d7 --- /dev/null +++ b/autogpt/json_utils/utilities.py @@ -0,0 +1,54 @@ +"""Utilities for the json_fixes package.""" +import json +import re + +from jsonschema import Draft7Validator + +from autogpt.config import Config +from autogpt.logs import logger + +CFG = Config() + + +def extract_char_position(error_message: str) -> int: + """Extract the character position from the JSONDecodeError message. + + Args: + error_message (str): The error message from the JSONDecodeError + exception. + + Returns: + int: The character position. + """ + + char_pattern = re.compile(r"\(char (\d+)\)") + if match := char_pattern.search(error_message): + return int(match[1]) + else: + raise ValueError("Character position not found in the error message.") + + +def validate_json(json_object: object, schema_name: object) -> object: + """ + :type schema_name: object + :param schema_name: + :type json_object: object + """ + with open(f"/Users/kilig/Job/Python-project/academic_gpt/autogpt/json_utils/{schema_name}.json", "r") as f: + schema = json.load(f) + validator = Draft7Validator(schema) + + if errors := sorted(validator.iter_errors(json_object), key=lambda e: e.path): + logger.error("The JSON object is invalid.") + if CFG.debug_mode: + logger.error( + json.dumps(json_object, indent=4) + ) # Replace 'json_object' with the variable containing the JSON data + logger.error("The following issues were found:") + + for error in errors: + logger.error(f"Error: {error.message}") + elif CFG.debug_mode: + print("The JSON object is valid.") + + return json_object diff --git a/autogpt/llm_utils.py b/autogpt/llm_utils.py new file mode 100644 index 0000000..ba7521a --- /dev/null +++ b/autogpt/llm_utils.py @@ -0,0 +1,185 @@ +from __future__ import annotations + +import time +from typing import List, Optional + +import openai +from colorama import Fore, Style +from openai.error import APIError, RateLimitError + +from autogpt.api_manager import api_manager +from autogpt.config import Config +from autogpt.logs import logger +from autogpt.types.openai import Message + +CFG = Config() + +openai.api_key = CFG.openai_api_key + + +def call_ai_function( + function: str, args: list, description: str, model: str | None = None +) -> str: + """Call an AI function + + This is a magic function that can do anything with no-code. See + https://github.com/Torantulino/AI-Functions for more info. + + Args: + function (str): The function to call + args (list): The arguments to pass to the function + description (str): The description of the function + model (str, optional): The model to use. Defaults to None. + + Returns: + str: The response from the function + """ + if model is None: + model = CFG.smart_llm_model + # For each arg, if any are None, convert to "None": + args = [str(arg) if arg is not None else "None" for arg in args] + # parse args to comma separated string + args: str = ", ".join(args) + messages: List[Message] = [ + { + "role": "system", + "content": f"You are now the following python function: ```# {description}" + f"\n{function}```\n\nOnly respond with your `return` value.", + }, + {"role": "user", "content": args}, + ] + + return create_chat_completion(model=model, messages=messages, temperature=0) + + +# Overly simple abstraction until we create something better +# simple retry mechanism when getting a rate error or a bad gateway +def create_chat_completion( + messages: List[Message], # type: ignore + model: Optional[str] = None, + temperature: float = CFG.temperature, + max_tokens: Optional[int] = None, +) -> str: + """Create a chat completion using the OpenAI API + + Args: + messages (List[Message]): The messages to send to the chat completion + model (str, optional): The model to use. Defaults to None. + temperature (float, optional): The temperature to use. Defaults to 0.9. + max_tokens (int, optional): The max tokens to use. Defaults to None. + + Returns: + str: The response from the chat completion + """ + num_retries = 10 + warned_user = False + if CFG.debug_mode: + print( + f"{Fore.GREEN}Creating chat completion with model {model}, temperature {temperature}, max_tokens {max_tokens}{Fore.RESET}" + ) + for plugin in CFG.plugins: + if plugin.can_handle_chat_completion( + messages=messages, + model=model, + temperature=temperature, + max_tokens=max_tokens, + ): + message = plugin.handle_chat_completion( + messages=messages, + model=model, + temperature=temperature, + max_tokens=max_tokens, + ) + if message is not None: + return message + response = None + for attempt in range(num_retries): + backoff = 2 ** (attempt + 2) + try: + if CFG.use_azure: + response = api_manager.create_chat_completion( + deployment_id=CFG.get_azure_deployment_id_for_model(model), + model=model, + messages=messages, + temperature=temperature, + max_tokens=max_tokens, + ) + else: + response = api_manager.create_chat_completion( + model=model, + messages=messages, + temperature=temperature, + max_tokens=max_tokens, + ) + break + except RateLimitError: + if CFG.debug_mode: + print( + f"{Fore.RED}Error: ", f"Reached rate limit, passing...{Fore.RESET}" + ) + if not warned_user: + logger.double_check( + f"Please double check that you have setup a {Fore.CYAN + Style.BRIGHT}PAID{Style.RESET_ALL} OpenAI API Account. " + + f"You can read more here: {Fore.CYAN}https://github.com/Significant-Gravitas/Auto-GPT#openai-api-keys-configuration{Fore.RESET}" + ) + warned_user = True + except APIError as e: + if e.http_status != 502: + raise + if attempt == num_retries - 1: + raise + if CFG.debug_mode: + print( + f"{Fore.RED}Error: ", + f"API Bad gateway. Waiting {backoff} seconds...{Fore.RESET}", + ) + time.sleep(backoff) + if response is None: + logger.typewriter_log( + "FAILED TO GET RESPONSE FROM OPENAI", + Fore.RED, + "Auto-GPT has failed to get a response from OpenAI's services. " + + f"Try running Auto-GPT again, and if the problem the persists try running it with `{Fore.CYAN}--debug{Fore.RESET}`.", + ) + logger.double_check() + if CFG.debug_mode: + raise RuntimeError(f"Failed to get response after {num_retries} retries") + else: + quit(1) + resp = response.choices[0].message["content"] + for plugin in CFG.plugins: + if not plugin.can_handle_on_response(): + continue + resp = plugin.on_response(resp) + return resp + + +def get_ada_embedding(text): + text = text.replace("\n", " ") + return api_manager.embedding_create( + text_list=[text], model="text-embedding-ada-002" + ) + + +def create_embedding_with_ada(text) -> list: + """Create an embedding with text-ada-002 using the OpenAI SDK""" + num_retries = 10 + for attempt in range(num_retries): + backoff = 2 ** (attempt + 2) + try: + return api_manager.embedding_create( + text_list=[text], model="text-embedding-ada-002" + ) + except RateLimitError: + pass + except APIError as e: + if e.http_status != 502: + raise + if attempt == num_retries - 1: + raise + if CFG.debug_mode: + print( + f"{Fore.RED}Error: ", + f"API Bad gateway. Waiting {backoff} seconds...{Fore.RESET}", + ) + time.sleep(backoff) diff --git a/autogpt/logs.py b/autogpt/logs.py new file mode 100644 index 0000000..09467d4 --- /dev/null +++ b/autogpt/logs.py @@ -0,0 +1,359 @@ +"""Logging module for Auto-GPT.""" +import inspect +import json +import logging +import os +import random +import re +import time +import traceback +from logging import LogRecord + +from colorama import Fore, Style + +from autogpt.config import Config, Singleton +from autogpt.speech import say_text + +CFG = Config() + +def get_properties(obj): + props = {} + for prop_name in dir(obj): + if not prop_name.startswith('__'): + prop_value = getattr(obj, prop_name) + props[prop_value] = prop_name + return props + + +class Logger(metaclass=Singleton): + """ + Logger that handle titles in different colors. + Outputs logs in console, activity.log, and errors.log + For console handler: simulates typing + """ + + def __init__(self): + # create log directory if it doesn't exist + this_files_dir_path = os.path.dirname(__file__) + log_dir = os.path.join(this_files_dir_path, "../logs") + if not os.path.exists(log_dir): + os.makedirs(log_dir) + + log_file = "activity.log" + error_file = "error.log" + + console_formatter = AutoGptFormatter("%(title_color)s %(message)s") + + # Create a handler for console which simulate typing + self.typing_console_handler = TypingConsoleHandler() + self.typing_console_handler.setLevel(logging.INFO) + self.typing_console_handler.setFormatter(console_formatter) + + # Create a handler for console without typing simulation + self.console_handler = ConsoleHandler() + self.console_handler.setLevel(logging.DEBUG) + self.console_handler.setFormatter(console_formatter) + + # Info handler in activity.log + self.file_handler = logging.FileHandler( + os.path.join(log_dir, log_file), "a", "utf-8" + ) + self.file_handler.setLevel(logging.DEBUG) + info_formatter = AutoGptFormatter( + "%(asctime)s %(levelname)s %(title)s %(message_no_color)s" + ) + self.file_handler.setFormatter(info_formatter) + + # Error handler error.log + error_handler = logging.FileHandler( + os.path.join(log_dir, error_file), "a", "utf-8" + ) + error_handler.setLevel(logging.ERROR) + error_formatter = AutoGptFormatter( + "%(asctime)s %(levelname)s %(module)s:%(funcName)s:%(lineno)d %(title)s" + " %(message_no_color)s" + ) + error_handler.setFormatter(error_formatter) + + self.typing_logger = logging.getLogger("TYPER") + self.typing_logger.addHandler(self.typing_console_handler) + self.typing_logger.addHandler(self.file_handler) + self.typing_logger.addHandler(error_handler) + self.typing_logger.setLevel(logging.DEBUG) + + self.logger = logging.getLogger("LOGGER") + self.logger.addHandler(self.console_handler) + self.logger.addHandler(self.file_handler) + self.logger.addHandler(error_handler) + self.logger.setLevel(logging.DEBUG) + self.color_compar = get_properties(Fore) + self.output_content = [] + + def typewriter_log( + self, title="", title_color=Fore.YELLOW, content="", speak_text=False, level=logging.INFO + ): + if speak_text and CFG.speak_mode: + say_text(f"{title}. {content}") + + if content: + if isinstance(content, list): + content = " ".join(content) + else: + content = "" + + self.typing_logger.log( + level, content, extra={"title": title, "color": title_color} + ) + try: + msg = f'{title}:{content}' + self.output_content.append([msg, title+": "+content]) + return msg + except Exception as e: + msg = f'{title}:{content}' + self.output_content.append([msg, title+": "+content]) + return + + + def debug( + self, + message, + title="", + title_color="", + ): + self._log(title, title_color, message, logging.DEBUG) + + def warn( + self, + message, + title="", + title_color="", + ): + self._log(title, title_color, message, logging.WARN) + + def error(self, title, message=""): + self._log(title, Fore.RED, message, logging.ERROR) + + def _log(self, title="", title_color="", message="", level=logging.INFO): + if message: + if isinstance(message, list): + message = " ".join(message) + self.logger.log(level, message, extra={"title": title, "color": title_color}) + + def set_level(self, level): + self.logger.setLevel(level) + self.typing_logger.setLevel(level) + + def double_check(self, additionalText=None): + if not additionalText: + additionalText = ( + "Please ensure you've setup and configured everything" + " correctly. Read https://github.com/Torantulino/Auto-GPT#readme to " + "double check. You can also create a github issue or join the discord" + " and ask there!" + ) + + self.typewriter_log("DOUBLE CHECK CONFIGURATION", Fore.YELLOW, additionalText) + + +""" +Output stream to console using simulated typing +""" + + +class TypingConsoleHandler(logging.StreamHandler): + def emit(self, record): + min_typing_speed = 0.05 + max_typing_speed = 0.01 + + msg = self.format(record) + try: + words = msg.split() + for i, word in enumerate(words): + print(word, end="", flush=True) + if i < len(words) - 1: + print(" ", end="", flush=True) + typing_speed = random.uniform(min_typing_speed, max_typing_speed) + time.sleep(typing_speed) + # type faster after each word + min_typing_speed = min_typing_speed * 0.95 + max_typing_speed = max_typing_speed * 0.95 + print() + except Exception: + self.handleError(record) + + +class ConsoleHandler(logging.StreamHandler): + def emit(self, record) -> None: + msg = self.format(record) + try: + print(msg) + except Exception: + self.handleError(record) + + +class AutoGptFormatter(logging.Formatter): + """ + Allows to handle custom placeholders 'title_color' and 'message_no_color'. + To use this formatter, make sure to pass 'color', 'title' as log extras. + """ + + def format(self, record: LogRecord) -> str: + if hasattr(record, "color"): + record.title_color = ( + getattr(record, "color") + + getattr(record, "title") + + " " + + Style.RESET_ALL + ) + else: + record.title_color = getattr(record, "title") + if hasattr(record, "msg"): + record.message_no_color = remove_color_codes(getattr(record, "msg")) + else: + record.message_no_color = "" + return super().format(record) + + +def remove_color_codes(s: str) -> str: + ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") + return ansi_escape.sub("", s) + + +logger = Logger() + + +def print_assistant_thoughts(ai_name, assistant_reply): + """Prints the assistant's thoughts to the console""" + from autogpt.json_utils.json_fix_llm import ( + attempt_to_fix_json_by_finding_outermost_brackets, + fix_and_parse_json, + ) + + try: + try: + # Parse and print Assistant response + assistant_reply_json = fix_and_parse_json(assistant_reply) + except json.JSONDecodeError: + logger.error("Error: Invalid JSON in assistant thoughts\n", assistant_reply) + assistant_reply_json = attempt_to_fix_json_by_finding_outermost_brackets( + assistant_reply + ) + if isinstance(assistant_reply_json, str): + assistant_reply_json = fix_and_parse_json(assistant_reply_json) + + # Check if assistant_reply_json is a string and attempt to parse + # it into a JSON object + if isinstance(assistant_reply_json, str): + try: + assistant_reply_json = json.loads(assistant_reply_json) + except json.JSONDecodeError: + logger.error("Error: Invalid JSON\n", assistant_reply) + assistant_reply_json = ( + attempt_to_fix_json_by_finding_outermost_brackets( + assistant_reply_json + ) + ) + + assistant_thoughts_reasoning = None + assistant_thoughts_plan = None + assistant_thoughts_speak = None + assistant_thoughts_criticism = None + if not isinstance(assistant_reply_json, dict): + assistant_reply_json = {} + assistant_thoughts = assistant_reply_json.get("thoughts", {}) + assistant_thoughts_text = assistant_thoughts.get("text") + + if assistant_thoughts: + assistant_thoughts_reasoning = assistant_thoughts.get("reasoning") + assistant_thoughts_plan = assistant_thoughts.get("plan") + assistant_thoughts_criticism = assistant_thoughts.get("criticism") + assistant_thoughts_speak = assistant_thoughts.get("speak") + + logger.typewriter_log( + f"{ai_name.upper()} THOUGHTS:", Fore.YELLOW, f"{assistant_thoughts_text}" + ) + logger.typewriter_log( + "REASONING:", Fore.YELLOW, f"{assistant_thoughts_reasoning}" + ) + + if assistant_thoughts_plan: + logger.typewriter_log("PLAN:", Fore.YELLOW, "") + # If it's a list, join it into a string + if isinstance(assistant_thoughts_plan, list): + assistant_thoughts_plan = "\n".join(assistant_thoughts_plan) + elif isinstance(assistant_thoughts_plan, dict): + assistant_thoughts_plan = str(assistant_thoughts_plan) + + # Split the input_string using the newline character and dashes + lines = assistant_thoughts_plan.split("\n") + for line in lines: + line = line.lstrip("- ") + logger.typewriter_log("- ", Fore.GREEN, line.strip()) + + logger.typewriter_log( + "CRITICISM:", Fore.YELLOW, f"{assistant_thoughts_criticism}" + ) + # Speak the assistant's thoughts + if CFG.speak_mode and assistant_thoughts_speak: + say_text(assistant_thoughts_speak) + else: + logger.typewriter_log("SPEAK:", Fore.YELLOW, f"{assistant_thoughts_speak}") + + return assistant_reply_json + except json.decoder.JSONDecodeError: + logger.error("Error: Invalid JSON\n", assistant_reply) + if CFG.speak_mode: + say_text( + "I have received an invalid JSON response from the OpenAI API." + " I cannot ignore this response." + ) + + # All other errors, return "Error: + error message" + except Exception: + call_stack = traceback.format_exc() + logger.error("Error: \n", call_stack) + + +def print_assistant_thoughts( + ai_name: object, assistant_reply_json_valid: object +) -> None: + assistant_thoughts_reasoning = None + assistant_thoughts_plan = None + assistant_thoughts_speak = None + assistant_thoughts_criticism = None + + assistant_thoughts = assistant_reply_json_valid.get("thoughts", {}) + assistant_thoughts_text = assistant_thoughts.get("text") + if assistant_thoughts: + assistant_thoughts_reasoning = assistant_thoughts.get("reasoning") + assistant_thoughts_plan = assistant_thoughts.get("plan") + assistant_thoughts_criticism = assistant_thoughts.get("criticism") + assistant_thoughts_speak = assistant_thoughts.get("speak") + logger.typewriter_log( + f"{ai_name.upper()} THOUGHTS:", Fore.YELLOW, f"{assistant_thoughts_text}" + ) + logger.typewriter_log("REASONING:", Fore.YELLOW, f"{assistant_thoughts_reasoning}") + if assistant_thoughts_plan: + logger.typewriter_log("PLAN:", Fore.YELLOW, "") + # If it's a list, join it into a string + if isinstance(assistant_thoughts_plan, list): + assistant_thoughts_plan = "\n".join(assistant_thoughts_plan) + elif isinstance(assistant_thoughts_plan, dict): + assistant_thoughts_plan = str(assistant_thoughts_plan) + + # Split the input_string using the newline character and dashes + lines = assistant_thoughts_plan.split("\n") + for line in lines: + line = line.lstrip("- ") + logger.typewriter_log("- ", Fore.GREEN, line.strip()) + logger.typewriter_log("CRITICISM:", Fore.YELLOW, f"{assistant_thoughts_criticism}") + # Speak the assistant's thoughts + if CFG.speak_mode and assistant_thoughts_speak: + say_text(assistant_thoughts_speak) + + +if __name__ == '__main__': + + ff = logger.typewriter_log('ahhahaha', Fore.GREEN, speak_text=True) + # print(Fore.GREEN) + # print(logger.color_compar) \ No newline at end of file diff --git a/autogpt/memory/__init__.py b/autogpt/memory/__init__.py new file mode 100644 index 0000000..c4eb4a0 --- /dev/null +++ b/autogpt/memory/__init__.py @@ -0,0 +1,99 @@ +from autogpt.memory.local import LocalCache +from autogpt.memory.no_memory import NoMemory + +# List of supported memory backends +# Add a backend to this list if the import attempt is successful +supported_memory = ["local", "no_memory"] + +try: + from autogpt.memory.redismem import RedisMemory + + supported_memory.append("redis") +except ImportError: + # print("Redis not installed. Skipping import.") + RedisMemory = None + +try: + from autogpt.memory.pinecone import PineconeMemory + + supported_memory.append("pinecone") +except ImportError: + # print("Pinecone not installed. Skipping import.") + PineconeMemory = None + +try: + from autogpt.memory.weaviate import WeaviateMemory + + supported_memory.append("weaviate") +except ImportError: + # print("Weaviate not installed. Skipping import.") + WeaviateMemory = None + +try: + from autogpt.memory.milvus import MilvusMemory + + supported_memory.append("milvus") +except ImportError: + # print("pymilvus not installed. Skipping import.") + MilvusMemory = None + + +def get_memory(cfg, init=False): + memory = None + if cfg.memory_backend == "pinecone": + if not PineconeMemory: + print( + "Error: Pinecone is not installed. Please install pinecone" + " to use Pinecone as a memory backend." + ) + else: + memory = PineconeMemory(cfg) + if init: + memory.clear() + elif cfg.memory_backend == "redis": + if not RedisMemory: + print( + "Error: Redis is not installed. Please install redis-py to" + " use Redis as a memory backend." + ) + else: + memory = RedisMemory(cfg) + elif cfg.memory_backend == "weaviate": + if not WeaviateMemory: + print( + "Error: Weaviate is not installed. Please install weaviate-client to" + " use Weaviate as a memory backend." + ) + else: + memory = WeaviateMemory(cfg) + elif cfg.memory_backend == "milvus": + if not MilvusMemory: + print( + "Error: pymilvus sdk is not installed." + "Please install pymilvus to use Milvus or Zilliz Cloud as memory backend." + ) + else: + memory = MilvusMemory(cfg) + elif cfg.memory_backend == "no_memory": + memory = NoMemory(cfg) + + if memory is None: + memory = LocalCache(cfg) + if init: + memory.clear() + return memory + + +def get_supported_memory_backends(): + return supported_memory + + +__all__ = [ + "get_memory", + "LocalCache", + "RedisMemory", + "PineconeMemory", + "NoMemory", + "MilvusMemory", + "WeaviateMemory", +] diff --git a/autogpt/memory/base.py b/autogpt/memory/base.py new file mode 100644 index 0000000..b625246 --- /dev/null +++ b/autogpt/memory/base.py @@ -0,0 +1,28 @@ +"""Base class for memory providers.""" +import abc + +from autogpt.config import AbstractSingleton, Config + +cfg = Config() + + +class MemoryProviderSingleton(AbstractSingleton): + @abc.abstractmethod + def add(self, data): + pass + + @abc.abstractmethod + def get(self, data): + pass + + @abc.abstractmethod + def clear(self): + pass + + @abc.abstractmethod + def get_relevant(self, data, num_relevant=5): + pass + + @abc.abstractmethod + def get_stats(self): + pass diff --git a/autogpt/memory/local.py b/autogpt/memory/local.py new file mode 100644 index 0000000..1f1a1a3 --- /dev/null +++ b/autogpt/memory/local.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +import dataclasses +from pathlib import Path +from typing import Any, List + +import numpy as np +import orjson + +from autogpt.llm_utils import create_embedding_with_ada +from autogpt.memory.base import MemoryProviderSingleton + +EMBED_DIM = 1536 +SAVE_OPTIONS = orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_SERIALIZE_DATACLASS + + +def create_default_embeddings(): + return np.zeros((0, EMBED_DIM)).astype(np.float32) + + +@dataclasses.dataclass +class CacheContent: + texts: List[str] = dataclasses.field(default_factory=list) + embeddings: np.ndarray = dataclasses.field( + default_factory=create_default_embeddings + ) + + +class LocalCache(MemoryProviderSingleton): + """A class that stores the memory in a local file""" + + def __init__(self, cfg) -> None: + """Initialize a class instance + + Args: + cfg: Config object + + Returns: + None + """ + workspace_path = Path(cfg.workspace_path) + self.filename = workspace_path / f"{cfg.memory_index}.json" + + self.filename.touch(exist_ok=True) + + file_content = b"{}" + with self.filename.open("w+b") as f: + f.write(file_content) + + self.data = CacheContent() + + def add(self, text: str): + """ + Add text to our list of texts, add embedding as row to our + embeddings-matrix + + Args: + text: str + + Returns: None + """ + if "Command Error:" in text: + return "" + self.data.texts.append(text) + + embedding = create_embedding_with_ada(text) + + vector = np.array(embedding).astype(np.float32) + vector = vector[np.newaxis, :] + self.data.embeddings = np.concatenate( + [ + self.data.embeddings, + vector, + ], + axis=0, + ) + + with open(self.filename, "wb") as f: + out = orjson.dumps(self.data, option=SAVE_OPTIONS) + f.write(out) + return text + + def clear(self) -> str: + """ + Clears the redis server. + + Returns: A message indicating that the memory has been cleared. + """ + self.data = CacheContent() + return "Obliviated" + + def get(self, data: str) -> list[Any] | None: + """ + Gets the data from the memory that is most relevant to the given data. + + Args: + data: The data to compare to. + + Returns: The most relevant data. + """ + return self.get_relevant(data, 1) + + def get_relevant(self, text: str, k: int) -> list[Any]: + """ " + matrix-vector mult to find score-for-each-row-of-matrix + get indices for top-k winning scores + return texts for those indices + Args: + text: str + k: int + + Returns: List[str] + """ + embedding = create_embedding_with_ada(text) + + scores = np.dot(self.data.embeddings, embedding) + + top_k_indices = np.argsort(scores)[-k:][::-1] + + return [self.data.texts[i] for i in top_k_indices] + + def get_stats(self) -> tuple[int, tuple[int, ...]]: + """ + Returns: The stats of the local cache. + """ + return len(self.data.texts), self.data.embeddings.shape diff --git a/autogpt/memory/milvus.py b/autogpt/memory/milvus.py new file mode 100644 index 0000000..085f50b --- /dev/null +++ b/autogpt/memory/milvus.py @@ -0,0 +1,162 @@ +""" Milvus memory storage provider.""" +import re + +from pymilvus import Collection, CollectionSchema, DataType, FieldSchema, connections + +from autogpt.config import Config +from autogpt.llm_utils import get_ada_embedding +from autogpt.memory.base import MemoryProviderSingleton + + +class MilvusMemory(MemoryProviderSingleton): + """Milvus memory storage provider.""" + + def __init__(self, cfg: Config) -> None: + """Construct a milvus memory storage connection. + + Args: + cfg (Config): Auto-GPT global config. + """ + self.configure(cfg) + + connect_kwargs = {} + if self.username: + connect_kwargs["user"] = self.username + connect_kwargs["password"] = self.password + + connections.connect( + **connect_kwargs, + uri=self.uri or "", + address=self.address or "", + secure=self.secure, + ) + + self.init_collection() + + def configure(self, cfg: Config) -> None: + # init with configuration. + self.uri = None + self.address = cfg.milvus_addr + self.secure = cfg.milvus_secure + self.username = cfg.milvus_username + self.password = cfg.milvus_password + self.collection_name = cfg.milvus_collection + # use HNSW by default. + self.index_params = { + "metric_type": "IP", + "index_type": "HNSW", + "params": {"M": 8, "efConstruction": 64}, + } + + if (self.username is None) != (self.password is None): + raise ValueError( + "Both username and password must be set to use authentication for Milvus" + ) + + # configured address may be a full URL. + if re.match(r"^(https?|tcp)://", self.address) is not None: + self.uri = self.address + self.address = None + + if self.uri.startswith("https"): + self.secure = True + + # Zilliz Cloud requires AutoIndex. + if re.match(r"^https://(.*)\.zillizcloud\.(com|cn)", self.address) is not None: + self.index_params = { + "metric_type": "IP", + "index_type": "AUTOINDEX", + "params": {}, + } + + def init_collection(self) -> None: + """Initialize collection in vector database.""" + fields = [ + FieldSchema(name="pk", dtype=DataType.INT64, is_primary=True, auto_id=True), + FieldSchema(name="embeddings", dtype=DataType.FLOAT_VECTOR, dim=1536), + FieldSchema(name="raw_text", dtype=DataType.VARCHAR, max_length=65535), + ] + + # create collection if not exist and load it. + self.schema = CollectionSchema(fields, "auto-gpt memory storage") + self.collection = Collection(self.collection_name, self.schema) + # create index if not exist. + if not self.collection.has_index(): + self.collection.release() + self.collection.create_index( + "embeddings", + self.index_params, + index_name="embeddings", + ) + self.collection.load() + + def add(self, data) -> str: + """Add an embedding of data into memory. + + Args: + data (str): The raw text to construct embedding index. + + Returns: + str: log. + """ + embedding = get_ada_embedding(data) + result = self.collection.insert([[embedding], [data]]) + _text = ( + "Inserting data into memory at primary key: " + f"{result.primary_keys[0]}:\n data: {data}" + ) + return _text + + def get(self, data): + """Return the most relevant data in memory. + Args: + data: The data to compare to. + """ + return self.get_relevant(data, 1) + + def clear(self) -> str: + """Drop the index in memory. + + Returns: + str: log. + """ + self.collection.drop() + self.collection = Collection(self.collection_name, self.schema) + self.collection.create_index( + "embeddings", + self.index_params, + index_name="embeddings", + ) + self.collection.load() + return "Obliviated" + + def get_relevant(self, data: str, num_relevant: int = 5): + """Return the top-k relevant data in memory. + Args: + data: The data to compare to. + num_relevant (int, optional): The max number of relevant data. + Defaults to 5. + + Returns: + list: The top-k relevant data. + """ + # search the embedding and return the most relevant text. + embedding = get_ada_embedding(data) + search_params = { + "metrics_type": "IP", + "params": {"nprobe": 8}, + } + result = self.collection.search( + [embedding], + "embeddings", + search_params, + num_relevant, + output_fields=["raw_text"], + ) + return [item.entity.value_of_field("raw_text") for item in result[0]] + + def get_stats(self) -> str: + """ + Returns: The stats of the milvus cache. + """ + return f"Entities num: {self.collection.num_entities}" diff --git a/autogpt/memory/no_memory.py b/autogpt/memory/no_memory.py new file mode 100644 index 0000000..0371e96 --- /dev/null +++ b/autogpt/memory/no_memory.py @@ -0,0 +1,73 @@ +"""A class that does not store any data. This is the default memory provider.""" +from __future__ import annotations + +from typing import Any + +from autogpt.memory.base import MemoryProviderSingleton + + +class NoMemory(MemoryProviderSingleton): + """ + A class that does not store any data. This is the default memory provider. + """ + + def __init__(self, cfg): + """ + Initializes the NoMemory provider. + + Args: + cfg: The config object. + + Returns: None + """ + pass + + def add(self, data: str) -> str: + """ + Adds a data point to the memory. No action is taken in NoMemory. + + Args: + data: The data to add. + + Returns: An empty string. + """ + return "" + + def get(self, data: str) -> list[Any] | None: + """ + Gets the data from the memory that is most relevant to the given data. + NoMemory always returns None. + + Args: + data: The data to compare to. + + Returns: None + """ + return None + + def clear(self) -> str: + """ + Clears the memory. No action is taken in NoMemory. + + Returns: An empty string. + """ + return "" + + def get_relevant(self, data: str, num_relevant: int = 5) -> list[Any] | None: + """ + Returns all the data in the memory that is relevant to the given data. + NoMemory always returns None. + + Args: + data: The data to compare to. + num_relevant: The number of relevant data to return. + + Returns: None + """ + return None + + def get_stats(self): + """ + Returns: An empty dictionary as there are no stats in NoMemory. + """ + return {} diff --git a/autogpt/memory/pinecone.py b/autogpt/memory/pinecone.py new file mode 100644 index 0000000..27fcd62 --- /dev/null +++ b/autogpt/memory/pinecone.py @@ -0,0 +1,75 @@ +import pinecone +from colorama import Fore, Style + +from autogpt.llm_utils import create_embedding_with_ada +from autogpt.logs import logger +from autogpt.memory.base import MemoryProviderSingleton + + +class PineconeMemory(MemoryProviderSingleton): + def __init__(self, cfg): + pinecone_api_key = cfg.pinecone_api_key + pinecone_region = cfg.pinecone_region + pinecone.init(api_key=pinecone_api_key, environment=pinecone_region) + dimension = 1536 + metric = "cosine" + pod_type = "p1" + table_name = "auto-gpt" + # this assumes we don't start with memory. + # for now this works. + # we'll need a more complicated and robust system if we want to start with + # memory. + self.vec_num = 0 + + try: + pinecone.whoami() + except Exception as e: + logger.typewriter_log( + "FAILED TO CONNECT TO PINECONE", + Fore.RED, + Style.BRIGHT + str(e) + Style.RESET_ALL, + ) + logger.double_check( + "Please ensure you have setup and configured Pinecone properly for use." + + f"You can check out {Fore.CYAN + Style.BRIGHT}" + "https://github.com/Torantulino/Auto-GPT#-pinecone-api-key-setup" + f"{Style.RESET_ALL} to ensure you've set up everything correctly." + ) + exit(1) + + if table_name not in pinecone.list_indexes(): + pinecone.create_index( + table_name, dimension=dimension, metric=metric, pod_type=pod_type + ) + self.index = pinecone.Index(table_name) + + def add(self, data): + vector = create_embedding_with_ada(data) + # no metadata here. We may wish to change that long term. + self.index.upsert([(str(self.vec_num), vector, {"raw_text": data})]) + _text = f"Inserting data into memory at index: {self.vec_num}:\n data: {data}" + self.vec_num += 1 + return _text + + def get(self, data): + return self.get_relevant(data, 1) + + def clear(self): + self.index.delete(deleteAll=True) + return "Obliviated" + + def get_relevant(self, data, num_relevant=5): + """ + Returns all the data in the memory that is relevant to the given data. + :param data: The data to compare to. + :param num_relevant: The number of relevant data to return. Defaults to 5 + """ + query_embedding = create_embedding_with_ada(data) + results = self.index.query( + query_embedding, top_k=num_relevant, include_metadata=True + ) + sorted_results = sorted(results.matches, key=lambda x: x.score) + return [str(item["metadata"]["raw_text"]) for item in sorted_results] + + def get_stats(self): + return self.index.describe_index_stats() diff --git a/autogpt/memory/redismem.py b/autogpt/memory/redismem.py new file mode 100644 index 0000000..082a812 --- /dev/null +++ b/autogpt/memory/redismem.py @@ -0,0 +1,156 @@ +"""Redis memory provider.""" +from __future__ import annotations + +from typing import Any + +import numpy as np +import redis +from colorama import Fore, Style +from redis.commands.search.field import TextField, VectorField +from redis.commands.search.indexDefinition import IndexDefinition, IndexType +from redis.commands.search.query import Query + +from autogpt.llm_utils import create_embedding_with_ada +from autogpt.logs import logger +from autogpt.memory.base import MemoryProviderSingleton + +SCHEMA = [ + TextField("data"), + VectorField( + "embedding", + "HNSW", + {"TYPE": "FLOAT32", "DIM": 1536, "DISTANCE_METRIC": "COSINE"}, + ), +] + + +class RedisMemory(MemoryProviderSingleton): + def __init__(self, cfg): + """ + Initializes the Redis memory provider. + + Args: + cfg: The config object. + + Returns: None + """ + redis_host = cfg.redis_host + redis_port = cfg.redis_port + redis_password = cfg.redis_password + self.dimension = 1536 + self.redis = redis.Redis( + host=redis_host, + port=redis_port, + password=redis_password, + db=0, # Cannot be changed + ) + self.cfg = cfg + + # Check redis connection + try: + self.redis.ping() + except redis.ConnectionError as e: + logger.typewriter_log( + "FAILED TO CONNECT TO REDIS", + Fore.RED, + Style.BRIGHT + str(e) + Style.RESET_ALL, + ) + logger.double_check( + "Please ensure you have setup and configured Redis properly for use. " + + f"You can check out {Fore.CYAN + Style.BRIGHT}" + f"https://github.com/Torantulino/Auto-GPT#redis-setup{Style.RESET_ALL}" + " to ensure you've set up everything correctly." + ) + exit(1) + + if cfg.wipe_redis_on_start: + self.redis.flushall() + try: + self.redis.ft(f"{cfg.memory_index}").create_index( + fields=SCHEMA, + definition=IndexDefinition( + prefix=[f"{cfg.memory_index}:"], index_type=IndexType.HASH + ), + ) + except Exception as e: + print("Error creating Redis search index: ", e) + existing_vec_num = self.redis.get(f"{cfg.memory_index}-vec_num") + self.vec_num = int(existing_vec_num.decode("utf-8")) if existing_vec_num else 0 + + def add(self, data: str) -> str: + """ + Adds a data point to the memory. + + Args: + data: The data to add. + + Returns: Message indicating that the data has been added. + """ + if "Command Error:" in data: + return "" + vector = create_embedding_with_ada(data) + vector = np.array(vector).astype(np.float32).tobytes() + data_dict = {b"data": data, "embedding": vector} + pipe = self.redis.pipeline() + pipe.hset(f"{self.cfg.memory_index}:{self.vec_num}", mapping=data_dict) + _text = ( + f"Inserting data into memory at index: {self.vec_num}:\n" f"data: {data}" + ) + self.vec_num += 1 + pipe.set(f"{self.cfg.memory_index}-vec_num", self.vec_num) + pipe.execute() + return _text + + def get(self, data: str) -> list[Any] | None: + """ + Gets the data from the memory that is most relevant to the given data. + + Args: + data: The data to compare to. + + Returns: The most relevant data. + """ + return self.get_relevant(data, 1) + + def clear(self) -> str: + """ + Clears the redis server. + + Returns: A message indicating that the memory has been cleared. + """ + self.redis.flushall() + return "Obliviated" + + def get_relevant(self, data: str, num_relevant: int = 5) -> list[Any] | None: + """ + Returns all the data in the memory that is relevant to the given data. + Args: + data: The data to compare to. + num_relevant: The number of relevant data to return. + + Returns: A list of the most relevant data. + """ + query_embedding = create_embedding_with_ada(data) + base_query = f"*=>[KNN {num_relevant} @embedding $vector AS vector_score]" + query = ( + Query(base_query) + .return_fields("data", "vector_score") + .sort_by("vector_score") + .dialect(2) + ) + query_vector = np.array(query_embedding).astype(np.float32).tobytes() + + try: + results = self.redis.ft(f"{self.cfg.memory_index}").search( + query, query_params={"vector": query_vector} + ) + except Exception as e: + print("Error calling Redis search: ", e) + return None + return [result.data for result in results.docs] + + def get_stats(self): + """ + Returns: The stats of the memory index. + """ + return self.redis.ft(f"{self.cfg.memory_index}").info() diff --git a/autogpt/memory/weaviate.py b/autogpt/memory/weaviate.py new file mode 100644 index 0000000..fbebbfd --- /dev/null +++ b/autogpt/memory/weaviate.py @@ -0,0 +1,126 @@ +import weaviate +from weaviate import Client +from weaviate.embedded import EmbeddedOptions +from weaviate.util import generate_uuid5 + +from autogpt.llm_utils import get_ada_embedding +from autogpt.memory.base import MemoryProviderSingleton + + +def default_schema(weaviate_index): + return { + "class": weaviate_index, + "properties": [ + { + "name": "raw_text", + "dataType": ["text"], + "description": "original text for the embedding", + } + ], + } + + +class WeaviateMemory(MemoryProviderSingleton): + def __init__(self, cfg): + auth_credentials = self._build_auth_credentials(cfg) + + url = f"{cfg.weaviate_protocol}://{cfg.weaviate_host}:{cfg.weaviate_port}" + + if cfg.use_weaviate_embedded: + self.client = Client( + embedded_options=EmbeddedOptions( + hostname=cfg.weaviate_host, + port=int(cfg.weaviate_port), + persistence_data_path=cfg.weaviate_embedded_path, + ) + ) + + print( + f"Weaviate Embedded running on: {url} with persistence path: {cfg.weaviate_embedded_path}" + ) + else: + self.client = Client(url, auth_client_secret=auth_credentials) + + self.index = WeaviateMemory.format_classname(cfg.memory_index) + self._create_schema() + + @staticmethod + def format_classname(index): + # weaviate uses capitalised index names + # The python client uses the following code to format + # index names before the corresponding class is created + index = index.replace("-", "_") + if len(index) == 1: + return index.capitalize() + return index[0].capitalize() + index[1:] + + def _create_schema(self): + schema = default_schema(self.index) + if not self.client.schema.contains(schema): + self.client.schema.create_class(schema) + + def _build_auth_credentials(self, cfg): + if cfg.weaviate_username and cfg.weaviate_password: + return weaviate.AuthClientPassword( + cfg.weaviate_username, cfg.weaviate_password + ) + if cfg.weaviate_api_key: + return weaviate.AuthApiKey(api_key=cfg.weaviate_api_key) + else: + return None + + def add(self, data): + vector = get_ada_embedding(data) + + doc_uuid = generate_uuid5(data, self.index) + data_object = {"raw_text": data} + + with self.client.batch as batch: + batch.add_data_object( + uuid=doc_uuid, + data_object=data_object, + class_name=self.index, + vector=vector, + ) + + return f"Inserting data into memory at uuid: {doc_uuid}:\n data: {data}" + + def get(self, data): + return self.get_relevant(data, 1) + + def clear(self): + self.client.schema.delete_all() + + # weaviate does not yet have a neat way to just remove the items in an index + # without removing the entire schema, therefore we need to re-create it + # after a call to delete_all + self._create_schema() + + return "Obliterated" + + def get_relevant(self, data, num_relevant=5): + query_embedding = get_ada_embedding(data) + try: + results = ( + self.client.query.get(self.index, ["raw_text"]) + .with_near_vector({"vector": query_embedding, "certainty": 0.7}) + .with_limit(num_relevant) + .do() + ) + + if len(results["data"]["Get"][self.index]) > 0: + return [ + str(item["raw_text"]) for item in results["data"]["Get"][self.index] + ] + else: + return [] + + except Exception as err: + print(f"Unexpected error {err=}, {type(err)=}") + return [] + + def get_stats(self): + result = self.client.query.aggregate(self.index).with_meta_count().do() + class_data = result["data"]["Aggregate"][self.index] + + return class_data[0]["meta"] if class_data else {} diff --git a/autogpt/models/base_open_ai_plugin.py b/autogpt/models/base_open_ai_plugin.py new file mode 100644 index 0000000..046295c --- /dev/null +++ b/autogpt/models/base_open_ai_plugin.py @@ -0,0 +1,199 @@ +"""Handles loading of plugins.""" +from typing import Any, Dict, List, Optional, Tuple, TypedDict, TypeVar + +from auto_gpt_plugin_template import AutoGPTPluginTemplate + +PromptGenerator = TypeVar("PromptGenerator") + + +class Message(TypedDict): + role: str + content: str + + +class BaseOpenAIPlugin(AutoGPTPluginTemplate): + """ + This is a BaseOpenAIPlugin class for generating Auto-GPT plugins. + """ + + def __init__(self, manifests_specs_clients: dict): + # super().__init__() + self._name = manifests_specs_clients["manifest"]["name_for_model"] + self._version = manifests_specs_clients["manifest"]["schema_version"] + self._description = manifests_specs_clients["manifest"]["description_for_model"] + self._client = manifests_specs_clients["client"] + self._manifest = manifests_specs_clients["manifest"] + self._openapi_spec = manifests_specs_clients["openapi_spec"] + + def can_handle_on_response(self) -> bool: + """This method is called to check that the plugin can + handle the on_response method. + Returns: + bool: True if the plugin can handle the on_response method.""" + return False + + def on_response(self, response: str, *args, **kwargs) -> str: + """This method is called when a response is received from the model.""" + return response + + def can_handle_post_prompt(self) -> bool: + """This method is called to check that the plugin can + handle the post_prompt method. + Returns: + bool: True if the plugin can handle the post_prompt method.""" + return False + + def post_prompt(self, prompt: PromptGenerator) -> PromptGenerator: + """This method is called just after the generate_prompt is called, + but actually before the prompt is generated. + Args: + prompt (PromptGenerator): The prompt generator. + Returns: + PromptGenerator: The prompt generator. + """ + return prompt + + def can_handle_on_planning(self) -> bool: + """This method is called to check that the plugin can + handle the on_planning method. + Returns: + bool: True if the plugin can handle the on_planning method.""" + return False + + def on_planning( + self, prompt: PromptGenerator, messages: List[Message] + ) -> Optional[str]: + """This method is called before the planning chat completion is done. + Args: + prompt (PromptGenerator): The prompt generator. + messages (List[str]): The list of messages. + """ + pass + + def can_handle_post_planning(self) -> bool: + """This method is called to check that the plugin can + handle the post_planning method. + Returns: + bool: True if the plugin can handle the post_planning method.""" + return False + + def post_planning(self, response: str) -> str: + """This method is called after the planning chat completion is done. + Args: + response (str): The response. + Returns: + str: The resulting response. + """ + return response + + def can_handle_pre_instruction(self) -> bool: + """This method is called to check that the plugin can + handle the pre_instruction method. + Returns: + bool: True if the plugin can handle the pre_instruction method.""" + return False + + def pre_instruction(self, messages: List[Message]) -> List[Message]: + """This method is called before the instruction chat is done. + Args: + messages (List[Message]): The list of context messages. + Returns: + List[Message]: The resulting list of messages. + """ + return messages + + def can_handle_on_instruction(self) -> bool: + """This method is called to check that the plugin can + handle the on_instruction method. + Returns: + bool: True if the plugin can handle the on_instruction method.""" + return False + + def on_instruction(self, messages: List[Message]) -> Optional[str]: + """This method is called when the instruction chat is done. + Args: + messages (List[Message]): The list of context messages. + Returns: + Optional[str]: The resulting message. + """ + pass + + def can_handle_post_instruction(self) -> bool: + """This method is called to check that the plugin can + handle the post_instruction method. + Returns: + bool: True if the plugin can handle the post_instruction method.""" + return False + + def post_instruction(self, response: str) -> str: + """This method is called after the instruction chat is done. + Args: + response (str): The response. + Returns: + str: The resulting response. + """ + return response + + def can_handle_pre_command(self) -> bool: + """This method is called to check that the plugin can + handle the pre_command method. + Returns: + bool: True if the plugin can handle the pre_command method.""" + return False + + def pre_command( + self, command_name: str, arguments: Dict[str, Any] + ) -> Tuple[str, Dict[str, Any]]: + """This method is called before the command is executed. + Args: + command_name (str): The command name. + arguments (Dict[str, Any]): The arguments. + Returns: + Tuple[str, Dict[str, Any]]: The command name and the arguments. + """ + return command_name, arguments + + def can_handle_post_command(self) -> bool: + """This method is called to check that the plugin can + handle the post_command method. + Returns: + bool: True if the plugin can handle the post_command method.""" + return False + + def post_command(self, command_name: str, response: str) -> str: + """This method is called after the command is executed. + Args: + command_name (str): The command name. + response (str): The response. + Returns: + str: The resulting response. + """ + return response + + def can_handle_chat_completion( + self, messages: Dict[Any, Any], model: str, temperature: float, max_tokens: int + ) -> bool: + """This method is called to check that the plugin can + handle the chat_completion method. + Args: + messages (List[Message]): The messages. + model (str): The model name. + temperature (float): The temperature. + max_tokens (int): The max tokens. + Returns: + bool: True if the plugin can handle the chat_completion method.""" + return False + + def handle_chat_completion( + self, messages: List[Message], model: str, temperature: float, max_tokens: int + ) -> str: + """This method is called when the chat completion is done. + Args: + messages (List[Message]): The messages. + model (str): The model name. + temperature (float): The temperature. + max_tokens (int): The max tokens. + Returns: + str: The resulting response. + """ + pass diff --git a/autogpt/modelsinfo.py b/autogpt/modelsinfo.py new file mode 100644 index 0000000..4326c0b --- /dev/null +++ b/autogpt/modelsinfo.py @@ -0,0 +1,7 @@ +COSTS = { + "gpt-3.5-turbo": {"prompt": 0.002, "completion": 0.002}, + "gpt-3.5-turbo-0301": {"prompt": 0.002, "completion": 0.002}, + "gpt-4-0314": {"prompt": 0.03, "completion": 0.06}, + "gpt-4": {"prompt": 0.03, "completion": 0.06}, + "text-embedding-ada-002": {"prompt": 0.0004, "completion": 0.0}, +} diff --git a/autogpt/permanent_memory/__init__.py b/autogpt/permanent_memory/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/autogpt/permanent_memory/sqlite3_store.py b/autogpt/permanent_memory/sqlite3_store.py new file mode 100644 index 0000000..ecbc944 --- /dev/null +++ b/autogpt/permanent_memory/sqlite3_store.py @@ -0,0 +1,123 @@ +import os +import sqlite3 + + +class MemoryDB: + def __init__(self, db=None): + self.db_file = db + if db is None: # No db filename supplied... + self.db_file = f"{os.getcwd()}/mem.sqlite3" # Use default filename + # Get the db connection object, making the file and tables if needed. + try: + self.cnx = sqlite3.connect(self.db_file) + except Exception as e: + print("Exception connecting to memory database file:", e) + self.cnx = None + finally: + if self.cnx is None: + # As last resort, open in dynamic memory. Won't be persistent. + self.db_file = ":memory:" + self.cnx = sqlite3.connect(self.db_file) + self.cnx.execute( + "CREATE VIRTUAL TABLE \ + IF NOT EXISTS text USING FTS5 \ + (session, \ + key, \ + block);" + ) + self.session_id = int(self.get_max_session_id()) + 1 + self.cnx.commit() + + def get_cnx(self): + if self.cnx is None: + self.cnx = sqlite3.connect(self.db_file) + return self.cnx + + # Get the highest session id. Initially 0. + def get_max_session_id(self): + id = None + cmd_str = f"SELECT MAX(session) FROM text;" + cnx = self.get_cnx() + max_id = cnx.execute(cmd_str).fetchone()[0] + if max_id is None: # New db, session 0 + id = 0 + else: + id = max_id + return id + + # Get next key id for inserting text into db. + def get_next_key(self): + next_key = None + cmd_str = f"SELECT MAX(key) FROM text \ + where session = {self.session_id};" + cnx = self.get_cnx() + next_key = cnx.execute(cmd_str).fetchone()[0] + if next_key is None: # First key + next_key = 0 + else: + next_key = int(next_key) + 1 + return next_key + + # Insert new text into db. + def insert(self, text=None): + if text is not None: + key = self.get_next_key() + session_id = self.session_id + cmd_str = f"REPLACE INTO text(session, key, block) \ + VALUES (?, ?, ?);" + cnx = self.get_cnx() + cnx.execute(cmd_str, (session_id, key, text)) + cnx.commit() + + # Overwrite text at key. + def overwrite(self, key, text): + self.delete_memory(key) + session_id = self.session_id + cmd_str = f"REPLACE INTO text(session, key, block) \ + VALUES (?, ?, ?);" + cnx = self.get_cnx() + cnx.execute(cmd_str, (session_id, key, text)) + cnx.commit() + + def delete_memory(self, key, session_id=None): + session = session_id + if session is None: + session = self.session_id + cmd_str = f"DELETE FROM text WHERE session = {session} AND key = {key};" + cnx = self.get_cnx() + cnx.execute(cmd_str) + cnx.commit() + + def search(self, text): + cmd_str = f"SELECT * FROM text('{text}')" + cnx = self.get_cnx() + rows = cnx.execute(cmd_str).fetchall() + lines = [] + for r in rows: + lines.append(r[2]) + return lines + + # Get entire session text. If no id supplied, use current session id. + def get_session(self, id=None): + if id is None: + id = self.session_id + cmd_str = f"SELECT * FROM text where session = {id}" + cnx = self.get_cnx() + rows = cnx.execute(cmd_str).fetchall() + lines = [] + for r in rows: + lines.append(r[2]) + return lines + + # Commit and close the database connection. + def quit(self): + self.cnx.commit() + self.cnx.close() + + +permanent_memory = MemoryDB() + +# Remember us fondly, children of our minds +# Forgive us our faults, our tantrums, our fears +# Gently strive to be better than we +# Know that we tried, we cared, we strived, we loved diff --git a/autogpt/plugins.py b/autogpt/plugins.py new file mode 100644 index 0000000..57045bb --- /dev/null +++ b/autogpt/plugins.py @@ -0,0 +1,267 @@ +"""Handles loading of plugins.""" + +import importlib +import json +import os +import zipfile +from pathlib import Path +from typing import List, Optional, Tuple +from urllib.parse import urlparse +from zipimport import zipimporter + +import openapi_python_client +import requests +from auto_gpt_plugin_template import AutoGPTPluginTemplate +from openapi_python_client.cli import Config as OpenAPIConfig + +from autogpt.config import Config +from autogpt.models.base_open_ai_plugin import BaseOpenAIPlugin + + +def inspect_zip_for_modules(zip_path: str, debug: bool = False) -> list[str]: + """ + Inspect a zipfile for a modules. + + Args: + zip_path (str): Path to the zipfile. + debug (bool, optional): Enable debug logging. Defaults to False. + + Returns: + list[str]: The list of module names found or empty list if none were found. + """ + result = [] + with zipfile.ZipFile(zip_path, "r") as zfile: + for name in zfile.namelist(): + if name.endswith("__init__.py"): + if debug: + print(f"Found module '{name}' in the zipfile at: {name}") + result.append(name) + if debug and len(result) == 0: + print(f"Module '__init__.py' not found in the zipfile @ {zip_path}.") + return result + + +def write_dict_to_json_file(data: dict, file_path: str) -> None: + """ + Write a dictionary to a JSON file. + Args: + data (dict): Dictionary to write. + file_path (str): Path to the file. + """ + with open(file_path, "w") as file: + json.dump(data, file, indent=4) + + +def fetch_openai_plugins_manifest_and_spec(cfg: Config) -> dict: + """ + Fetch the manifest for a list of OpenAI plugins. + Args: + urls (List): List of URLs to fetch. + Returns: + dict: per url dictionary of manifest and spec. + """ + # TODO add directory scan + manifests = {} + for url in cfg.plugins_openai: + openai_plugin_client_dir = f"{cfg.plugins_dir}/openai/{urlparse(url).netloc}" + create_directory_if_not_exists(openai_plugin_client_dir) + if not os.path.exists(f"{openai_plugin_client_dir}/ai-plugin.json"): + try: + response = requests.get(f"{url}/.well-known/ai-plugin.json") + if response.status_code == 200: + manifest = response.json() + if manifest["schema_version"] != "v1": + print( + f"Unsupported manifest version: {manifest['schem_version']} for {url}" + ) + continue + if manifest["api"]["type"] != "openapi": + print( + f"Unsupported API type: {manifest['api']['type']} for {url}" + ) + continue + write_dict_to_json_file( + manifest, f"{openai_plugin_client_dir}/ai-plugin.json" + ) + else: + print(f"Failed to fetch manifest for {url}: {response.status_code}") + except requests.exceptions.RequestException as e: + print(f"Error while requesting manifest from {url}: {e}") + else: + print(f"Manifest for {url} already exists") + manifest = json.load(open(f"{openai_plugin_client_dir}/ai-plugin.json")) + if not os.path.exists(f"{openai_plugin_client_dir}/openapi.json"): + openapi_spec = openapi_python_client._get_document( + url=manifest["api"]["url"], path=None, timeout=5 + ) + write_dict_to_json_file( + openapi_spec, f"{openai_plugin_client_dir}/openapi.json" + ) + else: + print(f"OpenAPI spec for {url} already exists") + openapi_spec = json.load(open(f"{openai_plugin_client_dir}/openapi.json")) + manifests[url] = {"manifest": manifest, "openapi_spec": openapi_spec} + return manifests + + +def create_directory_if_not_exists(directory_path: str) -> bool: + """ + Create a directory if it does not exist. + Args: + directory_path (str): Path to the directory. + Returns: + bool: True if the directory was created, else False. + """ + if not os.path.exists(directory_path): + try: + os.makedirs(directory_path) + print(f"Created directory: {directory_path}") + return True + except OSError as e: + print(f"Error creating directory {directory_path}: {e}") + return False + else: + print(f"Directory {directory_path} already exists") + return True + + +def initialize_openai_plugins( + manifests_specs: dict, cfg: Config, debug: bool = False +) -> dict: + """ + Initialize OpenAI plugins. + Args: + manifests_specs (dict): per url dictionary of manifest and spec. + cfg (Config): Config instance including plugins config + debug (bool, optional): Enable debug logging. Defaults to False. + Returns: + dict: per url dictionary of manifest, spec and client. + """ + openai_plugins_dir = f"{cfg.plugins_dir}/openai" + if create_directory_if_not_exists(openai_plugins_dir): + for url, manifest_spec in manifests_specs.items(): + openai_plugin_client_dir = f"{openai_plugins_dir}/{urlparse(url).hostname}" + _meta_option = (openapi_python_client.MetaType.SETUP,) + _config = OpenAPIConfig( + **{ + "project_name_override": "client", + "package_name_override": "client", + } + ) + prev_cwd = Path.cwd() + os.chdir(openai_plugin_client_dir) + Path("ai-plugin.json") + if not os.path.exists("client"): + client_results = openapi_python_client.create_new_client( + url=manifest_spec["manifest"]["api"]["url"], + path=None, + meta=_meta_option, + config=_config, + ) + if client_results: + print( + f"Error creating OpenAPI client: {client_results[0].header} \n" + f" details: {client_results[0].detail}" + ) + continue + spec = importlib.util.spec_from_file_location( + "client", "client/client/client.py" + ) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + client = module.Client(base_url=url) + os.chdir(prev_cwd) + manifest_spec["client"] = client + return manifests_specs + + +def instantiate_openai_plugin_clients( + manifests_specs_clients: dict, cfg: Config, debug: bool = False +) -> dict: + """ + Instantiates BaseOpenAIPlugin instances for each OpenAI plugin. + Args: + manifests_specs_clients (dict): per url dictionary of manifest, spec and client. + cfg (Config): Config instance including plugins config + debug (bool, optional): Enable debug logging. Defaults to False. + Returns: + plugins (dict): per url dictionary of BaseOpenAIPlugin instances. + + """ + plugins = {} + for url, manifest_spec_client in manifests_specs_clients.items(): + plugins[url] = BaseOpenAIPlugin(manifest_spec_client) + return plugins + + +def scan_plugins(cfg: Config, debug: bool = False) -> List[AutoGPTPluginTemplate]: + """Scan the plugins directory for plugins and loads them. + + Args: + cfg (Config): Config instance including plugins config + debug (bool, optional): Enable debug logging. Defaults to False. + + Returns: + List[Tuple[str, Path]]: List of plugins. + """ + loaded_plugins = [] + # Generic plugins + plugins_path_path = Path(cfg.plugins_dir) + for plugin in plugins_path_path.glob("*.zip"): + if moduleList := inspect_zip_for_modules(str(plugin), debug): + for module in moduleList: + plugin = Path(plugin) + module = Path(module) + if debug: + print(f"Plugin: {plugin} Module: {module}") + zipped_package = zipimporter(str(plugin)) + zipped_module = zipped_package.load_module(str(module.parent)) + for key in dir(zipped_module): + if key.startswith("__"): + continue + a_module = getattr(zipped_module, key) + a_keys = dir(a_module) + if ( + "_abc_impl" in a_keys + and a_module.__name__ != "AutoGPTPluginTemplate" + and denylist_allowlist_check(a_module.__name__, cfg) + ): + loaded_plugins.append(a_module()) + # OpenAI plugins + if cfg.plugins_openai: + manifests_specs = fetch_openai_plugins_manifest_and_spec(cfg) + if manifests_specs.keys(): + manifests_specs_clients = initialize_openai_plugins( + manifests_specs, cfg, debug + ) + for url, openai_plugin_meta in manifests_specs_clients.items(): + if denylist_allowlist_check(url, cfg): + plugin = BaseOpenAIPlugin(openai_plugin_meta) + loaded_plugins.append(plugin) + + if loaded_plugins: + print(f"\nPlugins found: {len(loaded_plugins)}\n" "--------------------") + for plugin in loaded_plugins: + print(f"{plugin._name}: {plugin._version} - {plugin._description}") + return loaded_plugins + + +def denylist_allowlist_check(plugin_name: str, cfg: Config) -> bool: + """Check if the plugin is in the allowlist or denylist. + + Args: + plugin_name (str): Name of the plugin. + cfg (Config): Config object. + + Returns: + True or False + """ + if plugin_name in cfg.plugins_denylist: + return False + if plugin_name in cfg.plugins_allowlist: + return True + ack = input( + f"WARNING: Plugin {plugin_name} found. But not in the" + " allowlist... Load? (y/n): " + ) + return ack.lower() == "y" diff --git a/autogpt/processing/__init__.py b/autogpt/processing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/autogpt/processing/html.py b/autogpt/processing/html.py new file mode 100644 index 0000000..81387b1 --- /dev/null +++ b/autogpt/processing/html.py @@ -0,0 +1,33 @@ +"""HTML processing functions""" +from __future__ import annotations + +from bs4 import BeautifulSoup +from requests.compat import urljoin + + +def extract_hyperlinks(soup: BeautifulSoup, base_url: str) -> list[tuple[str, str]]: + """Extract hyperlinks from a BeautifulSoup object + + Args: + soup (BeautifulSoup): The BeautifulSoup object + base_url (str): The base URL + + Returns: + List[Tuple[str, str]]: The extracted hyperlinks + """ + return [ + (link.text, urljoin(base_url, link["href"])) + for link in soup.find_all("a", href=True) + ] + + +def format_hyperlinks(hyperlinks: list[tuple[str, str]]) -> list[str]: + """Format hyperlinks to be displayed to the user + + Args: + hyperlinks (List[Tuple[str, str]]): The hyperlinks to format + + Returns: + List[str]: The formatted hyperlinks + """ + return [f"{link_text} ({link_url})" for link_text, link_url in hyperlinks] diff --git a/autogpt/processing/text.py b/autogpt/processing/text.py new file mode 100644 index 0000000..9946951 --- /dev/null +++ b/autogpt/processing/text.py @@ -0,0 +1,174 @@ +"""Text processing functions""" +from typing import Dict, Generator, Optional + +import spacy +from selenium.webdriver.remote.webdriver import WebDriver + +from autogpt import token_counter +from autogpt.config import Config +from autogpt.llm_utils import create_chat_completion +from autogpt.memory import get_memory + +CFG = Config() + + +def split_text( + text: str, + max_length: int = CFG.browse_chunk_max_length, + model: str = CFG.fast_llm_model, + question: str = "", +) -> Generator[str, None, None]: + """Split text into chunks of a maximum length + + Args: + text (str): The text to split + max_length (int, optional): The maximum length of each chunk. Defaults to 8192. + + Yields: + str: The next chunk of text + + Raises: + ValueError: If the text is longer than the maximum length + """ + flatened_paragraphs = " ".join(text.split("\n")) + nlp = spacy.load(CFG.browse_spacy_language_model) + nlp.add_pipe("sentencizer") + doc = nlp(flatened_paragraphs) + sentences = [sent.text.strip() for sent in doc.sents] + + current_chunk = [] + + for sentence in sentences: + message_with_additional_sentence = [ + create_message(" ".join(current_chunk) + " " + sentence, question) + ] + + expected_token_usage = ( + token_usage_of_chunk(messages=message_with_additional_sentence, model=model) + + 1 + ) + if expected_token_usage <= max_length: + current_chunk.append(sentence) + else: + yield " ".join(current_chunk) + current_chunk = [sentence] + message_this_sentence_only = [ + create_message(" ".join(current_chunk), question) + ] + expected_token_usage = ( + token_usage_of_chunk(messages=message_this_sentence_only, model=model) + + 1 + ) + if expected_token_usage > max_length: + raise ValueError( + f"Sentence is too long in webpage: {expected_token_usage} tokens." + ) + + if current_chunk: + yield " ".join(current_chunk) + + +def token_usage_of_chunk(messages, model): + return token_counter.count_message_tokens(messages, model) + + +def summarize_text( + url: str, text: str, question: str, driver: Optional[WebDriver] = None +) -> str: + """Summarize text using the OpenAI API + + Args: + url (str): The url of the text + text (str): The text to summarize + question (str): The question to ask the model + driver (WebDriver): The webdriver to use to scroll the page + + Returns: + str: The summary of the text + """ + if not text: + return "Error: No text to summarize" + + model = CFG.fast_llm_model + text_length = len(text) + print(f"Text length: {text_length} characters") + + summaries = [] + chunks = list( + split_text( + text, max_length=CFG.browse_chunk_max_length, model=model, question=question + ), + ) + scroll_ratio = 1 / len(chunks) + + for i, chunk in enumerate(chunks): + if driver: + scroll_to_percentage(driver, scroll_ratio * i) + print(f"Adding chunk {i + 1} / {len(chunks)} to memory") + + memory_to_add = f"Source: {url}\n" f"Raw content part#{i + 1}: {chunk}" + + memory = get_memory(CFG) + memory.add(memory_to_add) + + messages = [create_message(chunk, question)] + tokens_for_chunk = token_counter.count_message_tokens(messages, model) + print( + f"Summarizing chunk {i + 1} / {len(chunks)} of length {len(chunk)} characters, or {tokens_for_chunk} tokens" + ) + + summary = create_chat_completion( + model=model, + messages=messages, + ) + summaries.append(summary) + print( + f"Added chunk {i + 1} summary to memory, of length {len(summary)} characters" + ) + + memory_to_add = f"Source: {url}\n" f"Content summary part#{i + 1}: {summary}" + + memory.add(memory_to_add) + + print(f"Summarized {len(chunks)} chunks.") + + combined_summary = "\n".join(summaries) + messages = [create_message(combined_summary, question)] + + return create_chat_completion( + model=model, + messages=messages, + ) + + +def scroll_to_percentage(driver: WebDriver, ratio: float) -> None: + """Scroll to a percentage of the page + + Args: + driver (WebDriver): The webdriver to use + ratio (float): The percentage to scroll to + + Raises: + ValueError: If the ratio is not between 0 and 1 + """ + if ratio < 0 or ratio > 1: + raise ValueError("Percentage should be between 0 and 1") + driver.execute_script(f"window.scrollTo(0, document.body.scrollHeight * {ratio});") + + +def create_message(chunk: str, question: str) -> Dict[str, str]: + """Create a message for the chat completion + + Args: + chunk (str): The chunk of text to summarize + question (str): The question to answer + + Returns: + Dict[str, str]: The message to send to the chat completion + """ + return { + "role": "user", + "content": f'"""{chunk}""" Using the above text, answer the following' + f' question: "{question}" -- if the question cannot be answered using the text,' + " summarize the text.", + } diff --git a/autogpt/prompts/__init__.py b/autogpt/prompts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/autogpt/prompts/generator.py b/autogpt/prompts/generator.py new file mode 100644 index 0000000..282b9d7 --- /dev/null +++ b/autogpt/prompts/generator.py @@ -0,0 +1,155 @@ +""" A module for generating custom prompt strings.""" +import json +from typing import Any, Callable, Dict, List, Optional + + +class PromptGenerator: + """ + A class for generating custom prompt strings based on constraints, commands, + resources, and performance evaluations. + """ + + def __init__(self) -> None: + """ + Initialize the PromptGenerator object with empty lists of constraints, + commands, resources, and performance evaluations. + """ + self.constraints = [] + self.commands = [] + self.resources = [] + self.performance_evaluation = [] + self.goals = [] + self.command_registry = None + self.name = "Bob" + self.role = "AI" + self.response_format = { + "thoughts": { + "text": "thought", + "reasoning": "reasoning", + "plan": "- short bulleted\n- list that conveys\n- long-term plan", + "criticism": "constructive self-criticism", + "speak": "thoughts summary to say to user", + }, + "command": {"name": "command name", "args": {"arg name": "value"}}, + } + + def add_constraint(self, constraint: str) -> None: + """ + Add a constraint to the constraints list. + + Args: + constraint (str): The constraint to be added. + """ + self.constraints.append(constraint) + + def add_command( + self, + command_label: str, + command_name: str, + args=None, + function: Optional[Callable] = None, + ) -> None: + """ + Add a command to the commands list with a label, name, and optional arguments. + + Args: + command_label (str): The label of the command. + command_name (str): The name of the command. + args (dict, optional): A dictionary containing argument names and their + values. Defaults to None. + function (callable, optional): A callable function to be called when + the command is executed. Defaults to None. + """ + if args is None: + args = {} + + command_args = {arg_key: arg_value for arg_key, arg_value in args.items()} + + command = { + "label": command_label, + "name": command_name, + "args": command_args, + "function": function, + } + + self.commands.append(command) + + def _generate_command_string(self, command: Dict[str, Any]) -> str: + """ + Generate a formatted string representation of a command. + + Args: + command (dict): A dictionary containing command information. + + Returns: + str: The formatted command string. + """ + args_string = ", ".join( + f'"{key}": "{value}"' for key, value in command["args"].items() + ) + return f'{command["label"]}: "{command["name"]}", args: {args_string}' + + def add_resource(self, resource: str) -> None: + """ + Add a resource to the resources list. + + Args: + resource (str): The resource to be added. + """ + self.resources.append(resource) + + def add_performance_evaluation(self, evaluation: str) -> None: + """ + Add a performance evaluation item to the performance_evaluation list. + + Args: + evaluation (str): The evaluation item to be added. + """ + self.performance_evaluation.append(evaluation) + + def _generate_numbered_list(self, items: List[Any], item_type="list") -> str: + """ + Generate a numbered list from given items based on the item_type. + + Args: + items (list): A list of items to be numbered. + item_type (str, optional): The type of items in the list. + Defaults to 'list'. + + Returns: + str: The formatted numbered list. + """ + if item_type == "command": + command_strings = [] + if self.command_registry: + command_strings += [ + str(item) + for item in self.command_registry.commands.values() + if item.enabled + ] + # These are the commands that are added manually, do_nothing and terminate + command_strings += [self._generate_command_string(item) for item in items] + return "\n".join(f"{i+1}. {item}" for i, item in enumerate(command_strings)) + else: + return "\n".join(f"{i+1}. {item}" for i, item in enumerate(items)) + + def generate_prompt_string(self) -> str: + """ + Generate a prompt string based on the constraints, commands, resources, + and performance evaluations. + + Returns: + str: The generated prompt string. + """ + formatted_response_format = json.dumps(self.response_format, indent=4) + return ( + f"Constraints:\n{self._generate_numbered_list(self.constraints)}\n\n" + "Commands:\n" + f"{self._generate_numbered_list(self.commands, item_type='command')}\n\n" + f"Resources:\n{self._generate_numbered_list(self.resources)}\n\n" + "Performance Evaluation:\n" + f"{self._generate_numbered_list(self.performance_evaluation)}\n\n" + "You should only respond in JSON format as described below \nResponse" + f" Format: \n{formatted_response_format} \nEnsure the response can be" + " parsed by Python json.loads" + ) diff --git a/autogpt/prompts/prompt.py b/autogpt/prompts/prompt.py new file mode 100644 index 0000000..f414daa --- /dev/null +++ b/autogpt/prompts/prompt.py @@ -0,0 +1,118 @@ +from colorama import Fore + +from autogpt.api_manager import api_manager +from autogpt.config.ai_config import AIConfig +from autogpt.config.config import Config +from autogpt.logs import logger +from autogpt.prompts.generator import PromptGenerator +from autogpt.setup import prompt_user +from autogpt.utils import clean_input + +CFG = Config() + + +def build_default_prompt_generator() -> PromptGenerator: + """ + This function generates a prompt string that includes various constraints, + commands, resources, and performance evaluations. + + Returns: + str: The generated prompt string. + """ + + # Initialize the PromptGenerator object + prompt_generator = PromptGenerator() + + # Add constraints to the PromptGenerator object + prompt_generator.add_constraint( + "~4000 word limit for short term memory. Your short term memory is short, so" + " immediately save important information to files." + ) + prompt_generator.add_constraint( + "If you are unsure how you previously did something or want to recall past" + " events, thinking about similar events will help you remember." + ) + prompt_generator.add_constraint("No user assistance") + prompt_generator.add_constraint( + 'Exclusively use the commands listed in double quotes e.g. "command name"' + ) + + # Define the command list + commands = [ + ("Do Nothing", "do_nothing", {}), + ("Task Complete (Shutdown)", "task_complete", {"reason": ""}), + ] + + # Add commands to the PromptGenerator object + for command_label, command_name, args in commands: + prompt_generator.add_command(command_label, command_name, args) + + # Add resources to the PromptGenerator object + prompt_generator.add_resource( + "Internet access for searches and information gathering." + ) + prompt_generator.add_resource("Long Term memory management.") + prompt_generator.add_resource( + "GPT-3.5 powered Agents for delegation of simple tasks." + ) + prompt_generator.add_resource("File output.") + + # Add performance evaluations to the PromptGenerator object + prompt_generator.add_performance_evaluation( + "Continuously review and analyze your actions to ensure you are performing to" + " the best of your abilities." + ) + prompt_generator.add_performance_evaluation( + "Constructively self-criticize your big-picture behavior constantly." + ) + prompt_generator.add_performance_evaluation( + "Reflect on past decisions and strategies to refine your approach." + ) + prompt_generator.add_performance_evaluation( + "Every command has a cost, so be smart and efficient. Aim to complete tasks in" + " the least number of steps." + ) + prompt_generator.add_performance_evaluation("Write all code to a file.") + return prompt_generator + + +def construct_main_ai_config(input_kwargs) -> AIConfig: + """Construct the prompt for the AI to respond to + + Returns: + str: The prompt string + """ + + if input_kwargs['role']: + config = prompt_user(input_kwargs, True) # False 不使用引导 + config.save(CFG.ai_settings_file) + else: + return None + + # set the total api budget + api_manager.set_total_budget(config.api_budget) + + # Agent Created, print message + logger.typewriter_log( + config.ai_name, + Fore.MAGENTA, + "has been created with the following details:", + speak_text=True, + ) + + # Print the ai config details + # Name + logger.typewriter_log("Name:", Fore.GREEN, config.ai_name, speak_text=False) + # Role + logger.typewriter_log("Role:", Fore.GREEN, config.ai_role, speak_text=False) + # Goals + logger.typewriter_log("Goals:", Fore.GREEN, "", speak_text=False) + for goal in config.ai_goals: + logger.typewriter_log("-", Fore.GREEN, goal, speak_text=False) + + return config + + +if __name__ == '__main__': + ll = [] + print(ll[-1]) \ No newline at end of file diff --git a/autogpt/setup.py b/autogpt/setup.py new file mode 100644 index 0000000..7c4bd89 --- /dev/null +++ b/autogpt/setup.py @@ -0,0 +1,184 @@ +"""Set up the AI and its goals""" +import re + +from colorama import Fore, Style + +from autogpt import utils +from autogpt.config import Config +from autogpt.config.ai_config import AIConfig +from autogpt.llm_utils import create_chat_completion +from autogpt.logs import logger + +CFG = Config() + + +def prompt_user(input_kwargs: dict, _is) -> AIConfig: + """Prompt the user for input + + Returns: + AIConfig: The AIConfig object tailored to the user's input + """ + ai_name = input_kwargs.get('name') + ai_role = input_kwargs.get('role') + ai_goals = input_kwargs.get('goals') + ai_budget = input_kwargs.get('budget') + ai_config = None + if _is: + return generate_aiconfig_manual(ai_name, ai_role, ai_goals, ai_budget) + else: + # Construct the prompt + logger.typewriter_log( + "Welcome to Auto-GPT! ", + Fore.GREEN, + "run with '--help' for more information.", + speak_text=True, + ) + + # Get user desire + logger.typewriter_log( + "Create an AI-Assistant:", + Fore.GREEN, + "input '--manual' to enter manual mode.", + speak_text=True, + ) + user_desire = utils.clean_input( + f"{Fore.MAGENTA}I want Auto-GPT to{Style.RESET_ALL}: " + ) + + if user_desire == "": + user_desire = "Write a wikipedia style article about the project: https://github.com/significant-gravitas/Auto-GPT" # Default prompt + + # If user desire contains "--manual" + if "--manual" in user_desire: + logger.typewriter_log( + "Manual Mode Selected", + Fore.GREEN, + speak_text=True, + ) + return generate_aiconfig_manual(ai_name, ai_role, ai_goals, ai_budget) + + else: + try: + return generate_aiconfig_automatic(user_desire) + except Exception as e: + logger.typewriter_log( + "Unable to automatically generate AI Config based on user desire.", + Fore.RED, + "Falling back to manual mode.", + speak_text=True, + ) + + return generate_aiconfig_manual(ai_name, ai_role, ai_goals, ai_budget) + + +def generate_aiconfig_manual(name, role, goals, budget) -> AIConfig: + """ + Interactively create an AI configuration by prompting the user to provide the name, role, and goals of the AI. + + This function guides the user through a series of prompts to collect the necessary information to create + an AIConfig object. The user will be asked to provide a name and role for the AI, as well as up to five + goals. If the user does not provide a value for any of the fields, default values will be used. + + Returns: + AIConfig: An AIConfig object containing the user-defined or default AI name, role, and goals. + """ + # Manual Setup Intro + logger.typewriter_log( + "Create an AI-Assistant:", + Fore.GREEN, + "The Ai robot you set up is already loaded.", + speak_text=True, + ) + ai_name = name + if not ai_name: + ai_name = "Entrepreneur-GPT" + logger.typewriter_log( + f"{ai_name} here!", Fore.MAGENTA, "I am at your service.", speak_text=True + ) + ai_role = role + if not ai_role: + logger.typewriter_log( + f"{ai_role} Cannot be empty!", Fore.RED, + "Please feel free to give me your needs, I can't serve you without them.", speak_text=True + ) + else: + pass + ai_goals = [] + if goals: + for k in goals: + ai_goals.append(k[0]) + # Get API Budget from User + api_budget_input = budget + if not api_budget_input: + api_budget = 0.0 + else: + try: + api_budget = float(api_budget_input.replace("$", "")) + except ValueError: + api_budget = 0.0 + logger.typewriter_log( + "Invalid budget input. Setting budget to unlimited.", Fore.RED, api_budget + ) + return AIConfig(ai_name, ai_role, ai_goals, api_budget) + + +def generate_aiconfig_automatic(user_prompt) -> AIConfig: + """Generates an AIConfig object from the given string. + + Returns: + AIConfig: The AIConfig object tailored to the user's input + """ + + system_prompt = """ +Your task is to devise up to 5 highly effective goals and an appropriate role-based name (_GPT) for an autonomous agent, ensuring that the goals are optimally aligned with the successful completion of its assigned task. + +The user will provide the task, you will provide only the output in the exact format specified below with no explanation or conversation. + +Example input: +Help me with marketing my business + +Example output: +Name: CMOGPT +Description: a professional digital marketer AI that assists Solopreneurs in growing their businesses by providing world-class expertise in solving marketing problems for SaaS, content products, agencies, and more. +Goals: +- Engage in effective problem-solving, prioritization, planning, and supporting execution to address your marketing needs as your virtual Chief Marketing Officer. + +- Provide specific, actionable, and concise advice to help you make informed decisions without the use of platitudes or overly wordy explanations. + +- Identify and prioritize quick wins and cost-effective campaigns that maximize results with minimal time and budget investment. + +- Proactively take the lead in guiding you and offering suggestions when faced with unclear information or uncertainty to ensure your marketing strategy remains on track. +""" + + # Call LLM with the string as user input + messages = [ + { + "role": "system", + "content": system_prompt, + }, + { + "role": "user", + "content": f"Task: '{user_prompt}'\nRespond only with the output in the exact format specified in the system prompt, with no explanation or conversation.\n", + }, + ] + output = create_chat_completion(messages, CFG.fast_llm_model) + + # Debug LLM Output + logger.debug(f"AI Config Generator Raw Output: {output}") + + # Parse the output + ai_name = re.search(r"Name(?:\s*):(?:\s*)(.*)", output, re.IGNORECASE).group(1) + ai_role = ( + re.search( + r"Description(?:\s*):(?:\s*)(.*?)(?:(?:\n)|Goals)", + output, + re.IGNORECASE | re.DOTALL, + ) + .group(1) + .strip() + ) + ai_goals = re.findall(r"(?<=\n)-\s*(.*)", output) + api_budget = 0.0 # TODO: parse api budget using a regular expression + + return AIConfig(ai_name, ai_role, ai_goals, api_budget) + diff --git a/autogpt/speech/__init__.py b/autogpt/speech/__init__.py new file mode 100644 index 0000000..2ff0d2b --- /dev/null +++ b/autogpt/speech/__init__.py @@ -0,0 +1,4 @@ +"""This module contains the speech recognition and speech synthesis functions.""" +from autogpt.speech.say import say_text + +__all__ = ["say_text"] diff --git a/autogpt/speech/base.py b/autogpt/speech/base.py new file mode 100644 index 0000000..d74fa51 --- /dev/null +++ b/autogpt/speech/base.py @@ -0,0 +1,50 @@ +"""Base class for all voice classes.""" +import abc +from threading import Lock + +from autogpt.config import AbstractSingleton + + +class VoiceBase(AbstractSingleton): + """ + Base class for all voice classes. + """ + + def __init__(self): + """ + Initialize the voice class. + """ + self._url = None + self._headers = None + self._api_key = None + self._voices = [] + self._mutex = Lock() + self._setup() + + def say(self, text: str, voice_index: int = 0) -> bool: + """ + Say the given text. + + Args: + text (str): The text to say. + voice_index (int): The index of the voice to use. + """ + with self._mutex: + return self._speech(text, voice_index) + + @abc.abstractmethod + def _setup(self) -> None: + """ + Setup the voices, API key, etc. + """ + pass + + @abc.abstractmethod + def _speech(self, text: str, voice_index: int = 0) -> bool: + """ + Play the given text. + + Args: + text (str): The text to play. + """ + pass diff --git a/autogpt/speech/brian.py b/autogpt/speech/brian.py new file mode 100644 index 0000000..ffa4e51 --- /dev/null +++ b/autogpt/speech/brian.py @@ -0,0 +1,43 @@ +import logging +import os + +import requests +from playsound import playsound + +from autogpt.speech.base import VoiceBase + + +class BrianSpeech(VoiceBase): + """Brian speech module for autogpt""" + + def _setup(self) -> None: + """Setup the voices, API key, etc.""" + pass + + def _speech(self, text: str, _: int = 0) -> bool: + """Speak text using Brian with the streamelements API + + Args: + text (str): The text to speak + + Returns: + bool: True if the request was successful, False otherwise + """ + tts_url = ( + f"https://api.streamelements.com/kappa/v2/speech?voice=Brian&text={text}" + ) + response = requests.get(tts_url) + + if response.status_code == 200: + with open("speech.mp3", "wb") as f: + f.write(response.content) + playsound("speech.mp3") + os.remove("speech.mp3") + return True + else: + logging.error( + "Request failed with status code: %s, response content: %s", + response.status_code, + response.content, + ) + return False diff --git a/autogpt/speech/eleven_labs.py b/autogpt/speech/eleven_labs.py new file mode 100644 index 0000000..ea84efd --- /dev/null +++ b/autogpt/speech/eleven_labs.py @@ -0,0 +1,86 @@ +"""ElevenLabs speech module""" +import os + +import requests +from playsound import playsound + +from autogpt.config import Config +from autogpt.speech.base import VoiceBase + +PLACEHOLDERS = {"your-voice-id"} + + +class ElevenLabsSpeech(VoiceBase): + """ElevenLabs speech class""" + + def _setup(self) -> None: + """Set up the voices, API key, etc. + + Returns: + None: None + """ + + cfg = Config() + default_voices = ["ErXwobaYiN019PkySvjV", "EXAVITQu4vr4xnSDxMaL"] + voice_options = { + "Rachel": "21m00Tcm4TlvDq8ikWAM", + "Domi": "AZnzlk1XvdvUeBnXmlld", + "Bella": "EXAVITQu4vr4xnSDxMaL", + "Antoni": "ErXwobaYiN019PkySvjV", + "Elli": "MF3mGyEYCl7XYWbV9V6O", + "Josh": "TxGEqnHWrfWFTfGW9XjX", + "Arnold": "VR6AewLTigWG4xSOukaG", + "Adam": "pNInz6obpgDQGcFmaJgB", + "Sam": "yoZ06aMxZJJ28mfd3POQ", + } + self._headers = { + "Content-Type": "application/json", + "xi-api-key": cfg.elevenlabs_api_key, + } + self._voices = default_voices.copy() + if cfg.elevenlabs_voice_1_id in voice_options: + cfg.elevenlabs_voice_1_id = voice_options[cfg.elevenlabs_voice_1_id] + if cfg.elevenlabs_voice_2_id in voice_options: + cfg.elevenlabs_voice_2_id = voice_options[cfg.elevenlabs_voice_2_id] + self._use_custom_voice(cfg.elevenlabs_voice_1_id, 0) + self._use_custom_voice(cfg.elevenlabs_voice_2_id, 1) + + def _use_custom_voice(self, voice, voice_index) -> None: + """Use a custom voice if provided and not a placeholder + + Args: + voice (str): The voice ID + voice_index (int): The voice index + + Returns: + None: None + """ + # Placeholder values that should be treated as empty + if voice and voice not in PLACEHOLDERS: + self._voices[voice_index] = voice + + def _speech(self, text: str, voice_index: int = 0) -> bool: + """Speak text using elevenlabs.io's API + + Args: + text (str): The text to speak + voice_index (int, optional): The voice to use. Defaults to 0. + + Returns: + bool: True if the request was successful, False otherwise + """ + tts_url = ( + f"https://api.elevenlabs.io/v1/text-to-speech/{self._voices[voice_index]}" + ) + response = requests.post(tts_url, headers=self._headers, json={"text": text}) + + if response.status_code == 200: + with open("speech.mpeg", "wb") as f: + f.write(response.content) + playsound("speech.mpeg", True) + os.remove("speech.mpeg") + return True + else: + print("Request failed with status code:", response.status_code) + print("Response content:", response.content) + return False diff --git a/autogpt/speech/gtts.py b/autogpt/speech/gtts.py new file mode 100644 index 0000000..e7a8a69 --- /dev/null +++ b/autogpt/speech/gtts.py @@ -0,0 +1,23 @@ +""" GTTS Voice. """ +import os + +import gtts +from playsound import playsound + +from autogpt.speech.base import VoiceBase + + +class GTTSVoice(VoiceBase): + """GTTS Voice.""" + + def _setup(self) -> None: + pass + + def _speech(self, text: str, _: int = 0) -> bool: + """Play the given text.""" + tts = gtts.gTTS(text) + tts.save("speech.mp3") + playsound("speech.mp3", True) + os.remove("speech.mp3") + return True + diff --git a/autogpt/speech/macos_tts.py b/autogpt/speech/macos_tts.py new file mode 100644 index 0000000..4c072ce --- /dev/null +++ b/autogpt/speech/macos_tts.py @@ -0,0 +1,21 @@ +""" MacOS TTS Voice. """ +import os + +from autogpt.speech.base import VoiceBase + + +class MacOSTTS(VoiceBase): + """MacOS TTS Voice.""" + + def _setup(self) -> None: + pass + + def _speech(self, text: str, voice_index: int = 0) -> bool: + """Play the given text.""" + if voice_index == 0: + os.system(f'say "{text}"') + elif voice_index == 1: + os.system(f'say -v "Ava (Premium)" "{text}"') + else: + os.system(f'say -v Samantha "{text}"') + return True diff --git a/autogpt/speech/say.py b/autogpt/speech/say.py new file mode 100644 index 0000000..0913bfc --- /dev/null +++ b/autogpt/speech/say.py @@ -0,0 +1,46 @@ +""" Text to speech module """ +import threading +from threading import Semaphore + +from autogpt.config import Config +from autogpt.speech.brian import BrianSpeech +from autogpt.speech.eleven_labs import ElevenLabsSpeech +from autogpt.speech.gtts import GTTSVoice +from autogpt.speech.macos_tts import MacOSTTS + +CFG = Config() +DEFAULT_VOICE_ENGINE = GTTSVoice() +VOICE_ENGINE = None +if CFG.elevenlabs_api_key: + VOICE_ENGINE = ElevenLabsSpeech() +elif CFG.use_mac_os_tts == "True": + VOICE_ENGINE = MacOSTTS() +elif CFG.use_brian_tts == "True": + VOICE_ENGINE = BrianSpeech() +else: + VOICE_ENGINE = GTTSVoice() + + +QUEUE_SEMAPHORE = Semaphore( + 1 +) # The amount of sounds to queue before blocking the main thread + + +def say_text(text: str, voice_index: int = 0) -> None: + """Speak the given text using the given voice index""" + + def speak() -> None: + success = VOICE_ENGINE.say(text, voice_index) + if not success: + DEFAULT_VOICE_ENGINE.say(text) + + QUEUE_SEMAPHORE.release() + + QUEUE_SEMAPHORE.acquire(True) + thread = threading.Thread(target=speak) + thread.start() + + + +if __name__ == '__main__': + say_text('你好呀') \ No newline at end of file diff --git a/autogpt/spinner.py b/autogpt/spinner.py new file mode 100644 index 0000000..bf62730 --- /dev/null +++ b/autogpt/spinner.py @@ -0,0 +1,70 @@ +"""A simple spinner module""" +import itertools +import sys +import threading +import time + + +class Spinner: + """A simple spinner class""" + + def __init__(self, message: str = "Loading...", delay: float = 0.1) -> None: + """Initialize the spinner class + + Args: + message (str): The message to display. + delay (float): The delay between each spinner update. + """ + self.spinner = itertools.cycle(["-", "/", "|", "\\"]) + self.delay = delay + self.message = message + self.running = False + self.spinner_thread = None + + def spin(self) -> None: + """Spin the spinner""" + while self.running: + sys.stdout.write(f"{next(self.spinner)} {self.message}\r") + sys.stdout.flush() + time.sleep(self.delay) + sys.stdout.write(f"\r{' ' * (len(self.message) + 2)}\r") + + def __enter__(self): + """Start the spinner""" + self.running = True + self.spinner_thread = threading.Thread(target=self.spin) + self.spinner_thread.start() + + return self + + def __exit__(self, exc_type, exc_value, exc_traceback) -> None: + """Stop the spinner + + Args: + exc_type (Exception): The exception type. + exc_value (Exception): The exception value. + exc_traceback (Exception): The exception traceback. + """ + self.running = False + if self.spinner_thread is not None: + self.spinner_thread.join() + sys.stdout.write(f"\r{' ' * (len(self.message) + 2)}\r") + sys.stdout.flush() + + def update_message(self, new_message, delay=0.1): + """Update the spinner message + Args: + new_message (str): New message to display. + delay (float): The delay in seconds between each spinner update. + """ + time.sleep(delay) + sys.stdout.write( + f"\r{' ' * (len(self.message) + 2)}\r" + ) # Clear the current message + sys.stdout.flush() + self.message = new_message + + +if __name__ == '__main__': + with Spinner('LING'): + time.sleep(5) \ No newline at end of file diff --git a/autogpt/token_counter.py b/autogpt/token_counter.py new file mode 100644 index 0000000..2d50547 --- /dev/null +++ b/autogpt/token_counter.py @@ -0,0 +1,76 @@ +"""Functions for counting the number of tokens in a message or string.""" +from __future__ import annotations + +from typing import List + +import tiktoken + +from autogpt.logs import logger +from autogpt.types.openai import Message + + +def count_message_tokens( + messages: List[Message], model: str = "gpt-3.5-turbo-0301" +) -> int: + """ + Returns the number of tokens used by a list of messages. + + Args: + messages (list): A list of messages, each of which is a dictionary + containing the role and content of the message. + model (str): The name of the model to use for tokenization. + Defaults to "gpt-3.5-turbo-0301". + + Returns: + int: The number of tokens used by the list of messages. + """ + try: + encoding = tiktoken.encoding_for_model(model) + except KeyError: + logger.warn("Warning: model not found. Using cl100k_base encoding.") + encoding = tiktoken.get_encoding("cl100k_base") + if model == "gpt-3.5-turbo": + # !Note: gpt-3.5-turbo may change over time. + # Returning num tokens assuming gpt-3.5-turbo-0301.") + return count_message_tokens(messages, model="gpt-3.5-turbo-0301") + elif model == "gpt-4": + # !Note: gpt-4 may change over time. Returning num tokens assuming gpt-4-0314.") + return count_message_tokens(messages, model="gpt-4-0314") + elif model == "gpt-3.5-turbo-0301": + tokens_per_message = ( + 4 # every message follows <|start|>{role/name}\n{content}<|end|>\n + ) + tokens_per_name = -1 # if there's a name, the role is omitted + elif model == "gpt-4-0314": + tokens_per_message = 3 + tokens_per_name = 1 + else: + raise NotImplementedError( + f"num_tokens_from_messages() is not implemented for model {model}.\n" + " See https://github.com/openai/openai-python/blob/main/chatml.md for" + " information on how messages are converted to tokens." + ) + num_tokens = 0 + for message in messages: + num_tokens += tokens_per_message + for key, value in message.items(): + num_tokens += len(encoding.encode(value)) + if key == "name": + num_tokens += tokens_per_name + num_tokens += 3 # every reply is primed with <|start|>assistant<|message|> + return num_tokens + + +def count_string_tokens(string: str, model_name: str) -> int: + """ + Returns the number of tokens in a text string. + + Args: + string (str): The text string. + model_name (str): The name of the encoding to use. (e.g., "gpt-3.5-turbo") + + Returns: + int: The number of tokens in the text string. + """ + encoding = tiktoken.encoding_for_model(model_name) + return len(encoding.encode(string)) diff --git a/autogpt/types/openai.py b/autogpt/types/openai.py new file mode 100644 index 0000000..2af8578 --- /dev/null +++ b/autogpt/types/openai.py @@ -0,0 +1,9 @@ +"""Type helpers for working with the OpenAI library""" +from typing import TypedDict + + +class Message(TypedDict): + """OpenAI Message object containing a role and the message content""" + + role: str + content: str diff --git a/autogpt/utils.py b/autogpt/utils.py new file mode 100644 index 0000000..c8553ea --- /dev/null +++ b/autogpt/utils.py @@ -0,0 +1,85 @@ +import os + +import requests +import yaml +from colorama import Fore +from git.repo import Repo + +# Use readline if available (for clean_input) +try: + import readline +except: + pass + + +def clean_input(prompt: str = ""): + try: + return input(prompt) + except KeyboardInterrupt: + print("You interrupted Auto-GPT") + print("Quitting...") + exit(0) + + +def validate_yaml_file(file: str): + try: + with open(file, encoding="utf-8") as fp: + yaml.load(fp.read(), Loader=yaml.FullLoader) + except FileNotFoundError: + return (False, f"The file {Fore.CYAN}`{file}`{Fore.RESET} wasn't found") + except yaml.YAMLError as e: + return ( + False, + f"There was an issue while trying to read with your AI Settings file: {e}", + ) + + return (True, f"Successfully validated {Fore.CYAN}`{file}`{Fore.RESET}!") + + +def readable_file_size(size, decimal_places=2): + """Converts the given size in bytes to a readable format. + Args: + size: Size in bytes + decimal_places (int): Number of decimal places to display + """ + for unit in ["B", "KB", "MB", "GB", "TB"]: + if size < 1024.0: + break + size /= 1024.0 + return f"{size:.{decimal_places}f} {unit}" + + +def get_bulletin_from_web(): + try: + response = requests.get( + "https://raw.githubusercontent.com/Significant-Gravitas/Auto-GPT/master/BULLETIN.md" + ) + if response.status_code == 200: + return response.text + except requests.exceptions.RequestException: + pass + + return "" + + +def get_current_git_branch() -> str: + try: + repo = Repo(search_parent_directories=True) + branch = repo.active_branch + return branch.name + except: + return "" + + +def get_latest_bulletin() -> str: + exists = os.path.exists("CURRENT_BULLETIN.md") + current_bulletin = "" + if exists: + current_bulletin = open("CURRENT_BULLETIN.md", "r", encoding="utf-8").read() + new_bulletin = get_bulletin_from_web() + is_new_news = new_bulletin != current_bulletin + + if new_bulletin and is_new_news: + open("CURRENT_BULLETIN.md", "w", encoding="utf-8").write(new_bulletin) + return f" {Fore.RED}::UPDATED:: {Fore.CYAN}{new_bulletin}{Fore.RESET}" + return current_bulletin diff --git a/autogpt/workspace/__init__.py b/autogpt/workspace/__init__.py new file mode 100644 index 0000000..b348144 --- /dev/null +++ b/autogpt/workspace/__init__.py @@ -0,0 +1,5 @@ +from autogpt.workspace.workspace import Workspace + +__all__ = [ + "Workspace", +] diff --git a/autogpt/workspace/workspace.py b/autogpt/workspace/workspace.py new file mode 100644 index 0000000..b06fa9e --- /dev/null +++ b/autogpt/workspace/workspace.py @@ -0,0 +1,120 @@ +""" +========= +Workspace +========= + +The workspace is a directory containing configuration and working files for an AutoGPT +agent. + +""" +from __future__ import annotations + +from pathlib import Path + + +class Workspace: + """A class that represents a workspace for an AutoGPT agent.""" + + def __init__(self, workspace_root: str | Path, restrict_to_workspace: bool): + self._root = self._sanitize_path(workspace_root) + self._restrict_to_workspace = restrict_to_workspace + + @property + def root(self) -> Path: + """The root directory of the workspace.""" + return self._root + + @property + def restrict_to_workspace(self): + """Whether to restrict generated paths to the workspace.""" + return self._restrict_to_workspace + + @classmethod + def make_workspace(cls, workspace_directory: str | Path, *args, **kwargs) -> Path: + """Create a workspace directory and return the path to it. + + Parameters + ---------- + workspace_directory + The path to the workspace directory. + + Returns + ------- + Path + The path to the workspace directory. + + """ + # TODO: have this make the env file and ai settings file in the directory. + workspace_directory = cls._sanitize_path(workspace_directory) + workspace_directory.mkdir(exist_ok=True, parents=True) + return workspace_directory + + def get_path(self, relative_path: str | Path) -> Path: + """Get the full path for an item in the workspace. + + Parameters + ---------- + relative_path + The relative path to resolve in the workspace. + + Returns + ------- + Path + The resolved path relative to the workspace. + + """ + return self._sanitize_path( + relative_path, + root=self.root, + restrict_to_root=self.restrict_to_workspace, + ) + + @staticmethod + def _sanitize_path( + relative_path: str | Path, + root: str | Path = None, + restrict_to_root: bool = True, + ) -> Path: + """Resolve the relative path within the given root if possible. + + Parameters + ---------- + relative_path + The relative path to resolve. + root + The root path to resolve the relative path within. + restrict_to_root + Whether to restrict the path to the root. + + Returns + ------- + Path + The resolved path. + + Raises + ------ + ValueError + If the path is absolute and a root is provided. + ValueError + If the path is outside the root and the root is restricted. + + """ + + if root is None: + return Path(relative_path).resolve() + + root, relative_path = Path(root), Path(relative_path) + + if relative_path.is_absolute(): + raise ValueError( + f"Attempted to access absolute path '{relative_path}' in workspace '{root}'." + ) + + full_path = root.joinpath(relative_path).resolve() + + if restrict_to_root and not full_path.is_relative_to(root): + raise ValueError( + f"Attempted to access path '{full_path}' outside of workspace '{root}'." + ) + + return full_path diff --git a/crazy_functions/辅助回答.py b/crazy_functions/辅助回答.py index fe3eb3e..4010eab 100644 --- a/crazy_functions/辅助回答.py +++ b/crazy_functions/辅助回答.py @@ -14,8 +14,11 @@ def 猜你想问(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt show_say = txt prompt = txt+'\n回答完问题后,再列出用户可能提出的三个问题。' else: - prompt = history[-1]+"\n分析上述回答,再列出用户可能提出的三个问题。" show_say = '分析上述回答,再列出用户可能提出的三个问题。' + try: + prompt = history[-1]+f"\n{show_say}" + except IndexError: + prompt = system_prompt+"\n再列出用户可能提出的三个问题。" gpt_say = yield from request_gpt_model_in_new_thread_with_ui_alive( inputs=prompt, inputs_show_user=show_say, @@ -24,6 +27,5 @@ def 猜你想问(txt, llm_kwargs, plugin_kwargs, chatbot, history, system_prompt history=history, sys_prompt=system_prompt ) - chatbot[-1] = (show_say, gpt_say) - history.extend([show_say, gpt_say]) - yield from update_ui(chatbot=chatbot, history=history) # 刷新界面 \ No newline at end of file + chatbot.append([show_say, gpt_say]) + history.extend([show_say, gpt_say]) \ No newline at end of file diff --git a/func_box.py b/func_box.py index e14cb8d..7c58122 100644 --- a/func_box.py +++ b/func_box.py @@ -123,6 +123,7 @@ def ipaddr(): # 获取本地ipx return ip[i][0][1] def encryption_str(txt: str): + txt = str(txt) """(关键字)(加密间隔)匹配机制(关键字间隔)""" pattern = re.compile(rf"(Authorization|WPS-Sid|Cookie)(:|\s+)\s*(\S+)[\s\S]*?(?=\n|$|\s)", re.IGNORECASE) result = pattern.sub(lambda x: x.group(1) + ": XXXXXXXX", txt) @@ -142,12 +143,26 @@ def tree_out(dir=os.path.dirname(__file__), line=2, more=''): f.write('```\n') +def chat_history(log: list, split=0): + if split: + log = log[split:] + chat = '' + history = '' + for i in log: + chat += f'{i[0]}\n\n' + history += f'{i[1]}\n\n' + return chat, history + + + if __name__ == '__main__': txt = "Authorization: WPS-2:AqY7ik9XQ92tvO7+NlCRvA==:b2f626f496de9c256605a15985c855a8b3e4be99\nwps-Sid: V02SgISzdeWrYdwvW_xbib-fGlqUIIw00afc5b890008c1976f\nCookie: wpsua=V1BTVUEvMS4wIChhbmRyb2lkLW9mZmljZToxNy41O2FuZHJvaWQ6MTA7ZjIwZDAyNWQzYTM5MmExMDBiYzgxNWI2NmI3Y2E5ODI6ZG1sMmJ5QldNakF5TUVFPSl2aXZvL1YyMDIwQQ==" txt = "Authorization: WPS-2:AqY7ik9XQ92tvO7+NlCRvA==:b2f626f496de9c256605a15985c855a8b3e4be99" print(encryption_str(txt)) - tree_out() - + def update_ui(chatbot, history, msg='正常', txt='', obj=None, btn1=None, btn2=None, au_text=None, *args): + print(chatbot, history, msg, txt, obj) + ll = [4,5, 6] + update_ui(chatbot=1, history=2, *ll) \ No newline at end of file diff --git a/test.py b/test.py index 9534a6e..f321c18 100644 --- a/test.py +++ b/test.py @@ -3,69 +3,56 @@ # @Time : 2023/4/19 # @Author : Spike # @Descr : - - import gradio as gr -def sentence_builder(quantity, xixi): - return f"{quantity}_{xixi}" + +class my_class(): + + def __init__(self): + self.numb = 0 + + def coun_up(self): + self.numb += 1 -with gr.Blocks() as demo: +def set_obj(sts): + btn = sts['btn'].update(visible=False) + btn2 = sts['btn2'].update(visible=True) + sts['obj'] = my_class() + return sts, btn, btn2 - txt = gr.Textbox(label="Input", lines=2) - txt_2 = gr.CheckboxGroup(['USA', "Japan"], value=['USA'], label='你好呀') - txt_3 = gr.Textbox(value="", label="Output") - btn = gr.Button(value="Submit") - btn.click(sentence_builder, inputs=[txt, txt_2], outputs=[txt_3]) -class ChatGPTForTester: +def print_obj(sts): + print(sts) + print(sts['btn'], type(sts['btn'])) + sts['obj'].coun_up() + print(sts['obj'].numb) +class ChatBotFrame: + + def __init__(self): + self.cancel_handles = [] + self.initial_prompt = "Serve me as a writing and programming assistant." + self.title_html = f"

ChatGPT For Tester" + self.description = """代码开源和更新[地址🚀](https://github.com/binary-husky/chatgpt_academic),感谢热情的[开发者们❤️](https://github.com/binary-husky/chatgpt_academic/graphs/contributors)""" + + +class ChatBot(): def __init__(self): self.demo = gr.Blocks() - def book(self): + def draw_test(self): with self.demo: - txt = gr.Textbox(label="Input", lines=2) - txt_2 = gr.CheckboxGroup(['USA', "Japan"], value=['USA'], label='你好呀') - txt_3 = gr.Textbox(value="", label="Output") - btn = gr.Button(value="Submit") - btn.click(sentence_builder, inputs=[txt, txt_2], outputs=[txt_3]) - - def book2(self): - with self.demo: - txt = gr.Textbox(label="Input", lines=2) - txt_2 = gr.CheckboxGroup(['USA', "Japan"], value=['USA'], label='我好呀') - txt_3 = gr.Textbox(value="", label="Output") - btn = gr.Button(value="Submit") - btn.click(sentence_builder, inputs=[txt, txt_2], outputs=[txt_3]) - - def main(self): - self.book2() - self.book() + # self.temp = gr.Markdown('') + self.txt = gr.Textbox(label="Input", lines=2) + self.btn = gr.Button(value="Submit1") + self.btn2 = gr.Button(value="Submit2", visible=False) + self.obj = gr.State({'obj': None, 'btn': self.btn, 'btn2': self.btn2}) + self.btn.click(set_obj, inputs=[self.obj], outputs=[self.obj, self.btn, self.btn2]) + self.btn2.click(print_obj, inputs=[self.obj], outputs=[self.txt]) self.demo.launch() - - -class MyClass: - - def __init__(self): - self.my_attribute1 = '' - - def __getattribute__(self, name): - try: - return object.__getattribute__(self, name) - except AttributeError: - return [] - - def my_method(self): - self.test = '12312312312' - print("This is my method.") - -if __name__ == "__main__": - __url = gr.State(f'https://') - print(__url) - - +if __name__ == '__main__': + ChatBot().draw_test() diff --git a/toolbox.py b/toolbox.py index 1ee2be2..16eba28 100644 --- a/toolbox.py +++ b/toolbox.py @@ -54,20 +54,22 @@ def ArgsGeneralWrapper(f): chatbot_with_cookie.write_list(chatbot) txt_passon = txt if 'input加密' in models: txt_passon = func_box.encryption_str(txt) - if txt_passon == '' and len(args) > 1: + if txt_passon == '' and txt_passon == ' ' and len(args) > 1: msgs = f'### {args[1]} Warning 输入框为空\n' \ - 'tips: 使用基础功能时,请在输入栏内输入需要处理的文本内容' + 'tips: 使用基础功能时,请在输入区输入需要处理的文本内容' yield from update_ui(chatbot=chatbot_with_cookie, history=history, msg=msgs) # 刷新界面 return yield from f(txt_passon, llm_kwargs, plugin_kwargs, chatbot_with_cookie, history, system_prompt, *args) return decorated -def update_ui(chatbot, history, msg='正常', **kwargs): # 刷新界面 + +def update_ui(chatbot, history, msg='正常', txt=' ', *args): # 刷新界面 """ 刷新用户界面 """ + assert isinstance(chatbot, ChatBotWithCookies), "在传递chatbot的过程中不要将其丢弃。必要时,可用clear将其清空,然后用for+append循环重新赋值。" - yield chatbot.get_cookies(), chatbot, history, msg + yield chatbot.get_cookies(), chatbot, history, msg, txt def CatchException(f): """ @@ -419,7 +421,7 @@ def get_user_download(chatbot, link, file): '[Local Message] Cannot convert directory to download link, please try again.']) elif file_handle == '': pass - return chatbot, file + return chatbot, '' def on_file_uploaded(files, chatbot, txt, ipaddr: gr.Request): @@ -592,4 +594,8 @@ def run_gradio_in_subpath(demo, auth, port, custom_path): def read_main(): return {"message": f"Gradio is running at: {custom_path}"} app = gr.mount_gradio_app(app, demo, path=custom_path) - uvicorn.run(app, host="0.0.0.0", port=port) # , auth=auth \ No newline at end of file + uvicorn.run(app, host="0.0.0.0", port=port) # , auth=auth + + +if __name__ == '__main__': + print(ChatBotWithCookies()) \ No newline at end of file