diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..ba0430d2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__/ \ No newline at end of file diff --git a/Plus agents/coordinator.py b/Plus agents/coordinator.py index abb58914..a49fe7fa 100644 --- a/Plus agents/coordinator.py +++ b/Plus agents/coordinator.py @@ -3,7 +3,7 @@ from anthropic import Anthropic from config import ANTHROPIC_API_KEY, COORDINATOR_MODEL, COORDINATOR_BASE_PROMPT, CONTINUATION_EXIT_PHRASE from tool_agent import ToolAgent -from tools import tool_definitions, execute_tool +from tool_box import ToolBox from utils import parse_goals, print_panel logging.basicConfig(level=logging.INFO) @@ -13,7 +13,8 @@ class Coordinator: def __init__(self): self.client = Anthropic(api_key=ANTHROPIC_API_KEY) self.conversation_history = [] - self.tool_agents = {tool["name"]: ToolAgent(tool["name"], tool["description"], tool["input_schema"]) for tool in tool_definitions} + self.toolbox = ToolBox() + self.tool_agents = {tool["name"]: ToolAgent(tool["name"], tool["description"], tool["input_schema"]) for tool in self.toolbox.tool_definitions} self.current_goals = [] self.automode = False @@ -45,7 +46,7 @@ def chat(self, user_input, image_base64=None, max_retries=3): max_tokens=4000, system=COORDINATOR_BASE_PROMPT, messages=messages, - tools=tool_definitions + tools=self.toolbox.tool_definitions ) self.conversation_history.append({"role": "user", "content": message_content}) @@ -61,7 +62,7 @@ def chat(self, user_input, image_base64=None, max_retries=3): logger.info(f"Tool input: {tool_input}") # Execute the actual tool function - actual_result = execute_tool(tool_name, tool_input) + actual_result = self.toolbox.execute_tool(tool_name, tool_input) logger.info(f"Tool result: {actual_result}") @@ -87,7 +88,7 @@ def chat(self, user_input, image_base64=None, max_retries=3): max_tokens=2000, system=COORDINATOR_BASE_PROMPT, messages=self.conversation_history, - tools=tool_definitions + tools=self.toolbox.tool_definitions ) # Process the continuation response diff --git a/Plus agents/tool_box.py b/Plus agents/tool_box.py new file mode 100644 index 00000000..37f44325 --- /dev/null +++ b/Plus agents/tool_box.py @@ -0,0 +1,52 @@ +import os +import sys +import json +import importlib +import inspect + +class ToolBox: + def __init__(self): + self.tools = [] + self.tool_definitions = [] + tools_folder = os.path.join(os.path.dirname(__file__), "tools") + self.tools = self.import_tools(tools_folder) + + def import_tools(self, subfolder_path): + sys.path.append(os.path.dirname(subfolder_path)) + + for filename in os.listdir(subfolder_path): + if filename.endswith('.py') and not filename.startswith('__') and filename != 'base_tool.py': + file_path = os.path.join(subfolder_path, filename) + module_name = f"tools.{os.path.splitext(filename)[0]}" + + try: + spec = importlib.util.spec_from_file_location(module_name, file_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + # Import base_tool here to ensure it's in the module's namespace + from tools.base_tool import base_tool + + for name, obj in inspect.getmembers(module): + if inspect.isclass(obj): + try: + if issubclass(obj, base_tool) and obj is not base_tool: + tool_instance = obj() + self.tools.append(tool_instance) + self.tool_definitions.append(tool_instance.definition) + except TypeError as e: + print(f"TypeError when checking {name}: {e}") + print(f"obj: {obj}, base_tool: {base_tool}") + print(f"obj type: {type(obj)}, base_tool type: {type(base_tool)}") + except Exception as e: + print(f"Error importing {filename}: {e}") + + sys.path.remove(os.path.dirname(subfolder_path)) + + return self.tools + + def execute_tool(self, tool_name, tool_input): + for tool in self.tools: + if tool_name == tool.name: + return tool.execute(tool_input) + return f"Unknown tool: {tool_name}" \ No newline at end of file diff --git a/Plus agents/tools.py b/Plus agents/tools.py deleted file mode 100644 index c1dc60a8..00000000 --- a/Plus agents/tools.py +++ /dev/null @@ -1,186 +0,0 @@ -import os -import json -from tavily import TavilyClient -import difflib -from config import TAVILY_API_KEY -from utils import highlight_diff, print_panel - -tavily = TavilyClient(api_key=TAVILY_API_KEY) - -def create_folder(path): - try: - os.makedirs(path, exist_ok=True) - return f"Folder created: {path}" - except Exception as e: - return f"Error creating folder: {str(e)}" - -def create_file(path, content=""): - try: - with open(path, 'w') as f: - f.write(content) - return f"File created: {path}" - except Exception as e: - return f"Error creating file: {str(e)}" - -def edit_and_apply(path, new_content): - try: - with open(path, 'r') as file: - original_content = file.read() - - if new_content != original_content: - diff = list(difflib.unified_diff( - original_content.splitlines(keepends=True), - new_content.splitlines(keepends=True), - fromfile=f"a/{path}", - tofile=f"b/{path}", - n=3 - )) - - with open(path, 'w') as f: - f.write(new_content) - - diff_text = ''.join(diff) - highlighted_diff = highlight_diff(diff_text) - - print_panel(highlighted_diff, f"Changes in {path}") - - added_lines = sum(1 for line in diff if line.startswith('+') and not line.startswith('+++')) - removed_lines = sum(1 for line in diff if line.startswith('-') and not line.startswith('---')) - - return f"Changes applied to {path}. Lines added: {added_lines}, Lines removed: {removed_lines}" - else: - return f"No changes needed for {path}" - except Exception as e: - return f"Error editing/applying to file: {str(e)}" - -def read_file(path): - try: - with open(path, 'r') as f: - content = f.read() - return content - except Exception as e: - return f"Error reading file: {str(e)}" - -def list_files(path="."): - try: - files = os.listdir(path) - return "\n".join(files) - except Exception as e: - return f"Error listing files: {str(e)}" - -def tavily_search(query): - try: - response = tavily.qna_search(query=query, search_depth="advanced") - return json.dumps(response, indent=2) - except Exception as e: - return f"Error performing search: {str(e)}" - -tool_definitions = [ - { - "name": "create_folder", - "description": "Create a new folder at the specified path. Use this when you need to create a new directory in the project structure.", - "input_schema": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "The path where the folder should be created" - } - }, - "required": ["path"] - } - }, - { - "name": "create_file", - "description": "Create a new file at the specified path with content. Use this when you need to create a new file in the project structure.", - "input_schema": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "The path where the file should be created" - }, - "content": { - "type": "string", - "description": "The content of the file" - } - }, - "required": ["path", "content"] - } - }, - { - "name": "edit_and_apply", - "description": "Apply changes to a file. Use this when you need to edit an existing file.", - "input_schema": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "The path of the file to edit" - }, - "new_content": { - "type": "string", - "description": "The new content to apply to the file" - } - }, - "required": ["path", "new_content"] - } - }, - { - "name": "read_file", - "description": "Read the contents of a file at the specified path. Use this when you need to examine the contents of an existing file.", - "input_schema": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "The path of the file to read" - } - }, - "required": ["path"] - } - }, - { - "name": "list_files", - "description": "List all files and directories in the specified folder. Use this when you need to see the contents of a directory.", - "input_schema": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "The path of the folder to list (default: current directory)" - } - } - } - }, - { - "name": "tavily_search", - "description": "Perform a web search using Tavily API to get up-to-date information or additional context. Use this when you need current information or feel a search could provide a better answer.", - "input_schema": { - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "The search query" - } - }, - "required": ["query"] - } - } -] - -def execute_tool(tool_name, tool_input): - if tool_name == "create_folder": - return create_folder(tool_input["path"]) - elif tool_name == "create_file": - return create_file(tool_input["path"], tool_input["content"]) - elif tool_name == "edit_and_apply": - return edit_and_apply(tool_input["path"], tool_input["new_content"]) - elif tool_name == "read_file": - return read_file(tool_input["path"]) - elif tool_name == "list_files": - return list_files(tool_input.get("path", ".")) - elif tool_name == "tavily_search": - return tavily_search(tool_input["query"]) - else: - return f"Unknown tool: {tool_name}" \ No newline at end of file diff --git a/Plus agents/tools/__init__.py b/Plus agents/tools/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Plus agents/tools/base_tool.py b/Plus agents/tools/base_tool.py new file mode 100644 index 00000000..6a32a194 --- /dev/null +++ b/Plus agents/tools/base_tool.py @@ -0,0 +1,11 @@ +from abc import ABC, abstractmethod +from typing import Dict, Any + +class base_tool(ABC): + def __init__(self): + self.name = None + pass + + @abstractmethod + def execute(self, tool_input: Dict[str, Any]) -> Any: + pass \ No newline at end of file diff --git a/Plus agents/tools/create_file.py b/Plus agents/tools/create_file.py new file mode 100644 index 00000000..e8ebe851 --- /dev/null +++ b/Plus agents/tools/create_file.py @@ -0,0 +1,34 @@ +from tools.base_tool import base_tool + +class create_file(base_tool): + def __init__(self): + super().__init__() + self.definition = { + "name": "create_file", + "description": "Create a new file at the specified path with content. Use this when you need to create a new file in the project structure.", + "input_schema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "The path where the file should be created" + }, + "content": { + "type": "string", + "description": "The content of the file" + } + }, + "required": ["path", "content"] + } + } + self.name = self.definition["name"] + + def execute(self, tool_input): + try: + path = tool_input["path"] + content = tool_input["content"] + with open(path, 'w') as f: + f.write(content) + return f"File created: {path}" + except Exception as e: + return f"Error creating file: {str(e)}" \ No newline at end of file diff --git a/Plus agents/tools/create_folder.py b/Plus agents/tools/create_folder.py new file mode 100644 index 00000000..48cc3d1a --- /dev/null +++ b/Plus agents/tools/create_folder.py @@ -0,0 +1,29 @@ +import os +from tools.base_tool import base_tool + +class create_folder(base_tool): + def __init__(self): + super().__init__() + self.definition = { + "name": "create_folder", + "description": "Create a new folder at the specified path. Use this when you need to create a new directory in the project structure.", + "input_schema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "The path where the folder should be created" + } + }, + "required": ["path"] + } + } + self.name = self.definition["name"] + + def execute(self, tool_input): + try: + path = tool_input["path"] + os.makedirs(path, exist_ok=True) + return f"Folder created: {path}" + except Exception as e: + return f"Error creating folder: {str(e)}" \ No newline at end of file diff --git a/Plus agents/tools/edit_and_apply.py b/Plus agents/tools/edit_and_apply.py new file mode 100644 index 00000000..410148ff --- /dev/null +++ b/Plus agents/tools/edit_and_apply.py @@ -0,0 +1,61 @@ +import difflib +from utils import highlight_diff, print_panel +from tools.base_tool import base_tool + + +class edit_and_apply(base_tool): + def __init__(self): + super().__init__() + self.definition = { + "name": "edit_and_apply", + "description": "Apply changes to a file. Use this when you need to edit an existing file.", + "input_schema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "The path of the file to edit" + }, + "new_content": { + "type": "string", + "description": "The new content to apply to the file" + } + }, + "required": ["path", "new_content"] + } + } + self.name = self.definition["name"] + + def execute(self, tool_input): + try: + path = tool_input["path"] + new_content = tool_input["new_content"] + with open(path, 'r') as file: + original_content = file.read() + + if new_content != original_content: + diff = list(difflib.unified_diff( + original_content.splitlines(keepends=True), + new_content.splitlines(keepends=True), + fromfile=f"a/{path}", + tofile=f"b/{path}", + n=3 + )) + + with open(path, 'w') as f: + f.write(new_content) + + diff_text = ''.join(diff) + highlighted_diff = highlight_diff(diff_text) + + print_panel(highlighted_diff, f"Changes in {path}") + + added_lines = sum(1 for line in diff if line.startswith('+') and not line.startswith('+++')) + removed_lines = sum(1 for line in diff if line.startswith('-') and not line.startswith('---')) + + return f"Changes applied to {path}. Lines added: {added_lines}, Lines removed: {removed_lines}" + else: + return f"No changes needed for {path}" + except Exception as e: + return f"Error editing/applying to file: {str(e)}" + \ No newline at end of file diff --git a/Plus agents/tools/list_files.py b/Plus agents/tools/list_files.py new file mode 100644 index 00000000..9ae9c1ca --- /dev/null +++ b/Plus agents/tools/list_files.py @@ -0,0 +1,28 @@ +import os +from tools.base_tool import base_tool + +class list_files(base_tool): + def __init__(self): + super().__init__() + self.definition = { + "name": "list_files", + "description": "List all files and directories in the specified folder. Use this when you need to see the contents of a directory.", + "input_schema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "The path of the folder to list (default: current directory)" + } + } + } + } + self.name = self.definition["name"] + + def execute(self, tool_input): + try: + path = tool_input["path"] + files = os.listdir(path) + return "\n".join(files) + except Exception as e: + return f"Error listing files: {str(e)}" \ No newline at end of file diff --git a/Plus agents/tools/read_file.py b/Plus agents/tools/read_file.py new file mode 100644 index 00000000..af53aeb0 --- /dev/null +++ b/Plus agents/tools/read_file.py @@ -0,0 +1,29 @@ +from tools.base_tool import base_tool + +class read_file(base_tool): + def __init__(self): + super().__init__() + self.definition = { + "name": "read_file", + "description": "Read the contents of a file at the specified path. Use this when you need to examine the contents of an existing file.", + "input_schema": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "The path of the file to read" + } + }, + "required": ["path"] + } + } + self.name = self.definition["name"] + + def execute(self, tool_input): + try: + path = tool_input["path"] + with open(path, 'r') as f: + content = f.read() + return content + except Exception as e: + return f"Error reading file: {str(e)}" \ No newline at end of file diff --git a/Plus agents/tools/tavily_search.py b/Plus agents/tools/tavily_search.py new file mode 100644 index 00000000..d87fef21 --- /dev/null +++ b/Plus agents/tools/tavily_search.py @@ -0,0 +1,35 @@ +import json +import os +from tavily import tavily +from tavily import TavilyClient +from tools.base_tool import base_tool +from config import TAVILY_API_KEY + +class tavily_search(base_tool): + def __init__(self): + super().__init__() + api_key=TAVILY_API_KEY + self.tavily = TavilyClient(api_key) + self.definition = { + "name": "tavily_search", + "description": "Perform a web search using Tavily API to get up-to-date information or additional context. Use this when you need current information or feel a search could provide a better answer.", + "input_schema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "The search query" + } + }, + "required": ["query"] + } + } + self.name = self.definition["name"] + + def execute(self, tool_input): + try: + query = tool_input["query"] + response = self.tavily.qna_search(query=query, search_depth="advanced") + return json.dumps(response, indent=2) + except Exception as e: + return f"Error performing search: {str(e)}" \ No newline at end of file