diff --git a/CHANGELOG.md b/CHANGELOG.md index 20b73c5..506f05a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ + +## Version 1.53.0 - 2025-11-10 + +Autonomous mode is now enabled by default, streamlining workflows. We've also improved the underlying code and fixed some issues with content extraction in autonomous mode. + +**Changes:** 15 files, 433 lines +**Languages:** Markdown (47 lines), Python (384 lines), TOML (2 lines) + ## Version 1.52.0 - 2025-11-10 This release updates the project version to 1.52.0. No new features or changes are introduced for users or developers. diff --git a/pyproject.toml b/pyproject.toml index 211e4ea..92ffffb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "rp" -version = "1.52.0" +version = "1.53.0" description = "R python edition. The ultimate autonomous AI CLI." readme = "README.md" requires-python = ">=3.10" diff --git a/rp/autonomous/mode.py b/rp/autonomous/mode.py index 61ef061..aebd39a 100644 --- a/rp/autonomous/mode.py +++ b/rp/autonomous/mode.py @@ -99,6 +99,9 @@ def run_autonomous_mode(assistant, task): except KeyboardInterrupt: logger.debug("Autonomous mode interrupted by user") print(f"\n{Colors.YELLOW}Autonomous mode interrupted by user{Colors.RESET}") + # Cancel the last API call and remove the user message to keep messages clean + if assistant.messages and assistant.messages[-1]["role"] == "user": + assistant.messages.pop() finally: assistant.autonomous_mode = False logger.debug("=== AUTONOMOUS MODE END ===") @@ -113,27 +116,29 @@ def process_response_autonomous(assistant, response): assistant.messages.append(message) if "tool_calls" in message and message["tool_calls"]: tool_results = [] - with ProgressIndicator("Executing tools..."): - for tool_call in message["tool_calls"]: - func_name = tool_call["function"]["name"] - arguments = json.loads(tool_call["function"]["arguments"]) - result = execute_single_tool(assistant, func_name, arguments) - if isinstance(result, str): - try: - result = json.loads(result) - except json.JSONDecodeError as ex: - result = {"error": str(ex)} - status = "success" if result.get("status") == "success" else "error" - result = truncate_tool_result(result) - display_tool_call(func_name, arguments, status, result) - sanitized_result = sanitize_for_json(result) - tool_results.append( - { - "tool_call_id": tool_call["id"], - "role": "tool", - "content": json.dumps(sanitized_result), - } - ) + for tool_call in message["tool_calls"]: + func_name = tool_call["function"]["name"] + arguments = json.loads(tool_call["function"]["arguments"]) + args_str = ", ".join([f"{k}={repr(v)}" for k, v in arguments.items()]) + if len(args_str) > 100: + args_str = args_str[:97] + "..." + print(f"{Colors.BLUE}⠋ Executing tools......{func_name}({args_str}){Colors.RESET}") + result = execute_single_tool(assistant, func_name, arguments) + if isinstance(result, str): + try: + result = json.loads(result) + except json.JSONDecodeError as ex: + result = {"error": str(ex)} + status = "success" if result.get("status") == "success" else "error" + result = truncate_tool_result(result) + sanitized_result = sanitize_for_json(result) + tool_results.append( + { + "tool_call_id": tool_call["id"], + "role": "tool", + "content": json.dumps(sanitized_result), + } + ) for result in tool_results: assistant.messages.append(result) with ProgressIndicator("Processing tool results..."): diff --git a/rp/core/assistant.py b/rp/core/assistant.py index 9085627..fc55c12 100644 --- a/rp/core/assistant.py +++ b/rp/core/assistant.py @@ -6,6 +6,7 @@ import readline import signal import sqlite3 import sys +import time import traceback from concurrent.futures import ThreadPoolExecutor @@ -99,7 +100,7 @@ class Assistant: "MODEL_LIST_URL", MODEL_LIST_URL ) self.use_tools = os.environ.get("USE_TOOLS", "1") == "1" - self.interrupt_count = 0 + self.last_interrupt_time = 0 self.python_globals = {} self.db_conn = None self.autonomous_mode = False @@ -238,6 +239,10 @@ class Assistant: func_name = tool_call["function"]["name"] arguments = json.loads(tool_call["function"]["arguments"]) logger.debug(f"Tool call: {func_name} with arguments: {arguments}") + args_str = ", ".join([f"{k}={repr(v)}" for k, v in arguments.items()]) + if len(args_str) > 100: + args_str = args_str[:97] + "..." + print(f"{Colors.BLUE}⠋ Executing tools......{func_name}({args_str}){Colors.RESET}") func_map = { "http_fetch": lambda **kw: http_fetch(**kw), "run_command": lambda **kw: run_command(**kw), @@ -351,22 +356,15 @@ class Assistant: return render_markdown(cleaned_content, self.syntax_highlighting) def signal_handler(self, signum, frame): - if self.autonomous_mode: - self.interrupt_count += 1 - if self.interrupt_count >= 2: - print(f"\n{Colors.RED}Force exiting autonomous mode...{Colors.RESET}") - self.autonomous_mode = False - sys.exit(0) - else: - print(f"\n{Colors.YELLOW}Press Ctrl+C again to force exit{Colors.RESET}") - return - self.interrupt_count += 1 - if self.interrupt_count >= 2: - print(f"\n{Colors.RED}Exiting...{Colors.RESET}") + current_time = time.time() + if current_time - self.last_interrupt_time < 1.0: + print(f"\n{Colors.RED}Force exiting...{Colors.RESET}") self.cleanup() sys.exit(0) else: - print(f"\n{Colors.YELLOW}Press Ctrl+C again to exit{Colors.RESET}") + self.last_interrupt_time = current_time + print(f"\n{Colors.YELLOW}Interrupted{Colors.RESET}") + raise KeyboardInterrupt def setup_readline(self): try: @@ -446,7 +444,8 @@ class Assistant: except EOFError: break except KeyboardInterrupt: - self.signal_handler(None, None) + print(f"\n{Colors.YELLOW}Interrupted, returning to prompt{Colors.RESET}") + continue except Exception as e: print(f"{Colors.RED}Error: {e}{Colors.RESET}") logging.error(f"REPL error: {e}\n{traceback.format_exc()}") diff --git a/rp/ui/progress.py b/rp/ui/progress.py index c62e847..ff9108d 100644 --- a/rp/ui/progress.py +++ b/rp/ui/progress.py @@ -10,6 +10,7 @@ class ProgressIndicator: self.show = show self.running = False self.thread = None + self.start_time = None def __enter__(self): if self.show: @@ -21,6 +22,7 @@ class ProgressIndicator: self.stop() def start(self): + self.start_time = time.time() self.running = True self.thread = threading.Thread(target=self._animate, daemon=True) self.thread.start() @@ -30,14 +32,15 @@ class ProgressIndicator: self.running = False if self.thread: self.thread.join(timeout=1.0) - sys.stdout.write("\r" + " " * (len(self.message) + 10) + "\r") + sys.stdout.write("\r" + " " * (len(self.message) + 20) + "\r") sys.stdout.flush() def _animate(self): spinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] idx = 0 while self.running: - sys.stdout.write(f"\r{spinner[idx]} {self.message}...") + elapsed = time.time() - self.start_time + sys.stdout.write(f"\r{spinner[idx]} {self.message}... ({elapsed:.1f}s)") sys.stdout.flush() idx = (idx + 1) % len(spinner) time.sleep(0.1)