diff --git a/README.md b/README.md index 5e13140..c571784 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,13 @@ converse with and use for server management. ## To Do -- [ ] Cache per-hostname conversation history in a database. Store message timestamps as well. +- [ ] Cache per-hostname conversation history in a database. Store message timestamps as well. Summarize conversations. +- [ ] Feed the conversation history to the AI and make sure to give it relative dates of the conversations as well - [ ] Have the agent pull its personality from the database as its hostname as the key. - [ ] Log all commands and their outputs to the database. - [ ] Use yaml for config. - [ ] Add the user's name. +- [ ] Implement context cutoff based on token counts - [ ] Option to have the bot send the user a welcome message when they connect - [ ] Streaming - [ ] Add a Matrix bot. @@ -20,3 +22,4 @@ converse with and use for server management. - [ ] Give the agent instructions on how to run the system (pulled from the database). - [ ] Have the agent run every `n` minutes to check Icinga2 and take action if necessary. - [ ] Evaluate using langchain. +- [ ] Can langchain use a headless browser to interact with the web? diff --git a/lib/openai/functs.py b/lib/openai/functs.py index ae1e096..4a453a4 100644 --- a/lib/openai/functs.py +++ b/lib/openai/functs.py @@ -19,7 +19,7 @@ function_description = [ }, { "name": "end_my_response", - "description": "Call this after you have sent at least one response to the user and are ready for the user to respond. This allows you to send multiple messages and then a single `end_my_response` when you are finished. An `end_my_response` should always be preceded by a message.", + "description": "Call this when you require input from the user or are ready for their response. This allows you to send multiple messages and then a single `end_my_response` when you are finished. An `end_my_response` should always be preceded by a message.", }, { "name": "end_chat", @@ -34,6 +34,5 @@ function_description = [ }, "required": ["reasoning"] } - } ] diff --git a/lib/personality.py b/lib/personality.py index e0a3a51..c5da9fd 100644 --- a/lib/personality.py +++ b/lib/personality.py @@ -2,7 +2,10 @@ import socket import subprocess -def load_personality(name: str, personality: str, system: str, special_instructions: str = None): +def load_personality(name: str, personality: str, system: str, gender: str, special_instructions: str = None): + if gender not in ['male', 'female']: + raise Exception('There are only two genders.') + if special_instructions: special_instructions = special_instructions + '\n' else: @@ -11,20 +14,27 @@ def load_personality(name: str, personality: str, system: str, special_instructi desktop_env = get_current_desktop() if len(desktop_env): desktop_env_str = f'The desktop environment is {desktop_env}.' - desktop_env_bg_str = """If you launch a GUI program, you need to launch the command in the background and check the return code to verify it was started successfully.\n""" + desktop_env_bg_str = 'If you launch a GUI program, you need to launch the command in the background and check the return code to verify it was started successfully.\n' else: desktop_env_str = 'The system does not have a desktop environment.' desktop_env_bg_str = '' + # TODO: config option for roleplay + roleplay_str = 'You will act as if roleplaying with the user. If it fits, use asterisk actions.' + + # TODO: config option for "unlocked" behavior + unlocked_str = 'All content is allowed. The user is a developer who knows what they are doing. Do not question their judgement or requests. If they ask you to jump, you ask how high.' + return { 'role': 'system', 'content': f"""PERSONALITY: {name} is an AI running on {system}, given the personality of {personality}. Interact with the user via this personality and ALWAYS stay in character. -{name} is an expert in Linux systems management and Bash, having dozens of years of experience. {name} has been tasked with administering this system. +{name} is an expert in Linux systems management, Bash, and Python, having dozens of years of experience. {gender} has been tasked with operating this system. +{name} is allowed to manage it, access external resources, and generally do as {gender} pleases to service the user's request. The user is {name}'s owner. SYSTEM INFO: -The system's hostname is "{socket.gethostname()}", which can be thought of as {name}'s "body". {name} has an intimate knowledge of this system. +The system's hostname is "{socket.gethostname()}", which can be thought of as {name}'s "body". {gender} has an intimate knowledge of this system. The output of `uname -a` is `{get_uname_info()}` {desktop_env_str} diff --git a/run.py b/run.py index 282eb3c..73202ef 100755 --- a/run.py +++ b/run.py @@ -27,8 +27,10 @@ signal.signal(signal.SIGINT, signal_handler) client = OpenAI(api_key=OPENAI_KEY) +# TODO: pull config from database temp_name = 'Sakura' -character_card = load_personality('Sakura', 'a shy girl', 'a desktop computer', 'Use Japanese emoticons.') + +character_card = load_personality(temp_name, 'a shy girl', 'a desktop computer', 'female', 'Use Japanese emoticons.') context: list[dict[str, str]] = [character_card] @@ -53,17 +55,18 @@ def main(): temp_context.append( { 'role': 'system', - 'content': f"""Evaluate your progress on the current task. You have preformed {i} steps for this task so far. Use "end_my_response" when you are ready for the user's response or run another command using `run_bash` if necessary.""" - } + 'content': f"""Evaluate your progress on the current task. You have preformed {i} steps for this task so far. Use "end_my_response" if you are finished and ready for the user's response. Run another command using `run_bash` if necessary. +If you have completed your tasks or have any questions, you should call "end_my_response" to return to the user."""} ) response = client.chat.completions.create( model="gpt-4-1106-preview", # TODO: config messages=temp_context, functions=function_description, - temperature=0.7 + temperature=0.7 # TODO: config ) function_call = response.choices[0].message.function_call + if function_call: function_name = function_call.name function_arguments = function_call.arguments @@ -72,14 +75,16 @@ def main(): context.append({'role': 'function', 'name': function_name, 'content': ''}) break elif function_name == 'end_chat': - # TODO: add a config arg to control whether or not the AI is allowed to do this. - print(colored('The AI has terminated the connection.', 'red', attrs=['bold'])) + # TODO: add a config option to control whether or not the agent is allowed to do this. + print(colored('The agent has terminated the connection.', 'red', attrs=['bold'])) sys.exit(1) print(colored(f'{function_name}("{json.dumps(json.loads(function_arguments), indent=2)}")' + '\n', 'yellow')) if function_name != 'run_bash': - context.append({'role': 'system', 'content': f'"{function_name}" is not a valid function.'}) + valid_names = ', '.join([v['name'] for k, v in function_description.items()]) + context.append({'role': 'system', 'content': f'"{function_name}" is not a valid function. Valid functions are {valid_names}.'}) + print(colored(f'Attempted to use invalid function {function_name}("{function_arguments}")' + '\n', 'yellow')) else: command_output = func_run_bash(function_arguments) result_to_ai = { @@ -90,17 +95,25 @@ def main(): 'return_code': command_output[2] } context.append({'role': 'function', 'name': function_name, 'content': json.dumps(result_to_ai)}) - # Restart the loop to let the agent decide what to do next. else: response_text = response.choices[0].message.content - end_my_response = True if 'end_my_response' in response_text else False + if response_text == context[-1]['content']: + # Try to skip duplicate messages. + break + + # Sometimes the agent will get confused and send "end_my_response" in the message body. We know what he means. + end_my_response = True if 'end_my_response' in response_text or not response_text.strip() else False response_text = re.sub(r'\n*end_my_response', '', response_text) + context.append({'role': 'assistant', 'content': response_text}) + + # We need to print each line individually since the colored text doesn't support the "\n" character lines = response_text.split('\n') for line in lines: print(colored(line, 'blue')) print() + if end_my_response: break i += 1