diff --git a/examples/simple/cat.py b/examples/simple/cat.py index 14428e4..d559457 100644 --- a/examples/simple/cat.py +++ b/examples/simple/cat.py @@ -9,7 +9,7 @@ }, { "role": "system", - "content": "Today's date is 2024-02-14 13:32:19.\nThe current working directory is /Users/jakekoenig/rawdog, which IS a git repository.\nThe user's operating system is Darwin.\nThe contents of the current working directory are:\n2024-02-05 07:56:58 11339 bytes LICENSE\n2024-02-14 13:22:51 32 bytes requirements.txt\n2024-02-07 09:47:43 4 items /dist\n2024-02-14 13:22:51 630 bytes pyproject.toml\n2024-02-13 09:38:11 60746 bytes training_data.jsonl\n2024-02-05 07:56:58 1 items /rawdog\n2024-02-13 08:58:21 5247 bytes script_2024-02-13_08-58-21.py\n2024-02-14 08:47:33 308 bytes todo\n2024-02-13 01:40:23 3081 bytes README.md\n2024-02-05 07:57:14 45 bytes .gitignore\n2024-02-07 09:47:28 5 items /.env\n2024-02-14 13:26:01 13 items /examples\n2024-02-13 13:19:10 3 items /scripts\n2024-02-14 09:25:18 3380 bytes octagon.png\n2024-02-14 13:22:51 13 items /.git\n2024-02-09 02:29:04 58388 bytes old_training_data.jsonl\n2024-02-14 08:46:44 4 items /my_env\n2024-02-05 07:56:58 1 items /src\nThe last commit message is: Pip install not found modules (#61)\n\nIf the llm tries to use a module that cannot be found then prompt the\r\nuser to install it with pip.\r\nMisc:\r\n* Moved execution specific utils from utils to execute_script\r\n* Removed almost all the requirements\r\n* Give errors from the script to the llm (was broken by move to subprocess)\r\n\r\nCo-authored-by: biobootloader <128252497+biobootloader@users.noreply.github.com>\r\n\r\n---------\r\n\r\nCo-authored-by: biobootloader <128252497+biobootloader@users.noreply.github.com>" + "content": "Today's date is 2024-02-14 13:32:19.\nThe current working directory is /Users/jakekoenig/rawdog, which IS a git repository.\nThe user's operating system is Darwin.\nThe last commit message is: Pip install not found modules (#61)\n\nIf the llm tries to use a module that cannot be found then prompt the\r\nuser to install it with pip.\r\nMisc:\r\n* Moved execution specific utils from utils to execute_script\r\n* Removed almost all the requirements\r\n* Give errors from the script to the llm (was broken by move to subprocess)\r\n\r\nCo-authored-by: biobootloader <128252497+biobootloader@users.noreply.github.com>\r\n\r\n---------\r\n\r\nCo-authored-by: biobootloader <128252497+biobootloader@users.noreply.github.com>" }, { "role": "user", diff --git a/examples/simple/cwd.py b/examples/simple/cwd.py index 148ea75..f5c4596 100644 --- a/examples/simple/cwd.py +++ b/examples/simple/cwd.py @@ -9,7 +9,7 @@ }, { "role": "system", - "content": "Today's date is 2024-02-14 13:27:51.\nThe current working directory is /Users/jakekoenig/rawdog, which IS a git repository.\nThe user's operating system is Darwin.\nThe contents of the current working directory are:\n2024-02-05 07:56:58 11339 bytes LICENSE\n2024-02-14 13:22:51 32 bytes requirements.txt\n2024-02-07 09:47:43 4 items /dist\n2024-02-14 13:22:51 630 bytes pyproject.toml\n2024-02-13 09:38:11 60746 bytes training_data.jsonl\n2024-02-05 07:56:58 1 items /rawdog\n2024-02-13 08:58:21 5247 bytes script_2024-02-13_08-58-21.py\n2024-02-14 08:47:33 308 bytes todo\n2024-02-13 01:40:23 3081 bytes README.md\n2024-02-05 07:57:14 45 bytes .gitignore\n2024-02-07 09:47:28 5 items /.env\n2024-02-14 13:26:01 13 items /examples\n2024-02-13 13:19:10 3 items /scripts\n2024-02-14 09:25:18 3380 bytes octagon.png\n2024-02-14 13:22:51 13 items /.git\n2024-02-09 02:29:04 58388 bytes old_training_data.jsonl\n2024-02-14 08:46:44 4 items /my_env\n2024-02-05 07:56:58 1 items /src\nThe last commit message is: Pip install not found modules (#61)\n\nIf the llm tries to use a module that cannot be found then prompt the\r\nuser to install it with pip.\r\nMisc:\r\n* Moved execution specific utils from utils to execute_script\r\n* Removed almost all the requirements\r\n* Give errors from the script to the llm (was broken by move to subprocess)\r\n\r\nCo-authored-by: biobootloader <128252497+biobootloader@users.noreply.github.com>\r\n\r\n---------\r\n\r\nCo-authored-by: biobootloader <128252497+biobootloader@users.noreply.github.com>" + "content": "Today's date is 2024-02-14 13:27:51.\nThe current working directory is /Users/jakekoenig/rawdog, which IS a git repository.\nThe user's operating system is Darwin.\nThe last commit message is: Pip install not found modules (#61)\n\nIf the llm tries to use a module that cannot be found then prompt the\r\nuser to install it with pip.\r\nMisc:\r\n* Moved execution specific utils from utils to execute_script\r\n* Removed almost all the requirements\r\n* Give errors from the script to the llm (was broken by move to subprocess)\r\n\r\nCo-authored-by: biobootloader <128252497+biobootloader@users.noreply.github.com>\r\n\r\n---------\r\n\r\nCo-authored-by: biobootloader <128252497+biobootloader@users.noreply.github.com>" }, { "role": "user", diff --git a/examples/simple/grep.py b/examples/simple/grep.py index 0ac3121..9f57295 100644 --- a/examples/simple/grep.py +++ b/examples/simple/grep.py @@ -9,7 +9,7 @@ }, { "role": "system", - "content": "Today's date is 2024-02-14 13:28:17.\nThe current working directory is /Users/jakekoenig/rawdog, which IS a git repository.\nThe user's operating system is Darwin.\nThe contents of the current working directory are:\n2024-02-05 07:56:58 11339 bytes LICENSE\n2024-02-14 13:22:51 32 bytes requirements.txt\n2024-02-07 09:47:43 4 items /dist\n2024-02-14 13:22:51 630 bytes pyproject.toml\n2024-02-13 09:38:11 60746 bytes training_data.jsonl\n2024-02-05 07:56:58 1 items /rawdog\n2024-02-13 08:58:21 5247 bytes script_2024-02-13_08-58-21.py\n2024-02-14 08:47:33 308 bytes todo\n2024-02-13 01:40:23 3081 bytes README.md\n2024-02-05 07:57:14 45 bytes .gitignore\n2024-02-07 09:47:28 5 items /.env\n2024-02-14 13:26:01 13 items /examples\n2024-02-13 13:19:10 3 items /scripts\n2024-02-14 09:25:18 3380 bytes octagon.png\n2024-02-14 13:22:51 13 items /.git\n2024-02-09 02:29:04 58388 bytes old_training_data.jsonl\n2024-02-14 08:46:44 4 items /my_env\n2024-02-05 07:56:58 1 items /src\nThe last commit message is: Pip install not found modules (#61)\n\nIf the llm tries to use a module that cannot be found then prompt the\r\nuser to install it with pip.\r\nMisc:\r\n* Moved execution specific utils from utils to execute_script\r\n* Removed almost all the requirements\r\n* Give errors from the script to the llm (was broken by move to subprocess)\r\n\r\nCo-authored-by: biobootloader <128252497+biobootloader@users.noreply.github.com>\r\n\r\n---------\r\n\r\nCo-authored-by: biobootloader <128252497+biobootloader@users.noreply.github.com>" + "content": "Today's date is 2024-02-14 13:28:17.\nThe current working directory is /Users/jakekoenig/rawdog, which IS a git repository.\nThe user's operating system is Darwin.\nThe last commit message is: Pip install not found modules (#61)\n\nIf the llm tries to use a module that cannot be found then prompt the\r\nuser to install it with pip.\r\nMisc:\r\n* Moved execution specific utils from utils to execute_script\r\n* Removed almost all the requirements\r\n* Give errors from the script to the llm (was broken by move to subprocess)\r\n\r\nCo-authored-by: biobootloader <128252497+biobootloader@users.noreply.github.com>\r\n\r\n---------\r\n\r\nCo-authored-by: biobootloader <128252497+biobootloader@users.noreply.github.com>" }, { "role": "user", diff --git a/examples/simple/ls.py b/examples/simple/ls.py index 3aa6947..f7aa055 100644 --- a/examples/simple/ls.py +++ b/examples/simple/ls.py @@ -9,7 +9,7 @@ }, { "role": "system", - "content": "Today's date is 2024-02-14 13:26:11.\nThe current working directory is /Users/jakekoenig/rawdog, which IS a git repository.\nThe user's operating system is Darwin.\nThe contents of the current working directory are:\n2024-02-05 07:56:58 11339 bytes LICENSE\n2024-02-14 13:22:51 32 bytes requirements.txt\n2024-02-07 09:47:43 4 items /dist\n2024-02-14 13:22:51 630 bytes pyproject.toml\n2024-02-13 09:38:11 60746 bytes training_data.jsonl\n2024-02-05 07:56:58 1 items /rawdog\n2024-02-13 08:58:21 5247 bytes script_2024-02-13_08-58-21.py\n2024-02-14 08:47:33 308 bytes todo\n2024-02-13 01:40:23 3081 bytes README.md\n2024-02-05 07:57:14 45 bytes .gitignore\n2024-02-07 09:47:28 5 items /.env\n2024-02-14 13:26:01 13 items /examples\n2024-02-13 13:19:10 3 items /scripts\n2024-02-14 09:25:18 3380 bytes octagon.png\n2024-02-14 13:22:51 13 items /.git\n2024-02-09 02:29:04 58388 bytes old_training_data.jsonl\n2024-02-14 08:46:44 4 items /my_env\n2024-02-05 07:56:58 1 items /src\nThe last commit message is: Pip install not found modules (#61)\n\nIf the llm tries to use a module that cannot be found then prompt the\r\nuser to install it with pip.\r\nMisc:\r\n* Moved execution specific utils from utils to execute_script\r\n* Removed almost all the requirements\r\n* Give errors from the script to the llm (was broken by move to subprocess)\r\n\r\nCo-authored-by: biobootloader <128252497+biobootloader@users.noreply.github.com>\r\n\r\n---------\r\n\r\nCo-authored-by: biobootloader <128252497+biobootloader@users.noreply.github.com>" + "content": "Today's date is 2024-02-14 13:26:11.\nThe current working directory is /Users/jakekoenig/rawdog, which IS a git repository.\nThe user's operating system is Darwin.\nThe last commit message is: Pip install not found modules (#61)\n\nIf the llm tries to use a module that cannot be found then prompt the\r\nuser to install it with pip.\r\nMisc:\r\n* Moved execution specific utils from utils to execute_script\r\n* Removed almost all the requirements\r\n* Give errors from the script to the llm (was broken by move to subprocess)\r\n\r\nCo-authored-by: biobootloader <128252497+biobootloader@users.noreply.github.com>\r\n\r\n---------\r\n\r\nCo-authored-by: biobootloader <128252497+biobootloader@users.noreply.github.com>" }, { "role": "user", diff --git a/examples/simple/math.py b/examples/simple/math.py index 4a772f1..2dd9a88 100644 --- a/examples/simple/math.py +++ b/examples/simple/math.py @@ -9,7 +9,7 @@ }, { "role": "system", - "content": "Today's date is 2024-02-14 13:33:57.\nThe current working directory is /Users/jakekoenig/rawdog, which IS a git repository.\nThe user's operating system is Darwin.\nThe contents of the current working directory are:\n2024-02-05 07:56:58 11339 bytes LICENSE\n2024-02-14 13:22:51 32 bytes requirements.txt\n2024-02-07 09:47:43 4 items /dist\n2024-02-14 13:22:51 630 bytes pyproject.toml\n2024-02-13 09:38:11 60746 bytes training_data.jsonl\n2024-02-05 07:56:58 1 items /rawdog\n2024-02-13 08:58:21 5247 bytes script_2024-02-13_08-58-21.py\n2024-02-14 08:47:33 308 bytes todo\n2024-02-13 01:40:23 3081 bytes README.md\n2024-02-05 07:57:14 45 bytes .gitignore\n2024-02-07 09:47:28 5 items /.env\n2024-02-14 13:26:01 13 items /examples\n2024-02-13 13:19:10 3 items /scripts\n2024-02-14 09:25:18 3380 bytes octagon.png\n2024-02-14 13:22:51 13 items /.git\n2024-02-09 02:29:04 58388 bytes old_training_data.jsonl\n2024-02-14 08:46:44 4 items /my_env\n2024-02-05 07:56:58 1 items /src\nThe last commit message is: Pip install not found modules (#61)\n\nIf the llm tries to use a module that cannot be found then prompt the\r\nuser to install it with pip.\r\nMisc:\r\n* Moved execution specific utils from utils to execute_script\r\n* Removed almost all the requirements\r\n* Give errors from the script to the llm (was broken by move to subprocess)\r\n\r\nCo-authored-by: biobootloader <128252497+biobootloader@users.noreply.github.com>\r\n\r\n---------\r\n\r\nCo-authored-by: biobootloader <128252497+biobootloader@users.noreply.github.com>" + "content": "Today's date is 2024-02-14 13:33:57.\nThe current working directory is /Users/jakekoenig/rawdog, which IS a git repository.\nThe user's operating system is Darwin.\nThe last commit message is: Pip install not found modules (#61)\n\nIf the llm tries to use a module that cannot be found then prompt the\r\nuser to install it with pip.\r\nMisc:\r\n* Moved execution specific utils from utils to execute_script\r\n* Removed almost all the requirements\r\n* Give errors from the script to the llm (was broken by move to subprocess)\r\n\r\nCo-authored-by: biobootloader <128252497+biobootloader@users.noreply.github.com>\r\n\r\n---------\r\n\r\nCo-authored-by: biobootloader <128252497+biobootloader@users.noreply.github.com>" }, { "role": "user", diff --git a/pyproject.toml b/pyproject.toml index 3740134..85c0703 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ packages=["src/rawdog"] [project] name = "rawdog-ai" -version = "0.1.5" +version = "0.1.6" description = "An AI command-line assistant" readme = "README.md" dependencies = [ diff --git a/src/rawdog/__init__.py b/src/rawdog/__init__.py index 1276d02..0a8da88 100644 --- a/src/rawdog/__init__.py +++ b/src/rawdog/__init__.py @@ -1 +1 @@ -__version__ = "0.1.5" +__version__ = "0.1.6" diff --git a/src/rawdog/__main__.py b/src/rawdog/__main__.py index 28e595d..8e4d7dd 100644 --- a/src/rawdog/__main__.py +++ b/src/rawdog/__main__.py @@ -9,48 +9,42 @@ def rawdog(prompt: str, config, llm_client): + llm_client.add_message("user", prompt) leash = config.get("leash") retries = int(config.get("retries")) _continue = True - _first = True while _continue is True: + _continue = False error, script, output, return_code = "", "", "", 0 try: - if _first: - message, script = llm_client.get_script(prompt, stream=leash) - _first = False - else: - message, script = llm_client.get_script(stream=leash) + if leash: + print(80 * "-") + message, script = llm_client.get_script() if script: if leash: - print(f"\n{80 * '-'}") - if ( - input("Execute script in markdown block? (Y/n): ") - .strip() - .lower() - == "n" - ): + _ok = input( + f"\n{38 * '-'} Execute script in markdown block? (Y/n):" + ) + if _ok.strip().lower() == "n": llm_client.add_message("user", "User chose not to run script") break output, error, return_code = execute_script(script, llm_client) - elif message: + elif not leash and message: print(message) except KeyboardInterrupt: break - _continue = (output and output.strip().endswith("CONTINUE")) or ( - return_code != 0 and error and retries > 0 - ) - if error: - retries -= 1 - llm_client.add_message("user", f"Error: {error}") - print(f"Error: {error}") - if script and not leash: - print(f"{80 * '-'}\n{script}\n{80 * '-'}") if output: llm_client.add_message("user", f"LAST SCRIPT OUTPUT:\n{output}") - if leash or not _continue: - print(output) + if output.endswith("CONTINUE"): + _continue = True + if error: + llm_client.add_message("user", f"Error: {error}") + if return_code != 0: + retries -= 1 + if retries > 0: + print("Retrying...\n") + _continue = True def banner(config): diff --git a/src/rawdog/execute_script.py b/src/rawdog/execute_script.py index 01e33fc..e6b474c 100644 --- a/src/rawdog/execute_script.py +++ b/src/rawdog/execute_script.py @@ -36,39 +36,71 @@ def install_pip_packages(*packages: str): ) -def execute_script(script: str, llm_client) -> tuple[str, str, int]: - python_executable = get_rawdog_python_executable() - with tempfile.NamedTemporaryFile(mode="w+", delete=False) as tmp_script: - tmp_script_name = tmp_script.name - tmp_script.write(script) - tmp_script.flush() +def _execute_script_in_subprocess(script) -> tuple[str, str, int]: + """Write script to tempfile, execute from .rawdog/venv, stream and return output""" + output, error, return_code = "", "", 0 + try: + python_executable = get_rawdog_python_executable() + with tempfile.NamedTemporaryFile(mode="w+", delete=False) as tmp_script: + tmp_script_name = tmp_script.name + tmp_script.write(script) + tmp_script.flush() - retry = True - while retry: - retry = False - result = subprocess.run( - [python_executable, tmp_script_name], capture_output=True, text=True + process = subprocess.Popen( + [python_executable, tmp_script_name], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + stdin=subprocess.DEVNULL, # Raises EOF error if subprocess asks for input + text=True, ) - output = result.stdout - error = result.stderr - return_code = result.returncode - if error and "ModuleNotFoundError: No module named" in error: - match = re.search(r"No module named '(\w+)'", error) - if match: - module = match.group(1) - module_name = llm_client.get_python_package(module) - if ( - input( - f"Rawdog wants to use {module_name}. Install to rawdog's" - " venv with pip? (Y/n): " - ) - .strip() - .lower() - != "n" - ): - install_result = install_pip_packages(module_name) - if install_result.returncode == 0: - retry = True - else: - print("Failed to install package") + while True: + _stdout = process.stdout.readline() + _stderr = process.stderr.readline() + if _stdout: + output += _stdout + print(_stdout, end="") + if _stderr: + error += _stderr + print(_stderr, end="", file=sys.stderr) + if _stdout == "" and _stderr == "" and process.poll() is not None: + break + return_code = process.returncode + except Exception as e: + error += str(e) + print(e) + return_code = 1 + return output, error, return_code + + +def _execute_script_with_dependency_resolution( + script, llm_client +) -> tuple[str, str, int]: + retry = True + output, error, return_code = "", "", 0 + while retry: + retry = False + output, error, return_code = _execute_script_in_subprocess(script) + if error and "ModuleNotFoundError: No module named" in error: + match = re.search(r"No module named '(\w+)'", error) + if match: + module = match.group(1) + module_name = llm_client.get_python_package(module) + if ( + input( + f"Rawdog wants to use {module_name}. Install to rawdog's" + " venv with pip? (Y/n): " + ) + .strip() + .lower() + != "n" + ): + install_result = install_pip_packages(module_name) + if install_result.returncode == 0: + retry = True + else: + print("Failed to install package") return output, error, return_code + + +def execute_script(script: str, llm_client) -> tuple[str, str, int]: + return _execute_script_with_dependency_resolution(script, llm_client) diff --git a/src/rawdog/llm_client.py b/src/rawdog/llm_client.py index 3418654..12468f3 100644 --- a/src/rawdog/llm_client.py +++ b/src/rawdog/llm_client.py @@ -1,7 +1,6 @@ import json import os from textwrap import dedent -from typing import Optional from litellm import completion, completion_cost @@ -75,15 +74,14 @@ def get_python_package(self, import_name: str): return response.choices[0].message.content - def get_script(self, prompt: Optional[str] = None, stream=False): - if prompt: - self.conversation.append({"role": "user", "content": prompt}) + def get_script(self): messages = self.conversation.copy() base_url = self.config.get("llm_base_url") model = self.config.get("llm_model") temperature = self.config.get("llm_temperature") custom_llm_provider = self.config.get("llm_custom_provider") + stream = self.config.get("leash") log = { "model": model, diff --git a/src/rawdog/parsing.py b/src/rawdog/parsing.py index aaef9df..0bbc00f 100644 --- a/src/rawdog/parsing.py +++ b/src/rawdog/parsing.py @@ -10,7 +10,7 @@ def parse_script(response: str) -> tuple[str, str]: # Parse delimiter n_delimiters = response.count("```") if n_delimiters < 2: - return f"Error: No script found in response:\n{response}", "" + return response, "" segments = response.split("```") message = f"{segments[0]}\n{segments[-1]}" script = "```".join(segments[1:-1]).strip() # Leave 'inner' delimiters alone diff --git a/src/rawdog/prompts.py b/src/rawdog/prompts.py index 020b9b9..2605c7f 100644 --- a/src/rawdog/prompts.py +++ b/src/rawdog/prompts.py @@ -54,7 +54,7 @@ - Actively clean up any temporary processes or files you use. - When looking through files, use git as available to skip files, and skip hidden files (.env, .git, etc) by default. - Let exceptions propagate to the user (rather than catching them in your SCRIPT) so that you can retry. -- At the user's request, you can inspect and update your configuration file: ~/.rawdog/config.yaml. Changes will take effect after restarting. +- At the user's request, you can inspect and update your configuration file: ~/.rawdog/config.yaml. Changes will take effect after restarting. "Your leash" refers to config.leash, which you can 'put on' (set to true) or take off. - Feel free to use any common python packages. For example matplotlib, beautifulsoup4, numpy. If the user doesn't have them installed they will be installed automatically with user confirmation. - ALWAYS Return your SCRIPT inside of a single pair of ``` delimiters. Only the console output of the first such SCRIPT is visible to the user, so make sure that it's complete and don't bother returning anything else. """ diff --git a/src/rawdog/utils.py b/src/rawdog/utils.py index 587c99c..4ba25fd 100644 --- a/src/rawdog/utils.py +++ b/src/rawdog/utils.py @@ -22,11 +22,10 @@ def __init__(self, config=None, data=None): def _set_from_dict(self, data): """Used when preparing fine-tuning examples""" - self.data = data["date"] + self.date = data["date"] self.cwd = data["cwd"] self.os = data["os"] self.is_git = data["is_git"] - self.cwd_info = data["cwd_info"] self.last_commit = data["last_commit"] self.retries = data["retries"] @@ -34,11 +33,11 @@ def _set_from_env(self): self.date = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") self.cwd = Path.cwd() self.os = platform.system() - self.is_git = "IS" if Path(".git").exists() else "is NOT" - self.cwd_info = self._get_cwd_info() + _is_git = Path(".git").exists() + self.is_git = "IS" if _is_git else "is NOT" self.last_commit = ( "" - if not self.is_git + if not _is_git else "\nThe last commit message is: " + ( subprocess.run( @@ -51,40 +50,15 @@ def _set_from_env(self): _retries = 0 if self.config is None else self.config.get("retries") self.retries = f"\nYou'll get {_retries} retries." - def _get_cwd_info(self, max_items=100): - output = [] - for i, item in enumerate(self.cwd.iterdir()): - if i >= max_items: - break - name = ("" if not item.is_dir() else "/") + item.name - last_modified = datetime.datetime.fromtimestamp( - item.stat().st_mtime - ).strftime("%Y-%m-%d %H:%M:%S") - size, unit = 0, "" - try: - size = ( - len(list(item.iterdir())) if item.is_dir() else item.stat().st_size - ) - unit = " bytes" if item.is_file() else " items" - except Exception: - pass - output.append(f"{last_modified} {size:10}{unit} {name}") - if not output: - return "The directory is empty." - return "\n".join(output) - def render_prompt(self): return """\ Today's date is {date}. The current working directory is {cwd}, which {is_git} a git repository. -The user's operating system is {os}. -The contents of the current working directory are: -{cwd_info}{last_commit}{retries}""".format( +The user's operating system is {os}.{last_commit}{retries}""".format( date=self.date, cwd=self.cwd, is_git=self.is_git, os=self.os, - cwd_info=self.cwd_info, last_commit=self.last_commit, retries=self.retries, )