From 65c0c6de8045cd44d08b58f08fbdd51c7afeb23b Mon Sep 17 00:00:00 2001 From: ChungHwan Han <51526347+hanchchch@users.noreply.github.com> Date: Thu, 23 Mar 2023 19:12:57 +0900 Subject: [PATCH] feat: code editor (#5) * feat: code editor * feat: code writer * doc: fix comment --- core/editor/__init__.py | 3 + core/editor/patch.py | 179 ++++++++++++++++++++++++++++++++++++++++ core/editor/read.py | 35 ++++++++ core/editor/write.py | 39 +++++++++ core/tools/cpu.py | 76 +++++++++-------- 5 files changed, 296 insertions(+), 36 deletions(-) create mode 100644 core/editor/__init__.py create mode 100644 core/editor/patch.py create mode 100644 core/editor/read.py create mode 100644 core/editor/write.py diff --git a/core/editor/__init__.py b/core/editor/__init__.py new file mode 100644 index 0000000..eb2ec5b --- /dev/null +++ b/core/editor/__init__.py @@ -0,0 +1,3 @@ +from .patch import CodePatcher +from .read import CodeReader +from .write import CodeWriter diff --git a/core/editor/patch.py b/core/editor/patch.py new file mode 100644 index 0000000..08474e1 --- /dev/null +++ b/core/editor/patch.py @@ -0,0 +1,179 @@ +""" +patch protocol: + +|,|,| +---~~~+++===+++~~~--- +|,|,| +---~~~+++===+++~~~--- +... +---~~~+++===+++~~~--- + +let say original code is: +``` +import requests + +def crawl_news(keyword): + url = f"https://www.google.com/search?q={keyword}+news" + response = requests.get(url) + + news = [] + for result in response: + news.append(result.text) + + return news +``` + +and we want to change it to: +``` +import requests +from bs4 import BeautifulSoup + +def crawl_news(keyword): + url = f"https://www.google.com/search?q={keyword}+news" + html = requests.get(url).text + soup = BeautifulSoup(html, "html.parser") + news_results = soup.find_all("div", class_="BNeawe vvjwJb AP7Wnd") + + news_titles = [] + for result in news_results: + news_titles.append(result.text) + + return news_titles +``` + +then the command will be: +test.py|2,1|2,1|from bs4 import BeautifulSoup + +---~~~+++===+++~~~--- +test.py|5,5|5,33|html = requests.get(url).text + soup = BeautifulSoup(html, "html.parser") + news_results = soup.find_all("div", class_="BNeawe vvjwJb AP7Wnd") +---~~~+++===+++~~~--- +test.py|7,5|9,13|news_titles = [] + for result in news_results: + news_titles +---~~~+++===+++~~~--- +test.py|11,16|11,16|_titles +""" + +from typing import Tuple + + +class Position: + separator = "," + + def __init__(self, line: int, col: int): + self.line: int = line + self.col: int = col + + @staticmethod + def from_str(pos: str) -> "Position": + line, col = pos.split(Position.separator) + return Position(int(line) - 1, int(col) - 1) + + +class PatchCommand: + separator = "|" + + def __init__(self, filepath: str, start: Position, end: Position, content: str): + self.filepath: str = filepath + self.start: Position = start + self.end: Position = end + self.content: str = content + + def read_lines(self) -> list[str]: + with open(self.filepath, "r") as f: + lines = f.readlines() + return lines + + def write_lines(self, lines: list[str]) -> int: + with open(self.filepath, "w") as f: + f.writelines(lines) + return sum([len(line) for line in lines]) + + def execute(self) -> Tuple[int, int]: + lines = self.read_lines() + before = sum([len(line) for line in lines]) + + lines[self.start.line] = ( + lines[self.start.line][: self.start.col] + + self.content + + lines[self.end.line][self.end.col :] + ) + lines = lines[: self.start.line + 1] + lines[self.end.line + 1 :] + + after = self.write_lines(lines) + + written = len(self.content) + deleted = before - after + written + + return written, deleted + + @staticmethod + def from_str(command: str) -> "PatchCommand": + filepath, start, end = command.split(PatchCommand.separator)[:3] + content = command[len(filepath + start + end) + 3 :] + return PatchCommand( + filepath, Position.from_str(start), Position.from_str(end), content + ) + + +class CodePatcher: + separator = "\n---~~~+++===+++~~~---\n" + + @staticmethod + def sort_commands(commands: list[PatchCommand]) -> list[PatchCommand]: + return sorted(commands, key=lambda c: c.start.line, reverse=True) + + @staticmethod + def patch(bulk_command: str) -> Tuple[int, int]: + commands = [ + PatchCommand.from_str(command) + for command in bulk_command.split(CodePatcher.separator) + if command != "" + ] + commands = CodePatcher.sort_commands(commands) + + written, deleted = 0, 0 + for command in commands: + if command: + w, d = command.execute() + written += w + deleted += d + return written, deleted + + +if __name__ == "__main__": + commands = """test.py|2,1|2,1|from bs4 import BeautifulSoup + +---~~~+++===+++~~~--- +test.py|5,5|5,33|html = requests.get(url).text + soup = BeautifulSoup(html, "html.parser") + news_results = soup.find_all("div", class_="BNeawe vvjwJb AP7Wnd") +---~~~+++===+++~~~--- +test.py|7,5|9,13|news_titles = [] + for result in news_results: + news_titles +---~~~+++===+++~~~--- +test.py|11,16|11,16|_titles +""" + + example = """import requests + +def crawl_news(keyword): + url = f"https://www.google.com/search?q={keyword}+news" + response = requests.get(url) + + news = [] + for result in response: + news.append(result.text) + + return news +""" + testfile = "test.py" + with open(testfile, "w") as f: + f.write(example) + + patcher = CodePatcher() + written, deleted = patcher.patch(commands) + print(f"written: {written}, deleted: {deleted}") diff --git a/core/editor/read.py b/core/editor/read.py new file mode 100644 index 0000000..c22624f --- /dev/null +++ b/core/editor/read.py @@ -0,0 +1,35 @@ +""" +read protocol: + +|- +""" + + +class ReadCommand: + separator = "|" + + def __init__(self, filepath: str, start: int, end: int): + self.filepath: str = filepath + self.start: int = start + self.end: int = end + + def execute(self): + with open(self.filepath, "r") as f: + code = f.readlines() + if self.start == self.end: + code = code[self.start - 1] + else: + code = "".join(code[self.start - 1 : self.end]) + return code + + @staticmethod + def from_str(command: str) -> "ReadCommand": + filepath, line = command.split(ReadCommand.separator) + start, end = line.split("-") + return ReadCommand(filepath, int(start), int(end)) + + +class CodeReader: + @staticmethod + def read(command: str) -> str: + return ReadCommand.from_str(command).execute() diff --git a/core/editor/write.py b/core/editor/write.py new file mode 100644 index 0000000..61e069b --- /dev/null +++ b/core/editor/write.py @@ -0,0 +1,39 @@ +""" +write protocol: + + + +""" + + +class WriteCommand: + separator = "\n" + + def __init__(self, filepath: str, content: int): + self.filepath: str = filepath + self.content: str = content + self.mode: str = "w" + + def with_mode(self, mode: str) -> "WriteCommand": + self.mode = mode + return self + + def execute(self) -> str: + with open(self.filepath, self.mode) as f: + f.write(self.content) + return self.content + + @staticmethod + def from_str(command: str) -> "WriteCommand": + filepath = command.split(WriteCommand.separator)[0] + return WriteCommand(filepath, command[len(filepath) + 1 :]) + + +class CodeWriter: + @staticmethod + def write(command: str) -> str: + return WriteCommand.from_str(command).with_mode("w").execute() + + @staticmethod + def append(command: str) -> str: + return WriteCommand.from_str(command).with_mode("a").execute() diff --git a/core/tools/cpu.py b/core/tools/cpu.py index 4f17c07..d18bd8b 100644 --- a/core/tools/cpu.py +++ b/core/tools/cpu.py @@ -9,6 +9,7 @@ from bs4 import BeautifulSoup import subprocess +from core.editor import CodePatcher, CodeReader, CodeWriter from .base import tool, BaseToolSet, ToolScope, SessionGetter from logger import logger @@ -48,21 +49,8 @@ class CodeEditor(BaseToolSet): "and the output will be code. ", ) def read(self, inputs: str) -> str: - filename, line = inputs.split(",") - line = line.split("-") - if len(line) == 1: - line = int(line[0]) - else: - line = [int(i) for i in line] - try: - with open(filename, "r") as f: - code = f.readlines() - if isinstance(line, int): - code = code[line - 1] - else: - code = "".join(code[line[0] - 1 : line[1]]) - output = code + output = CodeReader.read(inputs) except Exception as e: output = str(e) @@ -72,49 +60,65 @@ class CodeEditor(BaseToolSet): ) return output + @tool( + name="CodeEditor.APPEND", + description="Append code to the existing file. " + "If the code is completed, use the Terminal tool to execute it, if not, append the code through the this tool. " + "Input should be filename and code to append. " + "Input code must be the code that should be appended, NOT whole code. " + "ex. test.py\nprint('hello world')\n " + "and the output will be last 3 line.", + ) + def write(self, inputs: str) -> str: + try: + code = CodeWriter.append(inputs) + output = "Last 3 line was:\n" + "\n".join(code.split("\n")[-3:]) + except Exception as e: + output = str(e) + + logger.debug( + f"\nProcessed CodeEditor, Input: {inputs} " f"Output Answer: {output}" + ) + return output + @tool( name="CodeEditor.WRITE", description="Write code to create a new tool. " - "You must check the file's contents before writing. This tool only supports append code. " - "If the code is completed, use the Terminal tool to execute it, if not, append the code through the CodeEditor tool. " + "If the code is completed, use the Terminal tool to execute it, if not, append the code through the CodeEditor.APPEND tool. " "Input should be filename and code. " "ex. test.py\nprint('hello world')\n " "and the output will be last 3 line.", ) def write(self, inputs: str) -> str: - filename, code = inputs.split("\n", 1) - try: - with open(filename, "a") as f: - f.write(code) + code = CodeWriter.write(inputs) output = "Last 3 line was:\n" + "\n".join(code.split("\n")[-3:]) except Exception as e: output = str(e) logger.debug( - f"\nProcessed CodeEditor, Input Codes: {code} " f"Output Answer: {output}" + f"\nProcessed CodeEditor, Input: {inputs} " f"Output Answer: {output}" ) return output @tool( name="CodeEditor.PATCH", - description="Correct the error throught the code patch if an error occurs. " - "Input is a list of patches. The patch is separated by \\n.The patch consists of a file name, line number, and new code. It is seperated by -||-. " - "ex. \"test.py-||-1-||-print('hello world')\ntest.py-||-2-||-print('hello world')\n\" " - "and the output will be success or error message. ", + description="Patch the code to correct the error if an error occurs or to improve it. " + "Input is a list of patches. The patch is separated by {seperator}. ".format( + seperator=CodePatcher.separator.replace("\n", "\\n") + ) + + "Each patch has to be formatted like below.\n" + "|,|,|" + "Code between start and end will be replaced with new_code. " + "The output will be written/deleted bytes or error message. ", ) def patch(self, patches: str) -> str: - for patch in patches.split("\n"): - filename, line_number, new_line = patch.split("-||-") # TODO: fix this - try: - with open(filename, "r") as f: - lines = f.readlines() - lines[int(line_number) - 1] = new_line + "\n" - with open(filename, "w") as f: - f.writelines(lines) - output = "success" - except Exception as e: - output = str(e) + try: + w, d = CodePatcher.patch(patches) + output = f"successfully wrote {w}, deleted {d}" + except Exception as e: + output = str(e) + logger.debug( f"\nProcessed CodeEditor, Input Patch: {patches} " f"Output Answer: {output}"