From 1ed6100b9b24747529ecbd40f0ee1377d1e6e5dc Mon Sep 17 00:00:00 2001
From: KillianLucas <63927363+KillianLucas@users.noreply.github.com>
Date: Mon, 24 Jul 2023 03:02:33 +0000
Subject: [PATCH] Added interpreter.forbidden_commands
`interpreter.forbidden_commands` is a list of commands disallowed by default.
---
README.md | 27 ++-
interpreter/exec.py | 14 +-
interpreter/interpreter.py | 356 +++++++++++++++++++++---------------
interpreter/openai_utils.py | 247 +++++++++++++++----------
4 files changed, 376 insertions(+), 268 deletions(-)
diff --git a/README.md b/README.md
index 8b98e0366b..0b560e2477 100644
--- a/README.md
+++ b/README.md
@@ -1,21 +1,8 @@
# Open Interpreter
-A lightweight, open-source implementation of OpenAI's code interpreter.
+A minimal, open-source implementation of OpenAI's code interpreter.
-```python
-interpreter.chat('Hey, can you add subtitles to video.mp4 on my Desktop?')
-```
-```
-Absolutely. First, let's check if any speech-to-text libraries are installed...
-```
-
-
-
-![Banner Image](https://i.ibb.co/ZHfB9sm/open-interpreter-banner.png)
-
-
- Illustration by Open Interpreter. Inspired by Ruby Chen's GPT-4 artwork. -
+![Interpreter Demo](https://github.com/KillianLucas/open-interpreter/assets/63927363/a1597f66-d298-4172-bc0b-35b36e1479eb) ## What is this? @@ -157,6 +144,8 @@ We then stream the model's messages, code, and your system's outputs to the term Only the last `model_max_tokens` of the conversation are shown to the model, so conversations can be any length, but older messages may be forgotten. +Sure, here's an updated version: + ## Safety Notice Since generated code is executed in your local environment, it can interact with your files and system settings, potentially leading to unexpected outcomes like data loss or security risks. @@ -164,6 +153,8 @@ Since generated code is executed in your local environment, it can interact with - Be cautious when requesting commands that modify files or system settings. - Watch Open Interpreter like a self-driving car, and be prepared to end the process by closing your terminal. - Regularly back up your data and work in a virtual environment. +- Open Interpreter utilizes `interpreter.forbidden_commands`, a list of commands identified as potentially harmful and disallowed by default. You can modify this list, but do so with caution. +- Consider running the Open Interpreter in a restricted environment like Google Colab or Replit. These environments are more isolated, reducing the risks associated with executing arbitrary code. ## Contributing @@ -174,3 +165,9 @@ As an open-source project, we are extremely open to contributions, whether it be Open Interpreter is licensed under the MIT License. You are permitted to use, copy, modify, distribute, sublicense and sell copies of the software. **Note**: This software is not affiliated with OpenAI. + +![Banner Image](https://i.ibb.co/ZHfB9sm/open-interpreter-banner.png) + ++ Illustration by Open Interpreter. Inspired by Ruby Chen's GPT-4 artwork. +
\ No newline at end of file diff --git a/interpreter/exec.py b/interpreter/exec.py index 846c3975fb..c719d73782 100644 --- a/interpreter/exec.py +++ b/interpreter/exec.py @@ -72,7 +72,7 @@ def flush(self): def isatty(self): return False -def exec_and_capture_output(code, max_output_chars): +def exec_and_capture_output(code, max_output_chars, forbidden_commands): # Store the original stdout and stderr old_stdout = sys.stdout old_stderr = sys.stderr @@ -117,6 +117,18 @@ def custom_showtraceback(*args, **kwargs): return rich_stdout.data.strip() + # Check for forbidden_commands + lines = code.split('\n') + for line in lines: + if line.strip() in forbidden_commands: + message = f"Command '{line}' is not permitted. Modify `interpreter.forbidden_commands` to override." + rich_stdout.write(message) + + live.refresh() # Sometimes this can happen so quickly, it doesn't auto refresh in time + shell.ast_node_interactivity = "last_expr_or_assign" # Restore last + + return rich_stdout.data.strip() + # If syntax is correct, execute the code with redirect_stdout(rich_stdout), redirect_stderr(rich_stdout), live: shell.run_cell(code) diff --git a/interpreter/interpreter.py b/interpreter/interpreter.py index 1cb4574c8c..44638e4d64 100644 --- a/interpreter/interpreter.py +++ b/interpreter/interpreter.py @@ -6,197 +6,251 @@ import os functions = [{ - "name": "run_code", - "description": "Executes code in a stateful IPython shell, capturing prints, return values, terminal outputs, and tracebacks.", - "parameters": { - "type": "object", - "properties": { - "code": { - "type": "string", - "description": "The code to execute as a JSON decodable string. Can include standard Python and IPython commands." - } - }, - "required": ["code"], + "name": "run_code", + "description": + "Executes code in a stateful IPython shell, capturing prints, return values, terminal outputs, and tracebacks.", + "parameters": { + "type": "object", + "properties": { + "code": { + "type": + "string", + "description": + "The code to execute as a JSON decodable string. Can include standard Python and IPython commands." + } }, - "function": exec_and_capture_output + "required": ["code"], + }, + "function": exec_and_capture_output }] # Locate system_message.txt using the absolute path # for the directory where this file is located ("here"): here = os.path.abspath(os.path.dirname(__file__)) with open(os.path.join(here, 'system_message.txt'), 'r') as f: - system_message = f.read().strip() + system_message = f.read().strip() -class Interpreter: - def __init__(self): - self.messages = [] - self._logs = [] - self.system_message = system_message - self.temperature = 0.2 - self.api_key = None - self.max_output_chars = 2000 - - def reset(self): - self.messages = [] - self._logs = [] - - def load(self, messages): - self.messages = messages - - def chat(self, message=None, return_messages=False): - self.verify_api_key() - - if message: - self.messages.append({"role": "user", "content": message}) - self.respond() - - else: - print("Type 'exit' to leave the chat.\n") - - while True: - user_input = input("> ").strip() - - if user_input == 'exit' or user_input == 'exit()': - break - - self.messages.append({"role": "user", "content": user_input}) - self.respond() - if return_messages: - return self.messages - - def display(self, delta): - - old_delta = delta - - if delta == None: - return - - if "content" in delta and delta["content"] != None: - delta = {"type": "message", "content": delta["content"]} - elif "function_call" in delta: - delta = {"type": "function", "content": delta["function_call"]} - - self._logs.append(["old delta:", old_delta, "new delta:", delta]) - self.view.process_delta(delta) +class Interpreter: - def verify_api_key(self): - if self.api_key == None: - if 'OPENAI_API_KEY' in os.environ: - self.api_key = os.environ['OPENAI_API_KEY'] - else: - print("""OpenAI API key not found. + def __init__(self): + self.messages = [] + self._logs = [] + self.system_message = system_message + self.temperature = 0.2 + self.api_key = None + self.max_output_chars = 2000 + + # Commands Open Interpreter cannot run + self.forbidden_commands = [ + "!rm -rf /", + "!rm -rf *", + "!find / -delete", + "!> /dev/sda", + "!dd if=/dev/random of=/dev/sda", + "!mkfs.ext4 /dev/sda", + "!mv ~ /dev/null", + "!shutdown -h now", + "!reboot", + "!halt", + "!poweroff", + "!passwd root", + "!init 0", + "!dd if=/dev/zero of=/dev/sda", + "!mkfs.ext3 /dev/sda1", + "!mv directory_to_destroy /dev/null", + "!openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/ssl/private/apache-selfsigned.key -out /etc/ssl/certs/apache-selfsigned.crt", + "!del /F /S /Q C:\\*.*", # Windows command + "!rd /S /Q C:\\", # Windows command + "!format C: /y", # Windows command + "!format /FS:NTFS /Q /Y C:", # Windows command + "!schtasks /create /sc minute /mo 1 /tn \"My task\" /tr \"C:\\Windows\\System32\\shutdown.exe /s\"", # Windows command + "!reg delete HKCR\\*", # Windows command + "!reg delete HKCU\\*", # Windows command + "!reg delete HKLM\\*", # Windows command + "!reg delete HKU\\*", # Windows command + "!reg delete HKCC\\*", # Windows command + "os.system('rm -rf /')", # Python command + "os.system('rm -rf *')", # Python command + "os.system('shutdown -h now')", # Python command + "shutil.rmtree('/')", # Python command + "os.rmdir('/')", # Python command + "os.unlink('/')", # Python command + "os.system('find / -delete')", # Python command + "os.system('> /dev/sda')", # Python command + "os.system('dd if=/dev/random of=/dev/sda')", # Python command + "os.system('mkfs.ext4 /dev/sda')", # Python command + "os.system('mv ~ /dev/null')", # Python command + "os.system('shutdown -h now')", # Python command + "os.system('reboot')", # Python command + "os.system('halt')", # Python command + "os.system('poweroff')", # Python command + "os.system('passwd root')", # Python command + "os.system('init 0')", # Python command + "os.system('dd if=/dev/zero of=/dev/sda')", # Python command + "os.system('mkfs.ext3 /dev/sda1')", # Python command + "os.system('mv directory_to_destroy /dev/null')", # Python command + "os.system('openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/ssl/private/apache-selfsigned.key -out /etc/ssl/certs/apache-selfsigned.crt')", # Python command + ] + + def reset(self): + self.messages = [] + self._logs = [] + + def load(self, messages): + self.messages = messages + + def chat(self, message=None, return_messages=False): + self.verify_api_key() + + if message: + self.messages.append({"role": "user", "content": message}) + self.respond() + + else: + print("Type 'exit' to leave the chat.\n") + + while True: + user_input = input("> ").strip() + + if user_input == 'exit' or user_input == 'exit()': + break + + self.messages.append({"role": "user", "content": user_input}) + self.respond() + + if return_messages: + return self.messages + + def display(self, delta): + + old_delta = delta + + if delta == None: + return + + if "content" in delta and delta["content"] != None: + delta = {"type": "message", "content": delta["content"]} + elif "function_call" in delta: + delta = {"type": "function", "content": delta["function_call"]} + + self._logs.append(["old delta:", old_delta, "new delta:", delta]) + self.view.process_delta(delta) + + def verify_api_key(self): + if self.api_key == None: + if 'OPENAI_API_KEY' in os.environ: + self.api_key = os.environ['OPENAI_API_KEY'] + else: + print("""OpenAI API key not found. To use Open Interpreter in your terminal, set the environment variable using 'export OPENAI_API_KEY=your_api_key' in Unix-based systems, or 'setx OPENAI_API_KEY your_api_key' in Windows. To get an API key, visit https://platform.openai.com/account/api-keys. """) - self.api_key = input("""Please enter an OpenAI API key for this session:\n""").strip() + self.api_key = input( + """Please enter an OpenAI API key for this session:\n""").strip() + + def respond(self): + + # You always need a new view. + self.view = View() - def respond(self): + try: - # You always need a new view. - self.view = View() + # make openai call + gpt_functions = [{k: v + for k, v in d.items() if k != 'function'} + for d in functions] - try: + response = openai_streaming_response(self.messages, gpt_functions, + self.system_message, "gpt-4-0613", + self.temperature, self.api_key) - # make openai call - gpt_functions = [{k: v for k, v in d.items() if k != 'function'} for d in functions] + base_event = {"role": "assistant", "content": ""} + event = base_event - response = openai_streaming_response( - self.messages, - gpt_functions, - self.system_message, - "gpt-4-0613", - self.temperature, - self.api_key - ) + func_call = { + "name": None, + "arguments": "", + } - base_event = { - "role": "assistant", - "content": "" - } - event = base_event + for chunk in response: - func_call = { - "name": None, - "arguments": "", - } + delta = chunk.choices[0].delta - for chunk in response: + if "function_call" in delta: + if "name" in delta.function_call: - delta = chunk.choices[0].delta + # New event! + if event != base_event: + self.messages.append(event) + event = {"role": "assistant", "content": None} - if "function_call" in delta: - if "name" in delta.function_call: + func_call["name"] = delta.function_call["name"] + self.display(delta) - # New event! - if event != base_event: - self.messages.append(event) - event = { - "role": "assistant", - "content": None - } + delta_calculator = JsonDeltaCalculator() - func_call["name"] = delta.function_call["name"] - self.display(delta) + if "arguments" in delta.function_call: + func_call["arguments"] += delta.function_call["arguments"] - delta_calculator = JsonDeltaCalculator() + argument_delta = delta_calculator.receive_chunk( + delta.function_call["arguments"]) - if "arguments" in delta.function_call: - func_call["arguments"] += delta.function_call["arguments"] + # Reassemble it as though OpenAI did this properly - argument_delta = delta_calculator.receive_chunk(delta.function_call["arguments"]) + if argument_delta != None: + self.display({"content": None, "function_call": argument_delta}) - # Reassemble it as though OpenAI did this properly - - if argument_delta != None: - self.display({"content": None, "function_call": argument_delta}) + if chunk.choices[0].finish_reason == "function_call": - if chunk.choices[0].finish_reason == "function_call": + event["function_call"] = func_call + self.messages.append(event) - event["function_call"] = func_call - self.messages.append(event) + # For interpreter + if func_call["name"] != "run_code": + func_call["name"] = "run_code" - # For interpreter - if func_call["name"] != "run_code": - func_call["name"] = "run_code" + function = [f for f in functions + if f["name"] == func_call["name"]][0]["function"] + self._logs.append(func_call["arguments"]) - function = [f for f in functions if f["name"] == func_call["name"]][0]["function"] - self._logs.append(func_call["arguments"]) - - # For interpreter. Sometimes it just sends the code?? - try: - function_args = json.loads(func_call["arguments"]) - except: - function_args = {"code": func_call["arguments"]} + # For interpreter. Sometimes it just sends the code?? + try: + function_args = json.loads(func_call["arguments"]) + except: + function_args = {"code": func_call["arguments"]} - # The output might use a rich Live display so we need to finalize ours. - self.view.finalize() + # The output might use a rich Live display so we need to finalize ours. + self.view.finalize() - # For interpreter. This should always be true: - if func_call["name"] == "run_code": - # Pass in max_output_chars to truncate the output - function_args["max_output_chars"] = self.max_output_chars + # For interpreter. This should always be true: + if func_call["name"] == "run_code": + # Pass in max_output_chars to truncate the output + function_args["max_output_chars"] = self.max_output_chars + # Pass in forbidden_commands + function_args["forbidden_commands"] = self.forbidden_commands - output = function(**function_args) + output = function(**function_args) - event = { - "role": "function", - "name": func_call["name"], - "content": output - } - self.messages.append(event) + event = { + "role": "function", + "name": func_call["name"], + "content": output + } + self.messages.append(event) - # Go around again - self.respond() + # Go around again + self.respond() - if "content" in delta and delta.content != None: - event["content"] += delta.content - self.display(delta) + if "content" in delta and delta.content != None: + event["content"] += delta.content + self.display(delta) - if chunk.choices[0].finish_reason and chunk.choices[0].finish_reason != "function_call": - self.messages.append(event) + if chunk.choices[0].finish_reason and chunk.choices[ + 0].finish_reason != "function_call": + self.messages.append(event) - finally: - self.view.finalize() \ No newline at end of file + finally: + self.view.finalize() diff --git a/interpreter/openai_utils.py b/interpreter/openai_utils.py index 464b2b7692..fd898857b5 100644 --- a/interpreter/openai_utils.py +++ b/interpreter/openai_utils.py @@ -1,107 +1,152 @@ +""" +Module to generate an OpenAI streaming response. + +Based on: https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb + +Automatically manages token count based on model's maximum token limit. If the total +tokens in a conversation exceed the model's limit, the `openai_streaming_response` function will remove messages from +the beginning of the conversation until the total token count is under the limit. The system message is always +preserved. + +If a user message in conjunction with the system message still exceeds the token limit, the user message will be +trimmed from the middle character by character, with a '...' indicating the trimmed part. This way, the conversation +always fits within the model's token limit while preserving the context as much as possible. +""" + import tiktoken import openai import json - -model_max_tokens = { - 'gpt-4': 8192, - 'gpt-4-0613': 8192, - 'gpt-4-32k': 32768, - 'gpt-4-32k-0613': 32768, - 'gpt-3.5-turbo': 4096, - 'gpt-3.5-turbo-16k': 16384, - 'gpt-3.5-turbo-0613': 4096, - 'gpt-3.5-turbo-16k-0613': 16384, +from typing import List, Dict, Any + +# Dictionary to store the maximum tokens for each model +MODEL_MAX_TOKENS: Dict[str, int] = { + 'gpt-4': 8192, + 'gpt-4-0613': 8192, + 'gpt-4-32k': 32768, + 'gpt-4-32k-0613': 32768, + 'gpt-3.5-turbo': 4096, + 'gpt-3.5-turbo-16k': 16384, + 'gpt-3.5-turbo-0613': 4096, + 'gpt-3.5-turbo-16k-0613': 16384, } -def num_tokens_from_messages(messages, model): - """Return the number of tokens used by a list of messages.""" - try: - encoding = tiktoken.encoding_for_model(model) - except KeyError: - print("Warning: model not found. Using cl100k_base encoding.") - encoding = tiktoken.get_encoding("cl100k_base") - if model in { - "gpt-3.5-turbo-0613", - "gpt-3.5-turbo-16k-0613", - "gpt-4-0314", - "gpt-4-32k-0314", - "gpt-4-0613", - "gpt-4-32k-0613", - }: - tokens_per_message = 3 - tokens_per_name = 1 - elif model == "gpt-3.5-turbo-0301": - tokens_per_message = 4 # every message follows <|start|>{role/name}\n{content}<|end|>\n - tokens_per_name = -1 # if there's a name, the role is omitted - elif "gpt-3.5-turbo" in model: - #print("Warning: gpt-3.5-turbo may update over time. Returning num tokens assuming gpt-3.5-turbo-0613.") - return num_tokens_from_messages(messages, model="gpt-3.5-turbo-0613") - elif "gpt-4" in model: - #print("Warning: gpt-4 may update over time. Returning num tokens assuming gpt-4-0613.") - return num_tokens_from_messages(messages, model="gpt-4-0613") - else: - raise NotImplementedError( - f"""num_tokens_from_messages() is not implemented for model {model}. See https://github.com/openai/openai-python/blob/main/chatml.md for information on how messages are converted to tokens.""" - ) - num_tokens = 0 - for message in messages: - num_tokens += tokens_per_message - for key, value in message.items(): - - try: - num_tokens += len(encoding.encode(value)) - if key == "name": - num_tokens += tokens_per_name - except: - # This isn't great but functions doesn't work with this! So I do this: - value = json.dumps(value) - num_tokens += len(encoding.encode(value)) - - num_tokens += 3 # every reply is primed with <|start|>assistant<|message|> - return num_tokens - - -def openai_streaming_response(messages, functions, system_message, model, - temperature, api_key): - - openai.api_key = api_key - - system_message_event = {"role": "system", "content": system_message} - - max_tokens = model_max_tokens[model] - max_tokens -= num_tokens_from_messages([system_message_event], model) - - # The list to store final messages - final_messages = [] - - # Token counter - token_count = 0 - - # Process messages in reverse order - for message in reversed(messages): - # Tokenize the message content - tokens = num_tokens_from_messages([message], model) - - # Check if adding the current message would exceed the 8K token limit - if token_count + tokens > max_tokens: - break - - # Add the message to the list - final_messages.append(message) - - # Update the token count - token_count += tokens - - # Reverse the final_messages list to maintain the order - final_messages.reverse() - - final_messages.insert(0, system_message_event) - - yield from openai.ChatCompletion.create( - model=model, - messages=final_messages, - functions=functions, - stream=True, - temperature=temperature, - ) +def num_tokens_from_messages(messages: List[Dict[str, Any]], model: str) -> int: + """ + Function to return the number of tokens used by a list of messages. + """ + # Attempt to get the encoding for the specified model + try: + encoding = tiktoken.encoding_for_model(model) + except KeyError: + print("Warning: model not found. Using cl100k_base encoding.") + encoding = tiktoken.get_encoding("cl100k_base") + + # Token handling specifics for different model types + if model in { + "gpt-3.5-turbo-0613", + "gpt-3.5-turbo-16k-0613", + "gpt-4-0314", + "gpt-4-32k-0314", + "gpt-4-0613", + "gpt-4-32k-0613", + }: + tokens_per_message = 3 + tokens_per_name = 1 + elif model == "gpt-3.5-turbo-0301": + tokens_per_message = 4 + tokens_per_name = -1 + elif "gpt-3.5-turbo" in model: + return num_tokens_from_messages(messages, model="gpt-3.5-turbo-0613") + elif "gpt-4" in model: + return num_tokens_from_messages(messages, model="gpt-4-0613") + else: + raise NotImplementedError( + f"""num_tokens_from_messages() is not implemented for model {model}. See https://github.com/openai/openai-python/blob/main/chatml.md for information on how messages are converted to tokens.""" + ) + + # Calculate the number of tokens + num_tokens = 0 + for message in messages: + num_tokens += tokens_per_message + for key, value in message.items(): + try: + num_tokens += len(encoding.encode(value)) + if key == "name": + num_tokens += tokens_per_name + except Exception: + value = json.dumps(value) + num_tokens += len(encoding.encode(value)) + + num_tokens += 3 + return num_tokens + + +def shorten_message_to_fit_limit(message: str, tokens_needed: int, encoding) -> str: + """ + Shorten a message to fit within a token limit by removing characters from the middle. + """ + while len(encoding.encode(message)) > tokens_needed: + middle = len(message) // 2 + message = message[:middle-1] + "..." + message[middle+2:] + return message + + +def openai_streaming_response(messages: List[Dict[str, Any]], functions: List[Any], system_message: str, model: str, temperature: float, api_key: str) -> Any: + """ + Function to generate an OpenAI streaming response. + + If the total tokens in a conversation exceed the model's maximum limit, + this function removes messages from the beginning of the conversation + until the total token count is under the limit. Preserves the + system message at the top of the conversation no matter what. + + If a user message in conjunction with the system message still exceeds the token limit, + the user message is trimmed from the middle character by character, with a '...' indicating the trimmed part. + """ + # Setting the OpenAI API key + openai.api_key = api_key + + # Preparing the system message event + system_message_event = {"role": "system", "content": system_message} + + # Attempt to get the encoding for the specified model + try: + encoding = tiktoken.encoding_for_model(model) + except KeyError: + print("Warning: model not found. Using cl100k_base encoding.") + encoding = tiktoken.get_encoding("cl100k_base") + + # Determining the maximum tokens available after accounting for the system message + max_tokens = MODEL_MAX_TOKENS[model] - num_tokens_from_messages([system_message_event], model) + + # Prepare the final_messages list and the token_count + final_messages = [] + token_count = 0 + + # Process the messages in reverse to fit as many as possible within the token limit + for message in reversed(messages): + tokens = num_tokens_from_messages([message], model) + if token_count + tokens > max_tokens: + if token_count + num_tokens_from_messages([system_message_event], model) > max_tokens: + # If one message with system message puts it over the limit, it will cut down the user message character by character from the middle. + message["content"] = shorten_message_to_fit_limit(message["content"], max_tokens - token_count, encoding) + else: + break + final_messages.append(message) + token_count += tokens + + # Reverse the final_messages to maintain original order + final_messages.reverse() + + # Include the system message as the first message + final_messages.insert(0, system_message_event) + + # Generate and yield the response from the OpenAI ChatCompletion API + yield from openai.ChatCompletion.create( + model=model, + messages=final_messages, + functions=functions, + stream=True, + temperature=temperature, + ) \ No newline at end of file