This commit is contained in:
Cyberes 2024-01-21 13:20:15 -07:00
parent 785e7281d6
commit f00aca452a
4 changed files with 41 additions and 16 deletions

View File

@ -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?

View File

@ -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"]
}
}
]

View File

@ -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}

31
run.py
View File

@ -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