diff --git a/agixt/extensions/README.md b/agixt/extensions/README.md index 537dca9e7188..07891112e62b 100644 --- a/agixt/extensions/README.md +++ b/agixt/extensions/README.md @@ -1,27 +1,32 @@ -## AGiXT Extensions +# AGiXT Extensions Extensions are the way to extend the functionality of AGiXT with other software APIs or Python modules. Extensions are Python files that are placed in the `extensions` folder. AGiXT will automatically load all extensions and commands associated when it starts. -### Creating an Extension +## Creating an Extension -To create an extension, create a new Python file in the `extensions` folder. The file name will be the name of the extension. We will use the `GitHub` extension as an example. +To create an extension, create a new Python file in the `extensions` folder. The file name will be the name of the extension. We will make a smaller version of our `GitHub` extension as an example. + +The example `GitHub` extension will have two commands: -The `GitHub` extension will have two commands: - `Clone GitHub Repository` - `Create GitHub Repository` The `GibHub` extension will require two keys: + - `GITHUB_USERNAME` - The username of the GitHub account to use - `GITHUB_API_KEY` - The API key of the GitHub account to use All extensions will inherit from the `Extensions` class. This class will provide the following: + - `self.commands` - A dictionary of commands that the extension provides. The key is the name of the command and the value is the function that will be called when the command is run. -- `WORKING_DIRECTORY` - The directory that AGiXT Agent is allowed to work in. - Any API Keys for your extension. In this case, `GITHUB_USERNAME` and `GITHUB_API_KEY`. Add these to `__init__` as shown below. -- Any imports that are required for your extension. In this case, `os`, `git`, and `Github`. Add try/excepts to install the modules if they are not already installed. +- Any imports that are required for your extension that are not built into Python. In this case, `GitPython`, and `PyGithub`. Add try/excepts to install the modules if they are not already installed. ```python import os +import time +import datetime +from Extensions import Extensions try: import git @@ -29,18 +34,17 @@ except ImportError: import sys import subprocess - subprocess.check_call([sys.executable, "-m", "pip", "install", "GitPython==3.1.31"]) + subprocess.check_call([sys.executable, "-m", "pip", "install", "GitPython"]) import git try: - from github import Github + from github import Github, RateLimitExceededException except ImportError: import sys import subprocess - subprocess.check_call([sys.executable, "-m", "pip", "install", "PyGithub==1.58.2"]) - from github import Github -from Extensions import Extensions + subprocess.check_call([sys.executable, "-m", "pip", "install", "PyGithub"]) + from github import Github, RateLimitExceededException class github(Extensions): @@ -48,17 +52,36 @@ class github(Extensions): self, GITHUB_USERNAME: str = "", GITHUB_API_KEY: str = "", - WORKING_DIRECTORY: str = "./WORKSPACE", **kwargs, ): self.GITHUB_USERNAME = GITHUB_USERNAME self.GITHUB_API_KEY = GITHUB_API_KEY - self.WORKING_DIRECTORY = WORKING_DIRECTORY - self.commands = {"Clone Github Repository": self.clone_repo} if self.GITHUB_USERNAME and self.GITHUB_API_KEY: - self.commands["Create Github Repository"] = self.create_repo + self.commands = { + "Clone Github Repository": self.clone_repo, + "Create Github Repository": self.create_repo, + } + try: + self.gh = Github(self.GITHUB_API_KEY) + except Exception as e: + self.gh = None + self.commands = {} + print(f"GitHub Error: {str(e)}") + else: + self.commands = {} + self.gh = None + self.failures = 0 async def clone_repo(self, repo_url: str) -> str: + """ + Clone a GitHub repository to the local workspace + + Args: + repo_url (str): The URL of the GitHub repository to clone + + Returns: + str: The result of the cloning operation + """ split_url = repo_url.split("//") if self.GITHUB_USERNAME is not None and self.GITHUB_API_KEY is not None: auth_repo_url = f"//{self.GITHUB_USERNAME}:{self.GITHUB_API_KEY}@".join( @@ -68,29 +91,58 @@ class github(Extensions): auth_repo_url = "//".join(split_url) try: repo_name = repo_url.split("/")[-1] - repo_dir = os.path.join(self.WORKING_DIRECTORY, repo_name) + repo_dir = os.path.join("./WORKSPACE", repo_name) if os.path.exists(repo_dir): - return f"""{repo_dir} already exists""" + # Pull the latest changes + repo = git.Repo(repo_dir) + repo.remotes.origin.pull() + self.failures = 0 + return f"Pulled latest changes for {repo_url} to {repo_dir}" git.Repo.clone_from( url=auth_repo_url, to_path=repo_dir, ) + self.failures = 0 return f"Cloned {repo_url} to {repo_dir}" except Exception as e: + if self.failures < 3: + self.failures += 1 + time.sleep(5) + return await self.clone_repo(repo_url) return f"Error: {str(e)}" async def create_repo(self, repo_name: str, content_of_readme: str) -> str: - g = Github(self.GITHUB_API_KEY) - user = g.get_user(self.GITHUB_USERNAME) - repo = user.create_repo(repo_name, private=True) - repo_url = repo.clone_url - repo_dir = f"./{repo_name}" - repo = git.Repo.init(repo_dir) - with open(f"{repo_dir}/README.md", "w") as f: - f.write(content_of_readme) - repo.git.add(A=True) - repo.git.commit(m="Added README") - repo.create_remote("origin", repo_url) - repo.git.push("origin", "HEAD:main") - return repo_url -``` \ No newline at end of file + """ + Create a new private GitHub repository + + Args: + repo_name (str): The name of the repository to create + content_of_readme (str): The content of the README.md file + + Returns: + str: The URL of the newly created repository + """ + try: + try: + user = self.gh.get_organization(self.GITHUB_USERNAME) + except: + user = self.gh.get_user(self.GITHUB_USERNAME) + repo = user.create_repo(repo_name, private=True) + repo_url = repo.clone_url + repo_dir = f"./{repo_name}" + repo = git.Repo.init(repo_dir) + with open(f"{repo_dir}/README.md", "w") as f: + f.write(content_of_readme) + repo.git.add(A=True) + repo.git.commit(m="Added README") + repo.create_remote("origin", repo_url) + repo.git.push("origin", "HEAD:main") + self.failures = 0 + return repo_url + except Exception as e: + if self.failures < 3: + self.failures += 1 + time.sleep(5) + return await self.create_repo(repo_name, content_of_readme) + return f"Error: {str(e)}" +``` diff --git a/agixt/extensions/agixt_actions.py b/agixt/extensions/agixt_actions.py index 77ac269f3892..650b19dafb6a 100644 --- a/agixt/extensions/agixt_actions.py +++ b/agixt/extensions/agixt_actions.py @@ -182,6 +182,15 @@ def __init__(self, **kwargs): self.failures = 0 async def read_file_content(self, file_path: str): + """ + Read the content of a file and store it in long term memory + + Args: + file_path (str): The path to the file + + Returns: + str: Success message + """ with open(file_path, "r") as f: file_content = f.read() filename = os.path.basename(file_path) @@ -193,6 +202,15 @@ async def read_file_content(self, file_path: str): ) async def write_website_to_memory(self, url: str): + """ + Read the content of a website and store it in long term memory + + Args: + url (str): The URL of the website + + Returns: + str: Success message + """ return self.ApiClient.learn_url( agent_name=self.agent_name, url=url, @@ -202,6 +220,16 @@ async def write_website_to_memory(self, url: str): async def store_long_term_memory( self, input: str, data_to_correlate_with_input: str ): + """ + Store information in long term memory + + Args: + input (str): The user input + data_to_correlate_with_input (str): The data to correlate with the user input in long term memory, useful for feedback or remembering important information for later + + Returns: + str: Success message + """ return self.ApiClient.learn_text( agent_name=self.agent_name, user_input=input, @@ -209,6 +237,16 @@ async def store_long_term_memory( ) async def search_arxiv(self, query: str, max_articles: int = 5): + """ + Search for articles on arXiv, read into long term memory + + Args: + query (str): The search query + max_articles (int): The maximum number of articles to read + + Returns: + str: Success message + """ return self.ApiClient.learn_arxiv( query=query, article_ids=None, @@ -217,6 +255,15 @@ async def search_arxiv(self, query: str, max_articles: int = 5): ) async def read_github_repository(self, repository_url: str): + """ + Read the content of a GitHub repository and store it in long term memory + + Args: + repository_url (str): The URL of the GitHub repository + + Returns: + str: Success message + """ return self.ApiClient.learn_github_repo( agent_name=self.agent_name, github_repo=repository_url, @@ -233,6 +280,20 @@ async def create_task_chain( smart_chain: bool = False, researching: bool = False, ): + """ + Create a task chain from a numbered list of tasks + + Args: + agent (str): The agent to create the task chain for + primary_objective (str): The primary objective to keep in mind while working on the task + numbered_list_of_tasks (str): The numbered list of tasks to complete + short_chain_description (str): A short description of the chain + smart_chain (bool): Whether to create a smart chain + researching (bool): Whether to include web research in the chain + + Returns: + str: The name of the created chain + """ now = datetime.datetime.now() timestamp = now.strftime("%Y-%m-%d_%H-%M-%S") task_list = numbered_list_of_tasks.split("\n") @@ -309,6 +370,15 @@ async def create_task_chain( return chain_name async def run_chain(self, input_for_task: str = ""): + """ + Run a chain + + Args: + input_for_task (str): The input for the task + + Returns: + str: The response from the chain + """ response = await self.ApiClient.run_chain( chain_name=self.command_name, user_input=input_for_task, @@ -322,6 +392,15 @@ async def run_chain(self, input_for_task: str = ""): return response def parse_openapi(self, data): + """ + Parse OpenAPI data to extract endpoints + + Args: + data (dict): The OpenAPI data + + Returns: + list: The list of endpoints + """ endpoints = [] schemas = data.get("components", {}).get( "schemas", {} @@ -382,6 +461,15 @@ def resolve_schema(ref): return endpoints def get_auth_type(self, openapi_data): + """ + Get the authentication type from the OpenAPI data + + Args: + openapi_data (dict): The OpenAPI data + + Returns: + str: The authentication type + """ # The "components" section contains the security schemes if ( "components" in openapi_data @@ -399,6 +487,16 @@ def get_auth_type(self, openapi_data): return "basic" async def generate_openapi_chain(self, extension_name: str, openapi_json_url: str): + """ + Generate an AGiXT extension from an OpenAPI JSON URL + + Args: + extension_name (str): The name of the extension + openapi_json_url (str): The URL of the OpenAPI JSON file + + Returns: + str: The name of the created chain + """ # Experimental currently. openapi_str = requests.get(openapi_json_url).text openapi_data = json.loads(openapi_str) @@ -499,6 +597,17 @@ async def generate_openapi_chain(self, extension_name: str, openapi_json_url: st return chain_name async def generate_helper_chain(self, user_agent, helper_agent, task_in_question): + """ + Generate a helper chain for an agent + + Args: + user_agent (str): The user agent + helper_agent (str): The helper agent + task_in_question (str): The task in question + + Returns: + str: The name of the created chain + """ chain_name = f"Help Chain - {user_agent} to {helper_agent}" self.ApiClient.add_chain(chain_name=chain_name) i = 1 @@ -528,6 +637,16 @@ async def generate_helper_chain(self, user_agent, helper_agent, task_in_question return chain_name async def ask_for_help(self, your_agent_name, your_task): + """ + Ask for help from a helper agent + + Args: + your_agent_name (str): Your agent name + your_task (str): Your task + + Returns: + str: The response from the helper agent + """ return self.ApiClient.run_chain( chain_name="Ask Helper Agent for Help", user_input=your_task, @@ -542,6 +661,16 @@ async def ask_for_help(self, your_agent_name, your_task): async def create_command( self, function_description: str, agent: str = "AGiXT" ) -> List[str]: + """ + Create a new command + + Args: + function_description (str): The description of the function + agent (str): The agent to create the command for + + Returns: + str: The response from the chain + """ try: return self.ApiClient.run_chain( chain_name="Create New Command", @@ -557,6 +686,15 @@ async def create_command( return f"Unable to create command: {e}" async def ask(self, user_input: str) -> str: + """ + Ask a question + + Args: + user_input (str): The user input + + Returns: + str: The response to the question + """ response = self.ApiClient.prompt_agent( agent_name=self.agent_name, prompt_name="Chat", @@ -570,6 +708,15 @@ async def ask(self, user_input: str) -> str: return response async def instruct(self, user_input: str) -> str: + """ + Instruct the agent + + Args: + user_input (str): The user input + + Returns: + str: The response to the instruction + """ response = self.ApiClient.prompt_agent( agent_name=self.agent_name, prompt_name="instruct", @@ -583,11 +730,30 @@ async def instruct(self, user_input: str) -> str: return response async def get_python_code_from_response(self, response: str): + """ + Get the Python code from the response + + Args: + response (str): The response + + Returns: + str: The Python code + """ if "```python" in response: response = response.split("```python")[1].split("```")[0] return response async def execute_python_code_internal(self, code: str, text: str = "") -> str: + """ + Execute Python code + + Args: + code (str): The Python code + text (str): The text + + Returns: + str: The result of the Python code + """ working_dir = os.environ.get("WORKING_DIRECTORY", self.WORKING_DIRECTORY) if text: csv_content_header = text.split("\n")[0] @@ -602,6 +768,15 @@ async def execute_python_code_internal(self, code: str, text: str = "") -> str: return execute_python_code(code=code, working_directory=working_dir) async def get_mindmap(self, task: str): + """ + Get a mindmap for a task + + Args: + task (str): The task + + Returns: + dict: The mindmap + """ mindmap = self.ApiClient.prompt_agent( agent_name=self.agent_name, prompt_name="Mindmap", @@ -613,10 +788,27 @@ async def get_mindmap(self, task: str): return parse_mindmap(mindmap=mindmap) async def make_csv_code_block(self, data: str) -> str: + """ + Make a CSV code block + + Args: + data (str): The data + + Returns: + str: The CSV code block + """ return f"```csv\n{data}\n```" async def get_csv_preview(self, filename: str): - # Get first 2 lines of the file + """ + Get a preview of a CSV file + + Args: + filename (str): The filename + + Returns: + str: The preview of the CSV file consisting of the first 2 lines + """ filepath = self.safe_join(base=self.WORKING_DIRECTORY, paths=filename) with open(filepath, "r") as f: lines = f.readlines() @@ -625,17 +817,42 @@ async def get_csv_preview(self, filename: str): return lines_string async def get_csv_preview_text(self, text: str): - # Get first 2 lines of the text + """ + Get a preview of a CSV text + + Args: + text (str): The text + + Returns: + str: The preview of the CSV text consisting of the first 2 lines + """ lines = text.split("\n") lines = lines[:2] lines_string = "\n".join(lines) return lines_string async def get_csv_from_response(self, response: str) -> str: + """ + Get the CSV data from the response + + Args: + response (str): The response + + Returns: + str: The CSV data + """ return response.split("```csv")[1].split("```")[0] - # Convert LLM response of a list of either numbers like a numbered list, *'s, -'s to a list from the string response async def convert_llm_response_to_list(self, response): + """ + Convert an LLM response to a list + + Args: + response (str): The response + + Returns: + list: The list + """ response = response.split("\n") response = [item.lstrip("0123456789.*- ") for item in response if item.lstrip()] response = [item for item in response if item] @@ -643,6 +860,15 @@ async def convert_llm_response_to_list(self, response): return response async def convert_questions_to_dataset(self, response): + """ + Convert questions to a dataset + + Args: + response (str): The response + + Returns: + str: The dataset + """ questions = await self.convert_llm_response_to_list(response) tasks = [] i = 0 @@ -667,6 +893,16 @@ async def convert_questions_to_dataset(self, response): async def convert_string_to_pydantic_model( self, input_string: str, output_model: Type[BaseModel] ): + """ + Convert a string to a Pydantic model + + Args: + input_string (str): The input string + output_model (Type[BaseModel]): The output model + + Returns: + Type[BaseModel]: The Pydantic model + """ fields = output_model.model_fields field_descriptions = [f"{field}: {fields[field]}" for field in fields] schema = "\n".join(field_descriptions) diff --git a/agixt/extensions/discord.py b/agixt/extensions/discord.py index db48f3ffe653..7a677f40e6e2 100644 --- a/agixt/extensions/discord.py +++ b/agixt/extensions/discord.py @@ -53,28 +53,90 @@ async def on_command_error(ctx, error): pass async def send_message(self, channel_id: int, content: str): + """ + Send a message to a Discord channel + + Args: + channel_id (int): The ID of the Discord channel + content (str): The content of the message + + Returns: + str: The result of sending the message + """ channel = self.bot.get_channel(channel_id) await channel.send(content) + return f"Message sent to channel {channel_id} successfully!" async def get_messages(self, channel_id: int, limit: int = 100): + """ + Get messages from a Discord channel + + Args: + channel_id (int): The ID of the Discord channel + limit (int): The number of messages to retrieve + + Returns: + str: The messages from the channel + """ channel = self.bot.get_channel(channel_id) messages = await channel.history(limit=limit).flatten() - return [(message.author, message.content) for message in messages] + str_messages = "" + for message in messages: + str_messages += f"{message.author}: {message.content}\n" + return str_messages async def delete_message(self, channel_id: int, message_id: int): + """ + Delete a message from a Discord channel + + Args: + channel_id (int): The ID of the Discord channel + message_id (int): The ID of the message to delete + + Returns: + str: The result of deleting the message + """ channel = self.bot.get_channel(channel_id) message = await channel.fetch_message(message_id) await message.delete() + return f"Message {message_id} deleted successfully!" async def create_invite(self, channel_id: int, max_age: int = 0, max_uses: int = 0): + """ + Create an invite to a Discord channel + + Args: + channel_id (int): The ID of the Discord channel + max_age (int): The maximum age of the invite in seconds + max_uses (int): The maximum number of uses for the invite + + Returns: + str: The invite URL + """ channel = self.bot.get_channel(channel_id) invite = await channel.create_invite(max_age=max_age, max_uses=max_uses) return invite.url async def get_servers(self): - return [guild.name for guild in self.bot.guilds] + """ + Get the list of servers the bot is connected to + + Returns: + str: The list of servers + """ + servers = [guild.name for guild in self.bot.guilds] + return ", ".join(servers) async def get_server_info(self, server_id: int): + """ + Get information about a Discord server + + Args: + server_id (int): The ID of the Discord server + + Returns: + dict: The information about the server + """ server = self.bot.get_guild(server_id) return { "name": server.name, diff --git a/agixt/extensions/file_system.py b/agixt/extensions/file_system.py index 4d1241a5162e..71811945345d 100644 --- a/agixt/extensions/file_system.py +++ b/agixt/extensions/file_system.py @@ -32,6 +32,15 @@ def __init__( self.WORKING_DIRECTORY = WORKING_DIRECTORY async def execute_python_file(self, file: str): + """ + Execute a Python file in the workspace + + Args: + file (str): The name of the Python file to execute + + Returns: + str: The output of the Python file + """ logging.info(f"Executing file '{file}' in workspace '{self.WORKING_DIRECTORY}'") if not file.endswith(".py"): @@ -55,6 +64,15 @@ async def execute_python_file(self, file: str): return execute_python_code(code=code, working_directory=self.WORKING_DIRECTORY) async def execute_shell(self, command_line: str) -> str: + """ + Execute a shell command + + Args: + command_line (str): The shell command to execute + + Returns: + str: The output of the shell command + """ current_dir = os.getcwd() os.chdir(current_dir) logging.info( @@ -72,6 +90,16 @@ def we_are_running_in_a_docker_container() -> bool: return os.path.exists("/.dockerenv") def safe_join(self, base: str, paths) -> str: + """ + Safely join paths together + + Args: + base (str): The base path + paths (str): The paths to join + + Returns: + str: The joined path + """ if "/path/to/" in paths: paths = paths.replace("/path/to/", "") if str(self.WORKING_DIRECTORY_RESTRICTED).lower() == "true": @@ -86,6 +114,15 @@ def safe_join(self, base: str, paths) -> str: return new_path async def read_file(self, filename: str) -> str: + """ + Read a file in the workspace + + Args: + filename (str): The name of the file to read + + Returns: + str: The content of the file + """ try: filepath = self.safe_join(base=self.WORKING_DIRECTORY, paths=filename) with open(filepath, "r", encoding="utf-8") as f: @@ -95,6 +132,16 @@ async def read_file(self, filename: str) -> str: return f"Error: {str(e)}" async def write_to_file(self, filename: str, text: str) -> str: + """ + Write text to a file in the workspace + + Args: + filename (str): The name of the file to write to + text (str): The text to write to the file + + Returns: + str: The status of the write operation + """ try: filepath = self.safe_join(base=self.WORKING_DIRECTORY, paths=filename) directory = os.path.dirname(filepath) @@ -107,6 +154,16 @@ async def write_to_file(self, filename: str, text: str) -> str: return f"Error: {str(e)}" async def append_to_file(self, filename: str, text: str) -> str: + """ + Append text to a file in the workspace + + Args: + filename (str): The name of the file to append to + text (str): The text to append to the file + + Returns: + str: The status of the append operation + """ try: filepath = self.safe_join(base=self.WORKING_DIRECTORY, paths=filename) if not os.path.exists(filepath): @@ -120,6 +177,15 @@ async def append_to_file(self, filename: str, text: str) -> str: return f"Error: {str(e)}" async def delete_file(self, filename: str) -> str: + """ + Delete a file in the workspace + + Args: + filename (str): The name of the file to delete + + Returns: + str: The status of the delete operation + """ try: filepath = self.safe_join(base=self.WORKING_DIRECTORY, paths=filename) os.remove(filepath) @@ -128,6 +194,15 @@ async def delete_file(self, filename: str) -> str: return f"Error: {str(e)}" async def search_files(self, directory: str) -> List[str]: + """ + Search for files in the workspace + + Args: + directory (str): The directory to search in + + Returns: + List[str]: The list of files found + """ found_files = [] if directory in {"", "/"}: @@ -149,6 +224,16 @@ async def search_files(self, directory: str) -> List[str]: return found_files async def indent_string(self, string: str, indents: int = 1): + """ + Indent a string for Python code + + Args: + string (str): The string to indent + indents (int): The number of indents to add + + Returns: + str: The indented string + """ if indents == 1: indent = " " else: @@ -159,6 +244,15 @@ async def indent_string(self, string: str, indents: int = 1): return indented_string async def generate_commands_dict(self, python_file_content): + """ + Generate a dictionary of commands from a Python file + + Args: + python_file_content (str): The content of the Python file + + Returns: + str: The dictionary of commands + """ function_names = re.findall(r"async def (.*?)\(", python_file_content) commands_dict = { f_name.replace("_", " "): f"self.{f_name}" for f_name in function_names diff --git a/agixt/extensions/github.py b/agixt/extensions/github.py index 5be37990932f..cd7f7280b793 100644 --- a/agixt/extensions/github.py +++ b/agixt/extensions/github.py @@ -62,6 +62,15 @@ def __init__( self.failures = 0 async def clone_repo(self, repo_url: str) -> str: + """ + Clone a GitHub repository to the local workspace + + Args: + repo_url (str): The URL of the GitHub repository to clone + + Returns: + str: The result of the cloning operation + """ split_url = repo_url.split("//") if self.GITHUB_USERNAME is not None and self.GITHUB_API_KEY is not None: auth_repo_url = f"//{self.GITHUB_USERNAME}:{self.GITHUB_API_KEY}@".join( @@ -92,6 +101,16 @@ async def clone_repo(self, repo_url: str) -> str: return f"Error: {str(e)}" async def create_repo(self, repo_name: str, content_of_readme: str) -> str: + """ + Create a new private GitHub repository + + Args: + repo_name (str): The name of the repository to create + content_of_readme (str): The content of the README.md file + + Returns: + str: The URL of the newly created repository + """ try: try: user = self.gh.get_organization(self.GITHUB_USERNAME) @@ -117,6 +136,15 @@ async def create_repo(self, repo_name: str, content_of_readme: str) -> str: return f"Error: {str(e)}" async def get_repo_code_contents(self, repo_url: str) -> str: + """ + Get the code contents of a GitHub repository + + Args: + repo_url (str): The URL of the GitHub repository + + Returns: + str: The code contents of the repository in markdown format + """ repo_name = repo_url.split("/")[-1] await self.clone_repo(repo_url) python_files = [] @@ -177,6 +205,15 @@ async def get_repo_code_contents(self, repo_url: str) -> str: return content async def get_repo_issues(self, repo_url: str) -> str: + """ + Get the open issues for a GitHub repository + + Args: + repo_url (str): The URL of the GitHub repository + + Returns: + str: The open issues for the repository + """ try: repo = self.gh.get_repo(repo_url.split("github.com/")[-1]) issues = repo.get_issues(state="open") @@ -197,6 +234,16 @@ async def get_repo_issues(self, repo_url: str) -> str: return f"Error: {str(e)}" async def get_repo_issue(self, repo_url: str, issue_number: int) -> str: + """ + Get the details of a specific issue in a GitHub repository + + Args: + repo_url (str): The URL of the GitHub repository + issue_number (int): The issue number to retrieve + + Returns: + str: The details of the issue + """ try: repo = self.gh.get_repo(repo_url.split("github.com/")[-1]) issue = repo.get_issue(issue_number) @@ -214,6 +261,18 @@ async def get_repo_issue(self, repo_url: str, issue_number: int) -> str: async def create_repo_issue( self, repo_url: str, title: str, body: str, assignee: str = None ) -> str: + """ + Create a new issue in a GitHub repository + + Args: + repo_url (str): The URL of the GitHub repository + title (str): The title of the issue + body (str): The body of the issue + assignee (str): The assignee for the issue + + Returns: + str: The result of the issue creation operation + """ try: repo = self.gh.get_repo(repo_url.split("github.com/")[-1]) issue = repo.create_issue(title=title, body=body, assignee=assignee) @@ -238,6 +297,19 @@ async def update_repo_issue( body: str, assignee: str = None, ) -> str: + """ + Update an existing issue in a GitHub repository + + Args: + repo_url (str): The URL of the GitHub repository + issue_number (int): The issue number to update + title (str): The new title of the issue + body (str): The new body of the issue + assignee (str): The new assignee for the issue + + Returns: + str: The result of the issue update operation + """ try: repo = self.gh.get_repo(repo_url.split("github.com/")[-1]) issue = repo.get_issue(issue_number) @@ -254,6 +326,15 @@ async def update_repo_issue( return f"Error: {str(e)}" async def get_repo_pull_requests(self, repo_url: str) -> str: + """ + Get the open pull requests for a GitHub repository + + Args: + repo_url (str): The URL of the GitHub repository + + Returns: + str: The open pull requests for the repository + """ try: repo = self.gh.get_repo(repo_url.split("github.com/")[-1]) pull_requests = repo.get_pulls(state="open") @@ -277,6 +358,16 @@ async def get_repo_pull_requests(self, repo_url: str) -> str: async def get_repo_pull_request( self, repo_url: str, pull_request_number: int ) -> str: + """ + Get the details of a specific pull request in a GitHub repository + + Args: + repo_url (str): The URL of the GitHub repository + pull_request_number (int): The pull request number to retrieve + + Returns: + str: The details of the pull request + """ try: repo = self.gh.get_repo(repo_url.split("github.com/")[-1]) pull_request = repo.get_pull(pull_request_number) @@ -294,6 +385,19 @@ async def get_repo_pull_request( async def create_repo_pull_request( self, repo_url: str, title: str, body: str, head: str, base: str ) -> str: + """ + Create a new pull request in a GitHub repository + + Args: + repo_url (str): The URL of the GitHub repository + title (str): The title of the pull request + body (str): The body of the pull request + head (str): The branch to merge from + base (str): The branch to merge to + + Returns: + str: The result of the pull request creation operation + """ try: repo = self.gh.get_repo(repo_url.split("github.com/")[-1]) pull_request = repo.create_pull( @@ -315,6 +419,18 @@ async def create_repo_pull_request( async def update_repo_pull_request( self, repo_url: str, pull_request_number: int, title: str, body: str ) -> str: + """ + Update an existing pull request in a GitHub repository + + Args: + repo_url (str): The URL of the GitHub repository + pull_request_number (int): The pull request number to update + title (str): The new title of the pull request + body (str): The new body of the pull request + + Returns: + str: The result of the pull request update operation + """ try: repo = self.gh.get_repo(repo_url.split("github.com/")[-1]) pull_request = repo.get_pull(pull_request_number) @@ -333,6 +449,16 @@ async def update_repo_pull_request( return f"Error: {str(e)}" async def get_repo_commits(self, repo_url: str, days: int = 7) -> str: + """ + Get the commits for a GitHub repository + + Args: + repo_url (str): The URL of the GitHub repository + days (int): The number of days to retrieve commits for (default is 7 days) + + Returns: + str: The commits for the repository + """ try: repo = self.gh.get_repo(repo_url.split("github.com/")[-1]) if days == 0: @@ -358,6 +484,16 @@ async def get_repo_commits(self, repo_url: str, days: int = 7) -> str: return f"Error: {str(e)}" async def get_repo_commit(self, repo_url: str, commit_sha: str) -> str: + """ + Get the details of a specific commit in a GitHub repository + + Args: + repo_url (str): The URL of the GitHub repository + commit_sha (str): The commit SHA to retrieve + + Returns: + str: The details of the commit + """ try: repo = self.gh.get_repo(repo_url.split("github.com/")[-1]) commit = repo.get_commit(commit_sha) @@ -371,6 +507,17 @@ async def get_repo_commit(self, repo_url: str, commit_sha: str) -> str: async def add_comment_to_repo_issue( self, repo_url: str, issue_number: int, comment_body: str ) -> str: + """ + Add a comment to an issue in a GitHub repository + + Args: + repo_url (str): The URL of the GitHub repository + issue_number (int): The issue number to add a comment to + comment_body (str): The body of the comment + + Returns: + str: The result of the comment addition operation + """ try: repo = self.gh.get_repo(repo_url.split("github.com/")[-1]) issue = repo.get_issue(issue_number) @@ -391,6 +538,17 @@ async def add_comment_to_repo_issue( async def add_comment_to_repo_pull_request( self, repo_url: str, pull_request_number: int, comment_body: str ) -> str: + """ + Add a comment to a pull request in a GitHub repository + + Args: + repo_url (str): The URL of the GitHub repository + pull_request_number (int): The pull request number to add a comment to + comment_body (str): The body of the comment + + Returns: + str: The result of the comment addition operation + """ try: repo = self.gh.get_repo(repo_url.split("github.com/")[-1]) pull_request = repo.get_pull(pull_request_number) @@ -409,6 +567,16 @@ async def add_comment_to_repo_pull_request( return f"Error: {str(e)}" async def close_issue(self, repo_url, issue_number): + """ + Close an issue in a GitHub repository + + Args: + repo_url (str): The URL of the GitHub repository + issue_number (int): The issue number to close + + Returns: + str: The result of the issue closure operation + """ try: repo = self.gh.get_repo(repo_url.split("github.com/")[-1]) issue = repo.get_issue(issue_number) diff --git a/agixt/extensions/google.py b/agixt/extensions/google.py index 855e1d808f07..6b74c8784164 100644 --- a/agixt/extensions/google.py +++ b/agixt/extensions/google.py @@ -86,6 +86,16 @@ def authenticate(self): return None async def get_emails(self, query=None, max_emails=10): + """ + Get emails from the user's Gmail account + + Args: + query (str): The search query to filter emails + max_emails (int): The maximum number of emails to retrieve + + Returns: + List[Dict]: A list of email data + """ try: service = build("gmail", "v1", credentials=self.creds) result = ( @@ -126,6 +136,18 @@ async def get_emails(self, query=None, max_emails=10): return [] async def send_email(self, recipient, subject, body, attachments=None): + """ + Send an email using the user's Gmail account + + Args: + recipient (str): The email address of the recipient + subject (str): The subject of the email + body (str): The body of the email + attachments (List[str]): A list of file paths to attach to the email + + Returns: + str: The result of sending the email + """ try: service = build("gmail", "v1", credentials=self.creds) @@ -164,6 +186,16 @@ async def send_email(self, recipient, subject, body, attachments=None): return "Failed to send email." async def move_email_to_folder(self, message_id, folder_name): + """ + Move an email to a specific folder in the user's Gmail account + + Args: + message_id (str): The ID of the email message + folder_name (str): The name of the folder to move the email to + + Returns: + str: The result of moving the email + """ try: service = build("gmail", "v1", credentials=self.creds) @@ -197,6 +229,18 @@ async def move_email_to_folder(self, message_id, folder_name): return "Failed to move email." async def create_draft_email(self, recipient, subject, body, attachments=None): + """ + Create a draft email in the user's Gmail account + + Args: + recipient (str): The email address of the recipient + subject (str): The subject of the email + body (str): The body of the email + attachments (List[str]): A list of file paths to attach to the email + + Returns: + str: The result of creating the draft email + """ try: service = build("gmail", "v1", credentials=self.creds) @@ -235,6 +279,15 @@ async def create_draft_email(self, recipient, subject, body, attachments=None): return "Failed to create draft email." async def delete_email(self, message_id): + """ + Delete an email from the user's Gmail account + + Args: + message_id (str): The ID of the email message + + Returns: + str: The result of deleting the email + """ try: service = build("gmail", "v1", credentials=self.creds) service.users().messages().delete(userId="me", id=message_id).execute() @@ -244,6 +297,16 @@ async def delete_email(self, message_id): return "Failed to delete email." async def search_emails(self, query, max_emails=10): + """ + Search emails in the user's Gmail account + + Args: + query (str): The search query to filter emails + max_emails (int): The maximum number of emails to retrieve + + Returns: + List[Dict]: A list of email data + """ try: service = build("gmail", "v1", credentials=self.creds) result = ( @@ -284,6 +347,17 @@ async def search_emails(self, query, max_emails=10): return [] async def reply_to_email(self, message_id, body, attachments=None): + """ + Reply to an email in the user's Gmail account + + Args: + message_id (str): The ID of the email message + body (str): The body of the reply email + attachments (List[str]): A list of file paths to attach to the reply email + + Returns: + str: The result of sending the reply + """ try: service = build("gmail", "v1", credentials=self.creds) message = ( @@ -332,6 +406,15 @@ async def reply_to_email(self, message_id, body, attachments=None): return "Failed to send reply." async def process_attachments(self, message_id): + """ + Process attachments from an email in the user's Gmail account + + Args: + message_id (str): The ID of the email message + + Returns: + List[str]: A list of file paths to the saved attachments + """ try: service = build("gmail", "v1", credentials=self.creds) message = ( @@ -364,6 +447,17 @@ async def process_attachments(self, message_id): return [] async def get_calendar_items(self, start_date=None, end_date=None, max_items=10): + """ + Get calendar items from the user's Google Calendar + + Args: + start_date (datetime): The start date to filter calendar items + end_date (datetime): The end date to filter calendar items + max_items (int): The maximum number of calendar items to retrieve + + Returns: + List[Dict]: A list of calendar item data + """ try: service = build("calendar", "v3", credentials=self.creds) @@ -411,6 +505,19 @@ async def get_calendar_items(self, start_date=None, end_date=None, max_items=10) async def add_calendar_item( self, subject, start_time, end_time, location, attendees=None ): + """ + Add a calendar item to the user's Google Calendar + + Args: + subject (str): The subject of the calendar item + start_time (str): The start time of the calendar item + end_time (str): The end time of the calendar item + location (str): The location of the calendar item + attendees (List[str]): A list of email addresses of attendees + + Returns: + str: The result of adding the calendar item + """ try: service = build("calendar", "v3", credentials=self.creds) @@ -438,6 +545,15 @@ async def add_calendar_item( return "Failed to add calendar item." async def remove_calendar_item(self, item_id): + """ + Remove a calendar item from the user's Google Calendar + + Args: + item_id (str): The ID of the calendar item to remove + + Returns: + str: The result of removing the calendar item + """ try: service = build("calendar", "v3", credentials=self.creds) service.events().delete(calendarId="primary", eventId=item_id).execute() @@ -449,6 +565,16 @@ async def remove_calendar_item(self, item_id): async def google_official_search( self, query: str, num_results: int = 8 ) -> Union[str, List[str]]: + """ + Perform a Google search using the official Google API + + Args: + query (str): The search query + num_results (int): The number of search results to retrieve + + Returns: + Union[str, List[str]]: The search results + """ try: service = build("customsearch", "v1", developerKey=self.GOOGLE_API_KEY) result = ( diff --git a/agixt/extensions/microsoft365.py b/agixt/extensions/microsoft365.py index b96d4a355564..d7e8514ba847 100644 --- a/agixt/extensions/microsoft365.py +++ b/agixt/extensions/microsoft365.py @@ -62,6 +62,17 @@ def authenticate(self): return None async def get_emails(self, folder_name="Inbox", max_emails=10, page_size=10): + """ + Get emails from the specified folder in the Microsoft 365 email account + + Args: + folder_name (str): The name of the folder to retrieve emails from + max_emails (int): The maximum number of emails to retrieve + page_size (int): The number of emails to retrieve per page + + Returns: + list: A list of dictionaries containing email date + """ try: mailbox = self.account.mailbox() folder = mailbox.get_folder(folder_name=folder_name) @@ -90,6 +101,19 @@ async def get_emails(self, folder_name="Inbox", max_emails=10, page_size=10): async def send_email( self, recipient, subject, body, attachments=None, priority=None ): + """ + Send an email using the Microsoft 365 email account + + Args: + recipient (str): The email address of the recipient + subject (str): The subject of the email + body (str): The body of the email + attachments (list): A list of file paths to attach to the email + priority (str): The priority of the email (e.g. "normal", "high", "low") + + Returns: + str: The result of sending the email + """ try: mailbox = self.account.mailbox() message = mailbox.new_message() @@ -120,6 +144,19 @@ async def move_email_to_folder(self, message_id, destination_folder): async def create_draft_email( self, recipient, subject, body, attachments=None, priority=None ): + """ + Create a draft email in the Microsoft 365 email account + + Args: + recipient (str): The email address of the recipient + subject (str): The subject of the email + body (str): The body of the email + attachments (list): A list of file paths to attach to the email + priority (str): The priority of the email (e.g. "normal", "high", "low") + + Returns: + str: The result of creating the draft email + """ try: mailbox = self.account.mailbox() draft = mailbox.new_message() @@ -138,6 +175,15 @@ async def create_draft_email( return "Failed to create draft email." async def delete_email(self, message_id): + """ + Delete an email from the Microsoft 365 email account + + Args: + message_id (str): The ID of the email message to delete + + Returns: + str: The result of deleting the email + """ try: mailbox = self.account.mailbox() message = mailbox.get_message(object_id=message_id) @@ -150,6 +196,18 @@ async def delete_email(self, message_id): async def search_emails( self, query, folder_name="Inbox", max_emails=10, date_range=None ): + """ + Search for emails in the Microsoft 365 email account + + Args: + query (str): The search query to use + folder_name (str): The name of the folder to search in + max_emails (int): The maximum number of emails to retrieve + date_range (tuple): A tuple containing the start and end dates for the search + + Returns: + list: A list of dictionaries containing email data + """ try: mailbox = self.account.mailbox() folder = mailbox.get_folder(folder_name=folder_name) @@ -178,6 +236,17 @@ async def search_emails( return [] async def reply_to_email(self, message_id, body, attachments=None): + """ + Reply to an email in the Microsoft 365 email account + + Args: + message_id (str): The ID of the email message to reply to + body (str): The body of the reply email + attachments (list): A list of file paths to attach to the reply email + + Returns: + str: The result of sending the reply email + """ try: mailbox = self.account.mailbox() message = mailbox.get_message(object_id=message_id) @@ -193,6 +262,15 @@ async def reply_to_email(self, message_id, body, attachments=None): return "Failed to send reply." async def process_attachments(self, message_id): + """ + Process attachments from an email in the Microsoft 365 email account + + Args: + message_id (str): The ID of the email message to process attachments from + + Returns: + list: A list of file paths to the saved attachments + """ try: mailbox = self.account.mailbox() message = mailbox.get_message(object_id=message_id) @@ -209,6 +287,18 @@ async def process_attachments(self, message_id): return [] async def get_calendar_items(self, start_date=None, end_date=None, max_items=10): + """ + Get calendar items from the Microsoft 365 calendar account + + Args: + start_date (datetime): The start date for the calendar items + end_date (datetime): The end date for the calendar items + max_items (int): The maximum number of items to retrieve + + Returns: + list: A list of dictionaries containing calendar item data + """ + try: schedule = self.account.schedule() calendar = schedule.get_default_calendar() @@ -243,6 +333,20 @@ async def get_calendar_items(self, start_date=None, end_date=None, max_items=10) async def add_calendar_item( self, subject, start_time, end_time, location, attendees=None, body=None ): + """ + Add a calendar item to the Microsoft 365 calendar account + + Args: + subject (str): The subject of the calendar item + start_time (datetime): The start time of the calendar item + end_time (datetime): The end time of the calendar item + location (str): The location of the calendar item + attendees (list): A list of email addresses of attendees + body (str): The body of the calendar item + + Returns: + str: The result of adding the calendar item + """ try: schedule = self.account.schedule() calendar = schedule.get_default_calendar() @@ -268,6 +372,15 @@ async def add_calendar_item( return "Failed to add calendar item." async def remove_calendar_item(self, item_id): + """ + Remove a calendar item from the Microsoft 365 calendar account + + Args: + item_id (str): The ID of the calendar item to remove + + Returns: + str: The result of removing the calendar item + """ try: schedule = self.account.schedule() calendar = schedule.get_default_calendar() diff --git a/agixt/extensions/mysql_database.py b/agixt/extensions/mysql_database.py index 57982e4ddfc1..50d9fbba069a 100644 --- a/agixt/extensions/mysql_database.py +++ b/agixt/extensions/mysql_database.py @@ -40,6 +40,15 @@ def get_connection(self): return None async def execute_sql(self, query: str): + """ + Execute a custom SQL query in the MySQL database + + Args: + query (str): The SQL query to execute + + Returns: + str: The result of the SQL query + """ if "```sql" in query: query = query.split("```sql")[1].split("```")[0] logging.info(f"Executing SQL Query: {query}") @@ -88,6 +97,12 @@ async def execute_sql(self, query: str): return await self.execute_sql(query=new_query) async def get_schema(self): + """ + Get the schema of the MySQL database + + Returns: + str: The schema of the MySQL database + """ logging.info(f"Getting schema for database '{self.MYSQL_DATABASE_NAME}'") connection = self.get_connection() cursor = connection.cursor() diff --git a/agixt/extensions/postgres_database.py b/agixt/extensions/postgres_database.py index 0a4abdd69d14..4c4ec687fa8d 100644 --- a/agixt/extensions/postgres_database.py +++ b/agixt/extensions/postgres_database.py @@ -51,6 +51,15 @@ def get_connection(self): return None async def execute_sql(self, query: str): + """ + Execute a custom SQL query in the Postgres database + + Args: + query (str): The SQL query to execute + + Returns: + str: The result of the SQL query + """ if "```sql" in query: query = query.split("```sql")[1].split("```")[0] query = query.replace("\n", " ") @@ -101,6 +110,13 @@ async def execute_sql(self, query: str): return await self.execute_sql(query=new_query) async def get_schema(self): + """ + Get the schema of the Postgres database + + Returns: + str: The schema of the Postgres database + """ + logging.info(f"Getting schema for database '{self.POSTGRES_DATABASE_NAME}'") connection = self.get_connection() cursor = connection.cursor(cursor_factory=psycopg2.extras.DictCursor) diff --git a/agixt/extensions/searxng.py b/agixt/extensions/searxng.py index b7f9fa04c7a1..d80a2fe68603 100644 --- a/agixt/extensions/searxng.py +++ b/agixt/extensions/searxng.py @@ -26,6 +26,15 @@ def get_server(self): return endpoint async def search(self, query: str) -> List[str]: + """ + Search using the SearXNG search engine + + Args: + query (str): The query to search for + + Returns: + List[str]: A list of search results + """ try: response = requests.get( self.SEARXNG_ENDPOINT, diff --git a/agixt/extensions/sendgrid_email.py b/agixt/extensions/sendgrid_email.py index f9e1b387efa0..d3d76826d0fd 100644 --- a/agixt/extensions/sendgrid_email.py +++ b/agixt/extensions/sendgrid_email.py @@ -19,7 +19,18 @@ def __init__(self, SENDGRID_API_KEY: str = "", SENDGRID_EMAIL: str = "", **kwarg if self.SENDGRID_API_KEY: self.commands = {"Send Email with Sendgrid": self.send_email} - async def send_email(self, to_email: str, subject: str, content: str) -> List[str]: + async def send_email(self, to_email: str, subject: str, content: str): + """ + Send an email using SendGrid + + Args: + to_email (str): The email address to send the email to + subject (str): The subject of the email + content (str): The content of the email + + Returns: + str: The result of sending the email + """ message = Mail( from_email=self.SENDGRID_EMAIL, to_emails=to_email, @@ -30,6 +41,6 @@ async def send_email(self, to_email: str, subject: str, content: str) -> List[st try: sg = SendGridAPIClient(self.SENDGRID_API_KEY) response = sg.send(message) - return [f"Email sent successfully. Status code: {response.status_code}"] + return f"Email to {to_email} sent successfully!\n\nThe subject of the email was: {subject}. The content of the email was:\n\n{content}" except Exception as e: - return [f"Error sending email: {e}"] + return f"Error sending email: {e}" diff --git a/agixt/extensions/sqlite_database.py b/agixt/extensions/sqlite_database.py index 264c4ef9573e..b28b8f2a145e 100644 --- a/agixt/extensions/sqlite_database.py +++ b/agixt/extensions/sqlite_database.py @@ -34,6 +34,15 @@ def get_connection(self): return None async def execute_sql(self, query: str): + """ + Execute a custom SQL query in the SQLite database + + Args: + query (str): The SQL query to execute + + Returns: + str: The result of the SQL query + """ if "```sql" in query: query = query.split("```sql")[1].split("```")[0] query = query.replace("\n", " ") @@ -83,6 +92,13 @@ async def execute_sql(self, query: str): return await self.execute_sql(query=new_query) async def get_schema(self): + """ + Get the schema of the SQLite database + + Returns: + str: The schema of the SQLite database + """ + logging.info(f"Getting schema for database '{self.SQLITE_DATABASE_PATH}'") connection = self.get_connection() cursor = connection.cursor() diff --git a/agixt/extensions/times.py b/agixt/extensions/times.py index e808a1bc2130..da4ad86c1622 100644 --- a/agixt/extensions/times.py +++ b/agixt/extensions/times.py @@ -7,4 +7,10 @@ def __init__(self, **kwargs): self.commands = {"Get Datetime": self.get_datetime} async def get_datetime(self) -> str: + """ + Get the current date and time + + Returns: + str: The current date and time in the format "YYYY-MM-DD HH:MM:SS" + """ return "Current date and time: " + datetime.now().strftime("%Y-%m-%d %H:%M:%S") diff --git a/agixt/extensions/web_playwright.py b/agixt/extensions/web_playwright.py index 778fd09b1045..3df0011e532b 100644 --- a/agixt/extensions/web_playwright.py +++ b/agixt/extensions/web_playwright.py @@ -34,6 +34,15 @@ def __init__(self, **kwargs): } async def scrape_text_with_playwright(self, url: str) -> str: + """ + Scrape the text content of a webpage using Playwright + + Args: + url (str): The URL of the webpage to scrape + + Returns: + str: The text content of the webpage + """ try: async with async_playwright() as p: browser = await p.chromium.launch() @@ -57,6 +66,15 @@ async def scrape_text_with_playwright(self, url: str) -> str: return text async def scrape_links_with_playwright(self, url: str) -> Union[str, List[str]]: + """ + Scrape the hyperlinks of a webpage using Playwright + + Args: + url (str): The URL of the webpage to scrape + + Returns: + Union[str, List[str]]: The hyperlinks of the webpage + """ try: async with async_playwright() as p: browser = await p.chromium.launch() @@ -81,6 +99,16 @@ async def scrape_links_with_playwright(self, url: str) -> Union[str, List[str]]: return formatted_links async def take_screenshot_with_playwright(self, url: str, path: str): + """ + Take a screenshot of a webpage using Playwright + + Args: + url (str): The URL of the webpage to take a screenshot of + path (str): The path to save the screenshot + + Returns: + str: The result of taking the screenshot + """ try: async with async_playwright() as p: browser = await p.chromium.launch()