From 00e8237b3c775e40dadd804358a431f02118fea5 Mon Sep 17 00:00:00 2001 From: inzi <889113+inzi@users.noreply.github.com> Date: Sat, 20 Jul 2024 18:37:31 -0500 Subject: [PATCH 1/3] Refactored tools.py into ToolBox, where tools can be added into the tools folder for quick extension of CEPA (Claude Engineer Plus Agents) --- .gitignore | 1 + Plus agents/coordinator.py | 11 +- Plus agents/tool_box.py | 50 ++++++++ Plus agents/tools.py | 186 ---------------------------- Plus agents/tools/__init__.py | 0 Plus agents/tools/base_tool.py | 11 ++ Plus agents/tools/create_file.py | 34 +++++ Plus agents/tools/create_folder.py | 29 +++++ Plus agents/tools/edit_and_apply.py | 61 +++++++++ Plus agents/tools/list_files.py | 28 +++++ Plus agents/tools/read_file.py | 29 +++++ Plus agents/tools/tavily_search.py | 30 +++++ 12 files changed, 279 insertions(+), 191 deletions(-) create mode 100644 .gitignore create mode 100644 Plus agents/tool_box.py delete mode 100644 Plus agents/tools.py create mode 100644 Plus agents/tools/__init__.py create mode 100644 Plus agents/tools/base_tool.py create mode 100644 Plus agents/tools/create_file.py create mode 100644 Plus agents/tools/create_folder.py create mode 100644 Plus agents/tools/edit_and_apply.py create mode 100644 Plus agents/tools/list_files.py create mode 100644 Plus agents/tools/read_file.py create mode 100644 Plus agents/tools/tavily_search.py 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..c7ca0d75 --- /dev/null +++ b/Plus agents/tool_box.py @@ -0,0 +1,50 @@ +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)) + + 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..f4e6b6ab --- /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(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..c1a4661a --- /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(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..2cf448e8 --- /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(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..b9f6befc --- /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(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..1758ea9b --- /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(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..5faf4e58 --- /dev/null +++ b/Plus agents/tools/tavily_search.py @@ -0,0 +1,30 @@ +import json +from tavily import tavily +from tools.base_tool import base_tool + +class tavily_search(base_tool): + def __init__(self): + super().__init__() + 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(tool_input): + try: + query = tool_input["query"] + 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)}" \ No newline at end of file From b1ec1c8daf919c5c0bfeff93513d0d2769e53c01 Mon Sep 17 00:00:00 2001 From: inzi <889113+inzi@users.noreply.github.com> Date: Sat, 20 Jul 2024 20:05:49 -0500 Subject: [PATCH 2/3] Update toolbox and tools --- Plus agents/tool_box.py | 2 ++ Plus agents/tools/create_file.py | 2 +- Plus agents/tools/create_folder.py | 2 +- Plus agents/tools/edit_and_apply.py | 2 +- Plus agents/tools/list_files.py | 2 +- Plus agents/tools/read_file.py | 2 +- Plus agents/tools/tavily_search.py | 2 +- 7 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Plus agents/tool_box.py b/Plus agents/tool_box.py index c7ca0d75..37f44325 100644 --- a/Plus agents/tool_box.py +++ b/Plus agents/tool_box.py @@ -42,6 +42,8 @@ def import_tools(self, subfolder_path): 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: diff --git a/Plus agents/tools/create_file.py b/Plus agents/tools/create_file.py index f4e6b6ab..e8ebe851 100644 --- a/Plus agents/tools/create_file.py +++ b/Plus agents/tools/create_file.py @@ -23,7 +23,7 @@ def __init__(self): } self.name = self.definition["name"] - def execute(tool_input): + def execute(self, tool_input): try: path = tool_input["path"] content = tool_input["content"] diff --git a/Plus agents/tools/create_folder.py b/Plus agents/tools/create_folder.py index c1a4661a..48cc3d1a 100644 --- a/Plus agents/tools/create_folder.py +++ b/Plus agents/tools/create_folder.py @@ -20,7 +20,7 @@ def __init__(self): } self.name = self.definition["name"] - def execute(tool_input): + def execute(self, tool_input): try: path = tool_input["path"] os.makedirs(path, exist_ok=True) diff --git a/Plus agents/tools/edit_and_apply.py b/Plus agents/tools/edit_and_apply.py index 2cf448e8..410148ff 100644 --- a/Plus agents/tools/edit_and_apply.py +++ b/Plus agents/tools/edit_and_apply.py @@ -26,7 +26,7 @@ def __init__(self): } self.name = self.definition["name"] - def execute(tool_input): + def execute(self, tool_input): try: path = tool_input["path"] new_content = tool_input["new_content"] diff --git a/Plus agents/tools/list_files.py b/Plus agents/tools/list_files.py index b9f6befc..9ae9c1ca 100644 --- a/Plus agents/tools/list_files.py +++ b/Plus agents/tools/list_files.py @@ -19,7 +19,7 @@ def __init__(self): } self.name = self.definition["name"] - def execute(tool_input): + def execute(self, tool_input): try: path = tool_input["path"] files = os.listdir(path) diff --git a/Plus agents/tools/read_file.py b/Plus agents/tools/read_file.py index 1758ea9b..af53aeb0 100644 --- a/Plus agents/tools/read_file.py +++ b/Plus agents/tools/read_file.py @@ -19,7 +19,7 @@ def __init__(self): } self.name = self.definition["name"] - def execute(tool_input): + def execute(self, tool_input): try: path = tool_input["path"] with open(path, 'r') as f: diff --git a/Plus agents/tools/tavily_search.py b/Plus agents/tools/tavily_search.py index 5faf4e58..43c37ccb 100644 --- a/Plus agents/tools/tavily_search.py +++ b/Plus agents/tools/tavily_search.py @@ -21,7 +21,7 @@ def __init__(self): } self.name = self.definition["name"] - def execute(tool_input): + def execute(self, tool_input): try: query = tool_input["query"] response = tavily.qna_search(query=query, search_depth="advanced") From 9f9d89b628f76e72cf224baa88b760c0d3542309 Mon Sep 17 00:00:00 2001 From: inzi <889113+inzi@users.noreply.github.com> Date: Sat, 20 Jul 2024 20:27:51 -0500 Subject: [PATCH 3/3] fixed tavily_search tool --- Plus agents/tools/tavily_search.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Plus agents/tools/tavily_search.py b/Plus agents/tools/tavily_search.py index 43c37ccb..d87fef21 100644 --- a/Plus agents/tools/tavily_search.py +++ b/Plus agents/tools/tavily_search.py @@ -1,10 +1,15 @@ 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.", @@ -24,7 +29,7 @@ def __init__(self): def execute(self, tool_input): try: query = tool_input["query"] - response = tavily.qna_search(query=query, search_depth="advanced") + 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