From 69c27d6cd11344ed9a9f8f2411c86283ff57b9b1 Mon Sep 17 00:00:00 2001 From: Daniel Vainsencher Date: Wed, 29 May 2024 17:03:34 -0400 Subject: [PATCH 1/7] aider: Refactored `show_messages` to call `format_messages` for formatting title and messages. # Aider chat conversation: USER: Refactor show_messages in utils, so that the formatting of title and messages is done in a separate function which returns a string. show_messages should call it and print its output. ASSISTANT: To refactor the `show_messages` function, we will: 1. Create a new function `format_messages` that formats the title and messages and returns a string. 2. Update `show_messages` to call `format_messages` and print its output. Here are the *SEARCH/REPLACE* blocks: aider/utils.py ```python <<<<<<< SEARCH def show_messages(messages, title=None, functions=None): if title: print(title.upper(), "*" * 50) for msg in messages: print() role = msg["role"].upper() content = msg.get("content") if isinstance(content, list): # Handle list content (e.g., image messages) for item in content: if isinstance(item, dict) and "image_url" in item: print(role, "Image URL:", item["image_url"]["url"]) elif isinstance(content, str): # Handle string content for line in content.splitlines(): print(role, line) content = msg.get("function_call") if content: print(role, content) if functions: dump(functions) ======= def format_messages(messages, title=None): output = [] if title: output.append(f"{title.upper()} {'*' * 50}") for msg in messages: output.append("") role = msg["role"].upper() content = msg.get("content") if isinstance(content, list): # Handle list content (e.g., image messages) for item in content: if isinstance(item, dict) and "image_url" in item: output.append(f"{role} Image URL: {item['image_url']['url']}") elif isinstance(content, str): # Handle string content for line in content.splitlines(): output.append(f"{role} {line}") content = msg.get("function_call") if content: output.append(f"{role} {content}") return "\n".join(output) def show_messages(messages, title=None, functions=None): formatted_output = format_messages(messages, title) print(formatted_output) if functions: dump(functions) >>>>>>> REPLACE ``` --- aider/utils.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/aider/utils.py b/aider/utils.py index 31767aec6..1e76b39bf 100644 --- a/aider/utils.py +++ b/aider/utils.py @@ -84,24 +84,32 @@ def safe_abs_path(res): return str(res) -def show_messages(messages, title=None, functions=None): +def format_messages(messages, title=None): + output = [] if title: - print(title.upper(), "*" * 50) + output.append(f"{title.upper()} {'*' * 50}") for msg in messages: - print() + output.append("") role = msg["role"].upper() content = msg.get("content") if isinstance(content, list): # Handle list content (e.g., image messages) for item in content: if isinstance(item, dict) and "image_url" in item: - print(role, "Image URL:", item["image_url"]["url"]) + output.append(f"{role} Image URL: {item['image_url']['url']}") elif isinstance(content, str): # Handle string content for line in content.splitlines(): - print(role, line) + output.append(f"{role} {line}") content = msg.get("function_call") if content: - print(role, content) + output.append(f"{role} {content}") + + return "\n".join(output) + + +def show_messages(messages, title=None, functions=None): + formatted_output = format_messages(messages, title) + print(formatted_output) if functions: dump(functions) From a0b279f1054ede7d565d3f83cb5bf11c14bed39d Mon Sep 17 00:00:00 2001 From: Daniel Vainsencher Date: Wed, 29 May 2024 17:07:41 -0400 Subject: [PATCH 2/7] Refactored format_messages to use format_content for string content handling. --- aider/utils.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/aider/utils.py b/aider/utils.py index 1e76b39bf..3a09f0236 100644 --- a/aider/utils.py +++ b/aider/utils.py @@ -84,7 +84,12 @@ def safe_abs_path(res): return str(res) -def format_messages(messages, title=None): +def format_content(role, content): + formatted_lines = [] + for line in content.splitlines(): + formatted_lines.append(f"{role} {line}") + return "\n".join(formatted_lines) + output = [] if title: output.append(f"{title.upper()} {'*' * 50}") @@ -98,8 +103,7 @@ def format_messages(messages, title=None): if isinstance(item, dict) and "image_url" in item: output.append(f"{role} Image URL: {item['image_url']['url']}") elif isinstance(content, str): # Handle string content - for line in content.splitlines(): - output.append(f"{role} {line}") + output.append(format_content(role, content)) content = msg.get("function_call") if content: output.append(f"{role} {content}") From abf8d5e60df4aae01538e03dee9ef9b901e93140 Mon Sep 17 00:00:00 2001 From: Daniel Vainsencher Date: Wed, 29 May 2024 17:07:42 -0400 Subject: [PATCH 3/7] aider: Refactored `format_messages` to use a new function `format_content` for line-by-line formatting. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Aider chat conversation: USER: Now refactor format_messages: extract a function that takes a role and content (where content is a string), and returns the line by line formatted string. ASSISTANT: To refactor `format_messages`, we will: 1. Create a new function `format_content` that takes a role and content (where content is a string) and returns the line-by-line formatted string. 2. Update `format_messages` to use `format_content`. Here are the *SEARCH/REPLACE* blocks: aider/utils.py ```python <<<<<<< SEARCH def format_messages(messages, title=None): ======= def format_content(role, content): formatted_lines = [] for line in content.splitlines(): formatted_lines.append(f"{role} {line}") return "\n".join(formatted_lines) >>>>>>> REPLACE ``` aider/utils.py ```python <<<<<<< SEARCH elif isinstance(content, str): # Handle string content for line in content.splitlines(): output.append(f"{role} {line}") ======= elif isinstance(content, str): # Handle string content output.append(format_content(role, content)) >>>>>>> REPLACE ``` USER: # Fix any errors below, if possible. ## Running: flake8 --select=E9,F821,F823,F831,F406,F407,F701,F702,F704,F706 --show-source --isolated aider/utils.py aider/utils.py:94:8: F821 undefined name 'title' if title: ^ aider/utils.py:95:26: F821 undefined name 'title' output.append(f"{title.upper()} {'*' * 50}") ^ aider/utils.py:97:16: F821 undefined name 'messages' for msg in messages: ^ aider/utils.py:115:24: F821 undefined name 'format_messages' formatted_output = format_messages(messages, title) ^ ## See relevant lines below marked with █. aider/utils.py: ...⋮... 87│def format_content(role, content): 88│ formatted_lines = [] ...⋮... 91│ return "\n".join(formatted_lines) 92│ 93│ output = [] 94█ if title: 95█ output.append(f"{title.upper()} {'*' * 50}") 96│ 97█ for msg in messages: 98│ output.append("") 99│ role = msg["role"].upper() 100│ content = msg.get("content") ...⋮... 112│ 113│ 114│def show_messages(messages, title=None, functions=None): 115█ formatted_output = format_messages(messages, title) 116│ print(formatted_output) 117│ 118│ if functions: ...⋮... ASSISTANT: To fix the errors, we need to ensure that the `format_messages` function is properly defined and that the misplaced code is correctly organized. Here are the corrected *SEARCH/REPLACE* blocks: aider/utils.py ```python <<<<<<< SEARCH return "\n".join(formatted_lines) output = [] if title: output.append(f"{title.upper()} {'*' * 50}") for msg in messages: output.append("") role = msg["role"].upper() content = msg.get("content") if isinstance(content, list): # Handle list content (e.g., image messages) for item in content: if isinstance(item, dict) and "image_url" in item: output.append(f"{role} Image URL: {item['image_url']['url']}") elif isinstance(content, str): # Handle string content output.append(format_content(role, content)) content = msg.get("function_call") if content: output.append(f"{role} {content}") return "\n".join(output) ======= return "\n".join(formatted_lines) def format_messages(messages, title=None): output = [] if title: output.append(f"{title.upper()} {'*' * 50}") for msg in messages: output.append("") role = msg["role"].upper() content = msg.get("content") if isinstance(content, list): # Handle list content (e.g., image messages) for item in content: if isinstance(item, dict) and "image_url" in item: output.append(f"{role} Image URL: {item['image_url']['url']}") elif isinstance(content, str): # Handle string content output.append(format_content(role, content)) content = msg.get("function_call") if content: output.append(f"{role} {content}") return "\n".join(output) >>>>>>> REPLACE ``` --- aider/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aider/utils.py b/aider/utils.py index 3a09f0236..7636eb119 100644 --- a/aider/utils.py +++ b/aider/utils.py @@ -90,6 +90,8 @@ def format_content(role, content): formatted_lines.append(f"{role} {line}") return "\n".join(formatted_lines) + +def format_messages(messages, title=None): output = [] if title: output.append(f"{title.upper()} {'*' * 50}") From 2bd5daf2aded1c5e8a313c74722be59597b0b79d Mon Sep 17 00:00:00 2001 From: Daniel Vainsencher Date: Wed, 29 May 2024 17:16:08 -0400 Subject: [PATCH 4/7] aider: Add logging of conversation between coder and LLM in .aider.llm.history file using new formatting functions in `send_new_user_message`. # Aider chat conversation: USER: We want to add logging of the conversation between coder and the LLM into a new file called .aider.llm.history. The formatting should be done using the new format_messages and format_content. Modify only the `send_new_user_message` function in base_coder, formatting and logging `messages` as soon as it is defined, and logging `content` before `tool_output`. Precede them by a single line saying "TO LLM {timestamp}" and "LLM RESPONSE {timestamp}" where the timestamp is uses ISO date format and has second precision. ASSISTANT: To implement the logging of the conversation between the coder and the LLM, we will: 1. Format and log `messages` as soon as it is defined in `send_new_user_message`. 2. Log `content` before `tool_output`. 3. Precede the logs with a timestamp in ISO date format with second precision. Here are the *SEARCH/REPLACE* blocks: aider/coders/base_coder.py ```python <<<<<<< SEARCH messages = self.format_messages() ======= import datetime from aider.utils import format_messages, format_content messages = self.format_messages() timestamp = datetime.datetime.now().isoformat(timespec='seconds') with open('.aider.llm.history', 'a') as log_file: log_file.write(f"TO LLM {timestamp}\n") log_file.write(format_messages(messages) + "\n") >>>>>>> REPLACE ``` aider/coders/base_coder.py ```python <<<<<<< SEARCH self.io.tool_output() ======= self.io.tool_output() timestamp = datetime.datetime.now().isoformat(timespec='seconds') with open('.aider.llm.history', 'a') as log_file: log_file.write(f"LLM RESPONSE {timestamp}\n") log_file.write(format_content("assistant", content) + "\n") >>>>>>> REPLACE ``` --- aider/coders/base_coder.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 76081a916..d179bcc0d 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -779,8 +779,16 @@ class Coder: dict(role="user", content=inp), ] + import datetime + from aider.utils import format_messages, format_content + messages = self.format_messages() + timestamp = datetime.datetime.now().isoformat(timespec='seconds') + with open('.aider.llm.history', 'a') as log_file: + log_file.write(f"TO LLM {timestamp}\n") + log_file.write(format_messages(messages) + "\n") + if self.verbose: utils.show_messages(messages, functions=self.functions) @@ -823,6 +831,11 @@ class Coder: self.io.tool_output() + timestamp = datetime.datetime.now().isoformat(timespec='seconds') + with open('.aider.llm.history', 'a') as log_file: + log_file.write(f"LLM RESPONSE {timestamp}\n") + log_file.write(format_content("assistant", content) + "\n") + if interrupted: content += "\n^C KeyboardInterrupt" self.cur_messages += [dict(role="assistant", content=content)] From 8896909e196dec584e03d55dfd3b93e76a2c84d3 Mon Sep 17 00:00:00 2001 From: Daniel Vainsencher Date: Wed, 29 May 2024 17:19:32 -0400 Subject: [PATCH 5/7] aider: Capitalize the "ASSISTANT" role when logging to the LLM history file. # Aider chat conversation: USER: When logging to llm history file, capitalize the ASSISTANT role. ASSISTANT: To capitalize the "ASSISTANT" role when logging to the LLM history file, we need to modify the `send_new_user_message` method in `aider/coders/base_coder.py`. Here is the *SEARCH/REPLACE* block: aider/coders/base_coder.py ```python <<<<<<< SEARCH with open('.aider.llm.history', 'a') as log_file: log_file.write(f"LLM RESPONSE {timestamp}\n") log_file.write(format_content("assistant", content) + "\n") ======= with open('.aider.llm.history', 'a') as log_file: log_file.write(f"LLM RESPONSE {timestamp}\n") log_file.write(format_content("ASSISTANT", content) + "\n") >>>>>>> REPLACE ``` --- aider/coders/base_coder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index d179bcc0d..b1616fb95 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -834,7 +834,7 @@ class Coder: timestamp = datetime.datetime.now().isoformat(timespec='seconds') with open('.aider.llm.history', 'a') as log_file: log_file.write(f"LLM RESPONSE {timestamp}\n") - log_file.write(format_content("assistant", content) + "\n") + log_file.write(format_content("ASSISTANT", content) + "\n") if interrupted: content += "\n^C KeyboardInterrupt" From e6e360a4ef568bb2d5c4eb60ad2b0c779fcaeff8 Mon Sep 17 00:00:00 2001 From: Daniel Vainsencher Date: Fri, 7 Jun 2024 09:48:23 -0400 Subject: [PATCH 6/7] Added support for logging LLM history to a specified file. --- aider/args.py | 8 +++++++- aider/coders/base_coder.py | 13 +++++-------- aider/io.py | 17 ++++++++++++++--- aider/main.py | 1 + 4 files changed, 27 insertions(+), 12 deletions(-) diff --git a/aider/args.py b/aider/args.py index f747016dc..24682fa55 100644 --- a/aider/args.py +++ b/aider/args.py @@ -20,11 +20,17 @@ def get_parser(default_config_files, git_root): auto_env_var_prefix="AIDER_", ) group = parser.add_argument_group("Main") + group.add_argument( + "--llm-history-file", + metavar="LLM_HISTORY_FILE", + default=".aider.llm.history", + help="Specify the LLM history file (default: .aider.llm.history)", + ) group.add_argument( "files", metavar="FILE", nargs="*", - help="files to edit with an LLM (optional)", + help="files to edit with an LLM (optional)" ) group.add_argument( "--openai-api-key", diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index b1616fb95..a8ddf8830 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -784,10 +784,7 @@ class Coder: messages = self.format_messages() - timestamp = datetime.datetime.now().isoformat(timespec='seconds') - with open('.aider.llm.history', 'a') as log_file: - log_file.write(f"TO LLM {timestamp}\n") - log_file.write(format_messages(messages) + "\n") + self.io.log_llm_history("TO LLM", format_messages(messages)) if self.verbose: utils.show_messages(messages, functions=self.functions) @@ -808,6 +805,9 @@ class Coder: exhausted = True else: raise err + except Exception as err: + self.io.tool_error(f"Unexpected error: {err}") + return if exhausted: self.num_exhausted_context_windows += 1 @@ -831,10 +831,7 @@ class Coder: self.io.tool_output() - timestamp = datetime.datetime.now().isoformat(timespec='seconds') - with open('.aider.llm.history', 'a') as log_file: - log_file.write(f"LLM RESPONSE {timestamp}\n") - log_file.write(format_content("ASSISTANT", content) + "\n") + self.io.log_llm_history("LLM RESPONSE", format_content("ASSISTANT", content)) if interrupted: content += "\n^C KeyboardInterrupt" diff --git a/aider/io.py b/aider/io.py index a365aa1f0..b6f7d15e5 100644 --- a/aider/io.py +++ b/aider/io.py @@ -106,6 +106,7 @@ class InputOutput: tool_error_color="red", encoding="utf-8", dry_run=False, + llm_history_file=None, ): no_color = os.environ.get("NO_COLOR") if no_color is not None and no_color != "": @@ -125,6 +126,7 @@ class InputOutput: self.yes = yes self.input_history_file = input_history_file + self.llm_history_file = llm_history_file if chat_history_file is not None: self.chat_history_file = Path(chat_history_file) else: @@ -206,10 +208,11 @@ class InputOutput: else: style = None + completer_instance = AutoCompleter( + root, rel_fnames, addable_rel_fnames, commands, self.encoding + ) + while True: - completer_instance = AutoCompleter( - root, rel_fnames, addable_rel_fnames, commands, self.encoding - ) if multiline_input: show = ". " @@ -266,6 +269,14 @@ class InputOutput: fh = FileHistory(self.input_history_file) return fh.load_history_strings() + def log_llm_history(self, role, content): + if not self.llm_history_file: + return + timestamp = datetime.now().isoformat(timespec='seconds') + with open(self.llm_history_file, 'a', encoding=self.encoding) as log_file: + log_file.write(f"{role.upper()} {timestamp}\n") + log_file.write(content + "\n") + def user_input(self, inp, log_only=True): if not log_only: style = dict(style=self.user_input_color) if self.user_input_color else dict() diff --git a/aider/main.py b/aider/main.py index 9cf0dda10..2e878934c 100644 --- a/aider/main.py +++ b/aider/main.py @@ -258,6 +258,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F tool_error_color=args.tool_error_color, dry_run=args.dry_run, encoding=args.encoding, + llm_history_file=args.llm_history_file, ) fnames = [str(Path(fn).resolve()) for fn in args.files] From c40b3fa3106c6fee80a0163b8ee07c6d65c963f9 Mon Sep 17 00:00:00 2001 From: Daniel Vainsencher Date: Wed, 12 Jun 2024 18:28:55 -0400 Subject: [PATCH 7/7] Review comments --- aider/args.py | 4 ++-- aider/coders/base_coder.py | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/aider/args.py b/aider/args.py index 24682fa55..a96dc196d 100644 --- a/aider/args.py +++ b/aider/args.py @@ -23,8 +23,8 @@ def get_parser(default_config_files, git_root): group.add_argument( "--llm-history-file", metavar="LLM_HISTORY_FILE", - default=".aider.llm.history", - help="Specify the LLM history file (default: .aider.llm.history)", + default=None, + help="Log the conversation with the LLM to this file (for example, .aider.llm.history)", ) group.add_argument( "files", diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index a8ddf8830..9b2a9ed9a 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -1,5 +1,6 @@ #!/usr/bin/env python +import datetime import hashlib import json import os @@ -27,7 +28,7 @@ from aider.mdstream import MarkdownStream from aider.repo import GitRepo from aider.repomap import RepoMap from aider.sendchat import send_with_retries -from aider.utils import is_image_file +from aider.utils import is_image_file, format_messages, format_content from ..dump import dump # noqa: F401 @@ -779,9 +780,6 @@ class Coder: dict(role="user", content=inp), ] - import datetime - from aider.utils import format_messages, format_content - messages = self.format_messages() self.io.log_llm_history("TO LLM", format_messages(messages))