diff --git a/aider/args.py b/aider/args.py index 716bca4f8..070905bb4 100644 --- a/aider/args.py +++ b/aider/args.py @@ -21,11 +21,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=None, + help="Log the conversation with the LLM to this file (for example, .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 b704841fa..255451e28 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 @@ -783,6 +784,8 @@ class Coder: messages = self.format_messages() + self.io.log_llm_history("TO LLM", format_messages(messages)) + if self.verbose: utils.show_messages(messages, functions=self.functions) @@ -805,6 +808,9 @@ class Coder: exhausted = True else: raise err + except Exception as err: + self.io.tool_error(f"Unexpected error: {err}") + return if exhausted: self.show_exhausted_error() @@ -824,6 +830,8 @@ class Coder: self.io.tool_output() + self.io.log_llm_history("LLM RESPONSE", format_content("ASSISTANT", content)) + if interrupted: content += "\n^C KeyboardInterrupt" self.cur_messages += [dict(role="assistant", content=content)] diff --git a/aider/io.py b/aider/io.py index 65093332c..1cf09f41f 100644 --- a/aider/io.py +++ b/aider/io.py @@ -107,6 +107,7 @@ class InputOutput: tool_error_color="red", encoding="utf-8", dry_run=False, + llm_history_file=None, editingmode=EditingMode.EMACS, ): self.editingmode = editingmode @@ -128,6 +129,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: @@ -209,10 +211,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 = ". " @@ -271,6 +274,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 79c1511d8..c4d211fc7 100644 --- a/aider/main.py +++ b/aider/main.py @@ -261,6 +261,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, editingmode=editing_mode, ) diff --git a/aider/utils.py b/aider/utils.py index 31767aec6..7636eb119 100644 --- a/aider/utils.py +++ b/aider/utils.py @@ -84,24 +84,38 @@ def safe_abs_path(res): return str(res) -def show_messages(messages, title=None, functions=None): +def format_content(role, content): + formatted_lines = [] + for line in content.splitlines(): + formatted_lines.append(f"{role} {line}") + return "\n".join(formatted_lines) + + +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(format_content(role, content)) 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)