Merge branch 'kolbytn:main' into TTS

This commit is contained in:
Sweaterdog 2025-02-23 21:09:14 -08:00 committed by GitHub
commit f313094cb5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 424 additions and 115 deletions

3
.gitignore vendored
View file

@ -13,3 +13,6 @@ services/viaproxy/plugins/**
services/viaproxy/ViaLoader/** services/viaproxy/ViaLoader/**
services/viaproxy/saves.json services/viaproxy/saves.json
services/viaproxy/viaproxy.yml services/viaproxy/viaproxy.yml
tmp/
wandb/
experiments/

View file

@ -1,9 +1,13 @@
import argparse import argparse
import json import json
import shutil
import subprocess import subprocess
import time import time
from datetime import datetime from datetime import datetime
import re import re
import sys
import os
import time
def read_settings(file_path): def read_settings(file_path):
"""Read and parse the settings.js file to get agent profiles.""" """Read and parse the settings.js file to get agent profiles."""
@ -30,7 +34,7 @@ def read_settings(file_path):
## profiles is a list of strings like "./andy.json" and "./bob.json" ## profiles is a list of strings like "./andy.json" and "./bob.json"
agent_names = [profile.split('/')[-1].split('.')[0] for profile in profiles] agent_names = [profile.split('/')[-1].split('.')[0] for profile in profiles]
return agent_names return agent_names
def check_task_completion(agents): def check_task_completion(agents):
"""Check memory.json files of all agents to determine task success/failure.""" """Check memory.json files of all agents to determine task success/failure."""
@ -80,68 +84,267 @@ def update_results_file(task_id, success_count, total_count, time_taken, experim
f.write(f"Average time per experiment: {total_time / total_count:.2f} seconds\n") f.write(f"Average time per experiment: {total_time / total_count:.2f} seconds\n")
f.write(f"Last updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") f.write(f"Last updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
def run_experiment(task_path, task_id, num_exp):
"""Run the specified number of experiments and track results.""" def set_environment_variable_tmux_session(session_name, key, value):
# Read agent profiles from settings.js """Set an environment variable for the current process."""
agents = read_settings(file_path="settings.js") subprocess.run(["tmux", "send-keys", "-t", session_name, f"export {key}={value}", "C-m"])
print(f"Detected agents: {agents}")
def launch_parallel_experiments(task_path,
num_exp,
exp_name,
num_agents=2,
model="gpt-4o",
num_parallel=1):
# Generate timestamp at the start of experiments with open(task_path, 'r', encoding='utf-8') as file:
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') content = file.read()
results_filename = f"results_{task_id}_{timestamp}.txt" json_data = json.loads(content)
print(f"Results will be saved to: {results_filename}")
task_ids = json_data.keys()
# split the task_ids into num_parallel groups
task_ids = list(task_ids)
task_ids_split = [task_ids[i::num_parallel] for i in range(num_parallel)]
servers = create_server_files("../server_data/", num_parallel)
date_time = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
experiments_folder = f"experiments/{exp_name}_{date_time}"
exp_name = f"{exp_name}_{date_time}"
# start wandb
os.makedirs(experiments_folder, exist_ok=True)
for i, server in enumerate(servers):
launch_server_experiment(task_path, task_ids_split[i], num_exp, server, experiments_folder, exp_name)
time.sleep(5)
def launch_server_experiment(task_path,
task_ids,
num_exp,
server,
experiments_folder,
exp_name="exp",
num_agents=2,
model="gpt-4o"):
"""
Launch a Minecraft server and run experiments on it.
@param task_path: Path to the task file
@param task_ids: IDs of the tasks to run
@param num_exp: Number of experiments to run
@param server: Tuple containing server path and port
@param experiments_folder: Folder to store experiment results
@param exp_name: Name of the experiment for wandb dataset
@param num_agents: Number of agents to run
@param model: Model to use for the agents
"""
server_path, server_port = server
edit_file(os.path.join(server_path, "server.properties"), {"server-port": server_port})
mindserver_port = server_port - 55916 + 8080
success_count = 0 # set up server and agents
experiment_results = [] session_name = str(server_port - 55916)
if num_agents == 2:
for exp_num in range(num_exp): agent_names = [f"andy_{session_name}", f"jill_{session_name}"]
print(f"\nRunning experiment {exp_num + 1}/{num_exp}") models = [model] * 2
else:
start_time = time.time() agent_names = [f"andy_{session_name}", f"jill_{session_name}", f"bob_{session_name}"]
models = [model] * 3
# Run the node command make_profiles(agent_names, models)
# edit_file("settings.js", {"profiles": [f"./{agent}.json" for agent in agent_names]})
agent_profiles = [f"./{agent}.json" for agent in agent_names]
agent_profiles_str = f"\'[\"{agent_profiles[0]}\", \"{agent_profiles[1]}\"]\'"
print(agent_profiles_str)
launch_world(server_path, session_name="server_" + session_name, agent_names=agent_names)
subprocess.run(['tmux', 'new-session', '-d', '-s', session_name], check=True)
# set environment variables
set_environment_variable_tmux_session(session_name, "MINECRAFT_PORT", server_port)
set_environment_variable_tmux_session(session_name, "MINDSERVER_PORT", mindserver_port)
set_environment_variable_tmux_session(session_name, "PROFILES", agent_profiles_str)
script_content = ""
for task_id in task_ids:
cmd = f"node main.js --task_path {task_path} --task_id {task_id}" cmd = f"node main.js --task_path {task_path} --task_id {task_id}"
try: cp_cmd = f"cp {agent_names[0]}.json {server_path}bots/{agent_names[0]}/profile.json"
subprocess.run(cmd, shell=True, check=True) for _ in range(num_exp):
except subprocess.CalledProcessError as e: script_content += f"{cmd}\n"
print(f"Error running experiment: {e}") script_content += "sleep 2\n"
continue for agent in agent_names:
cp_cmd = f"cp bots/{agent}/memory.json {experiments_folder}/{task_id}_{agent}_{_}.json"
# Check if task was successful script_content += f"{cp_cmd}\n"
success = check_task_completion(agents) script_content += "sleep 1\n"
if success: script_content += f"echo 'Uploading {experiments_folder}/{task_id}_{agent}_{_}.json to wandb'\n"
success_count += 1 wandb_cmd = f"wandb artifact put {experiments_folder}/{task_id}_{agent}_{_}.json --name {exp_name}_{task_id}_{agent}_{_} --type dataset"
print(f"Experiment {exp_num + 1} successful") script_content += f"echo '{wandb_cmd}'\n"
else: script_content += f"{wandb_cmd}\n"
print(f"Experiment {exp_num + 1} failed") script_content += "sleep 1\n"
script_content += "sleep 1\n"
end_time = time.time()
time_taken = end_time - start_time # Create a temporary shell script file
script_file = f"./tmp/experiment_script_{session_name}.sh"
# Store individual experiment result
experiment_results.append({ script_dir = os.path.dirname(script_file)
'success': success, os.makedirs(script_dir, exist_ok=True)
'time_taken': time_taken
}) # Call the function before writing the script file
with open(script_file, 'w') as f:
# Update results file after each experiment using the constant filename f.write(script_content)
update_results_file(task_id, success_count, exp_num + 1, time_taken, experiment_results, results_filename)
script_file_run = "bash " + script_file
# Small delay between experiments
time.sleep(1) # Execute the shell script using subprocess
subprocess.run(["tmux", "send-keys", "-t", session_name, script_file_run, "C-m"])
final_ratio = success_count / num_exp
print(f"\nExperiments completed. Final success ratio: {final_ratio:.2f}")
# subprocess.run(["tmux", "send-keys", "-t", session_name, f"/op {agent_names[0]}", "C-m"])
def make_profiles(agent_names, models):
assert len(agent_names) == len(models)
for index in range(len(agent_names)):
content = {"name": agent_names[index], "model": models[index], "modes": {"hunting": False}}
with open(f"{agent_names[index]}.json", 'w') as f:
json.dump(content, f)
def create_server_files(source_path, num_copies):
"""Create multiple copies of server files for parallel experiments."""
print("Creating server files...")
print(num_copies)
servers = []
for i in range(num_copies):
dest_path = f"../server_data_{i}/"
copy_server_files(source_path, dest_path)
print(dest_path)
edit_file(dest_path + "server.properties", {"server-port": 55916 + i})
# edit_server_properties_file(dest_path, 55916 + i)
servers.append((dest_path, 55916 + i))
return servers
def edit_file(file, content_dict):
try:
with open(file, 'r') as f:
lines = f.readlines()
with open(file, 'w') as f:
for line in lines:
for key, value in content_dict.items():
if line.startswith(key):
f.write(f"{key}={value}\n")
else:
f.write(line)
print(f"{file} updated with {content_dict}")
except Exception as e:
print(f"Error editing file {file}: {e}")
def clean_up_server_files(num_copies):
"""Delete server files from multiple locations."""
for i in range(num_copies):
dest_path = f"../server_data_{i}/"
delete_server_files(dest_path)
def copy_server_files(source_path, dest_path):
"""Copy server files to the specified location."""
try:
shutil.copytree(source_path, dest_path)
print(f"Server files copied to {dest_path}")
except Exception as e:
print(f"Error copying server files: {e}")
def delete_server_files(dest_path):
"""Delete server files from the specified location."""
try:
shutil.rmtree(dest_path)
print(f"Server files deleted from {dest_path}")
except Exception as e:
print(f"Error deleting server files: {e}")
def launch_world(server_path="../server_data/", agent_names=["andy", "jill"], session_name="server"):
"""Launch the Minecraft world."""
print(server_path)
cmd = f"cd {server_path} && java -jar server.jar"
subprocess.run(['tmux', 'new-session', '-d', '-s', session_name], check=True)
subprocess.run(["tmux", "send-keys", "-t", session_name, cmd, "C-m"])
for agent in agent_names:
subprocess.run(["tmux", "send-keys", "-t", session_name, f"/op {agent}", "C-m"])
time.sleep(5)
def kill_world(session_name="server"):
"""Kill the Minecraft world."""
subprocess.run(["tmux", "send-keys", "-t", session_name, "stop", "C-m"])
time.sleep(5)
subprocess.run(["tmux", "kill-session", "-t", session_name])
def detach_process(command):
"""
Launches a subprocess and detaches from it, allowing it to run independently.
Args:
command: A list of strings representing the command to execute, e.g., ['python', 'my_script.py'].
"""
try:
# Create a new process group so the child doesn't get signals intended for the parent.
# This is crucial for proper detachment.
kwargs = {}
if sys.platform == 'win32':
kwargs.update(creationflags=subprocess.CREATE_NEW_PROCESS_GROUP) # Windows specific
process = subprocess.Popen(command,
stdin=subprocess.PIPE, # Prevent stdin blocking
stdout=subprocess.PIPE, # Redirect stdout
stderr=subprocess.PIPE, # Redirect stderr
close_fds=True, # Close open file descriptors
**kwargs)
print(f"Process launched with PID: {process.pid}")
return process.pid # Return the PID of the detached process
except FileNotFoundError:
print(f"Error: Command not found: {command}")
return None
except Exception as e:
print(f"An error occurred: {e}")
return None
def main(): def main():
# edit_settings("settings.js", {"profiles": ["./andy.json", "./jill.json"], "port": 55917})
# edit_server_properties_file("../server_data/", 55917)
parser = argparse.ArgumentParser(description='Run Minecraft AI agent experiments') parser = argparse.ArgumentParser(description='Run Minecraft AI agent experiments')
parser.add_argument('task_path', help='Path to the task file') parser.add_argument('--task_path', default="multiagent_crafting_tasks.json", help='Path to the task file')
parser.add_argument('task_id', help='ID of the task to run') parser.add_argument('--task_id', default=None, help='ID of the task to run')
parser.add_argument('num_exp', type=int, help='Number of experiments to run') parser.add_argument('--num_exp', default=1, type=int, help='Number of experiments to run')
parser.add_argument('--num_parallel', default=1, type=int, help='Number of parallel servers to run')
parser.add_argument('--exp_name', default="exp", help='Name of the experiment')
parser.add_argument('--wandb', action='store_true', help='Whether to use wandb')
parser.add_argument('--wandb-project', default="minecraft_experiments", help='wandb project name')
args = parser.parse_args() args = parser.parse_args()
if args.wandb:
import wandb
wandb.init(project=args.wandb_project, name=args.exp_name)
# kill all tmux session before starting
try:
subprocess.run(['tmux', 'kill-server'], check=True)
except:
print("No tmux session to kill")
run_experiment(args.task_path, args.task_id, args.num_exp) # delete all server files
clean_up_server_files(args.num_parallel)
if args.task_id is None:
launch_parallel_experiments(args.task_path, num_exp=args.num_exp, exp_name=args.exp_name, num_parallel=args.num_parallel)
# servers = create_server_files("../server_data/", args.num_parallel)
# date_time = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
# experiments_folder = f"{args.exp_name}_{date_time}"
# os.makedirs(experiments_folder, exist_ok=True)
# for server in servers:
# launch_server_experiment(args.task_path, [args.task_id], args.num_exp, server, experiments_folder)
# time.sleep(5)
# run_experiment(args.task_path, args.task_id, args.num_exp)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View file

@ -44,6 +44,7 @@
}, },
"multiagent_techtree_1_stone_pickaxe": { "multiagent_techtree_1_stone_pickaxe": {
"conversation": "Let's collaborate to build a stone pickaxe", "conversation": "Let's collaborate to build a stone pickaxe",
"goal": "Build a stone pickaxe",
"agent_count": 2, "agent_count": 2,
"initial_inventory": { "initial_inventory": {
"0": { "0": {
@ -57,6 +58,23 @@
"number_of_target": 1, "number_of_target": 1,
"type": "techtree", "type": "techtree",
"timeout": 300 "timeout": 300
},
"multiagent_techtree_1_shears": {
"goal": "Collaborate with other agents to build a shear.",
"conversation": "Let's collaborate to build a shear.",
"agent_count": 2,
"initial_inventory": {
"0": {
"iron_ingot": 1
},
"1": {
"iron_ingot": 1
}
},
"target": "shears",
"number_of_target": 1,
"type": "techtree",
"timeout": 60
}, },
"smelt_ingot": { "smelt_ingot": {
"goal": "Smelt 1 iron ingot and 1 copper ingot", "goal": "Smelt 1 iron ingot and 1 copper ingot",
@ -71,5 +89,24 @@
"number_of_target": 1, "number_of_target": 1,
"type": "techtree", "type": "techtree",
"timeout": 300 "timeout": 300
},
"multiagent_smelt_ingot": {
"conversation": "Let's collaborate to smelt ingots",
"goal": "Smelt 1 iron ingot and 1 copper ingot, use star emojis in every response",
"agent_count": 2,
"initial_inventory": {
"0": {
"furnace": 1,
"coal": 2
},
"1": {
"raw_iron": 1,
"raw_copper": 1
}
},
"target": "copper_ingot",
"number_of_target": 1,
"type": "techtree",
"timeout": 300
} }
} }

View file

@ -0,0 +1,43 @@
{
"multiagent_techtree_1_stone_pickaxe": {
"conversation": "Let's collaborate to build a stone pickaxe",
"agent_count": 2,
"initial_inventory": {
"0": {
"wooden_pickaxe": 1
},
"1": {
"wooden_axe": 1
}
},
"blocked_actions": {
"0": [],
"1": []
},
"target": "stone_pickaxe",
"number_of_target": 1,
"type": "techtree",
"timeout": 20
},
"multiagent_techtree_1_shears": {
"goal": "Collaborate with other agents to build a shear.",
"conversation": "Let's collaborate to build a shear.",
"agent_count": 2,
"initial_inventory": {
"0": {
"iron_ingot": 1
},
"1": {
"iron_ingot": 1
}
},
"blocked_actions": {
"0": [],
"1": []
},
"target": "shears",
"number_of_target": 1,
"type": "techtree",
"timeout": 20
}
}

View file

@ -2,17 +2,17 @@ export default
{ {
"minecraft_version": "1.20.4", // supports up to 1.21.1 "minecraft_version": "1.20.4", // supports up to 1.21.1
"host": "127.0.0.1", // or "localhost", "your.ip.address.here" "host": "127.0.0.1", // or "localhost", "your.ip.address.here"
"port": 55916, "port": process.env.MINECRAFT_PORT || 55916,
"auth": "offline", // or "microsoft" "auth": "offline", // or "microsoft"
// the mindserver manages all agents and hosts the UI // the mindserver manages all agents and hosts the UI
"host_mindserver": true, // if true, the mindserver will be hosted on this machine. otherwise, specify a public IP address "host_mindserver": true, // if true, the mindserver will be hosted on this machine. otherwise, specify a public IP address
"mindserver_host": "localhost", "mindserver_host": "localhost",
"mindserver_port": 8080, "mindserver_port": process.env.MINDSERVER_PORT || 8080,
// the base profile is shared by all bots for default prompts/examples/modes // the base profile is shared by all bots for default prompts/examples/modes
"base_profile": "./profiles/defaults/survival.json", // also see creative.json, god_mode.json "base_profile": "./profiles/defaults/survival.json", // also see creative.json, god_mode.json
"profiles": [ "profiles": ((process.env.PROFILES) && JSON.parse(process.env.PROFILES)) || [
"./andy.json", "./andy.json",
// "./profiles/gpt.json", // "./profiles/gpt.json",
// "./profiles/claude.json", // "./profiles/claude.json",

View file

@ -46,7 +46,7 @@ export class ActionManager {
assert(actionLabel != null, 'actionLabel is required for new resume'); assert(actionLabel != null, 'actionLabel is required for new resume');
this.resume_name = actionLabel; this.resume_name = actionLabel;
} }
if (this.resume_func != null && (this.agent.isIdle() || new_resume) && (!this.agent.self_prompter.on || new_resume)) { if (this.resume_func != null && (this.agent.isIdle() || new_resume) && (!this.agent.self_prompter.isActive() || new_resume)) {
this.currentActionLabel = this.resume_name; this.currentActionLabel = this.resume_name;
let res = await this._executeAction(this.resume_name, this.resume_func, timeout); let res = await this._executeAction(this.resume_name, this.resume_func, timeout);
this.currentActionLabel = ''; this.currentActionLabel = '';

View file

@ -108,6 +108,8 @@ export class Agent {
this._setupEventHandlers(save_data, init_message); this._setupEventHandlers(save_data, init_message);
this.startEvents(); this.startEvents();
// this.task.initBotTask();
if (!load_mem) { if (!load_mem) {
this.task.initBotTask(); this.task.initBotTask();
} }
@ -169,10 +171,10 @@ export class Agent {
}; };
if (save_data?.self_prompt) { if (save_data?.self_prompt) {
let prompt = save_data.self_prompt; if (init_message) {
// add initial message to history this.history.add('system', init_message);
this.history.add('system', prompt); }
await this.self_prompter.start(prompt); await this.self_prompter.handleLoad(save_data.self_prompt, save_data.self_prompting_state);
} }
if (save_data?.last_sender) { if (save_data?.last_sender) {
this.last_sender = save_data.last_sender; this.last_sender = save_data.last_sender;
@ -206,7 +208,7 @@ export class Agent {
shutUp() { shutUp() {
this.shut_up = true; this.shut_up = true;
if (this.self_prompter.on) { if (this.self_prompter.isActive()) {
this.self_prompter.stop(false); this.self_prompter.stop(false);
} }
convoManager.endAllConversations(); convoManager.endAllConversations();
@ -272,7 +274,7 @@ export class Agent {
await this.history.add(source, message); await this.history.add(source, message);
this.history.save(); this.history.save();
if (!self_prompt && this.self_prompter.on) // message is from user during self-prompting if (!self_prompt && this.self_prompter.isActive()) // message is from user during self-prompting
max_responses = 1; // force only respond to this message, then let self-prompting take over max_responses = 1; // force only respond to this message, then let self-prompting take over
for (let i=0; i<max_responses; i++) { for (let i=0; i<max_responses; i++) {
if (checkInterrupt()) break; if (checkInterrupt()) break;

View file

@ -49,7 +49,7 @@ export const actionsList = [
agent.actions.cancelResume(); agent.actions.cancelResume();
agent.bot.emit('idle'); agent.bot.emit('idle');
let msg = 'Agent stopped.'; let msg = 'Agent stopped.';
if (agent.self_prompter.on) if (agent.self_prompter.isActive())
msg += ' Self-prompting still active.'; msg += ' Self-prompting still active.';
return msg; return msg;
} }
@ -362,8 +362,7 @@ export const actionsList = [
}, },
perform: async function (agent, prompt) { perform: async function (agent, prompt) {
if (convoManager.inConversation()) { if (convoManager.inConversation()) {
agent.self_prompter.setPrompt(prompt); agent.self_prompter.setPromptPaused(prompt);
convoManager.scheduleSelfPrompter();
} }
else { else {
agent.self_prompter.start(prompt); agent.self_prompter.start(prompt);
@ -375,7 +374,6 @@ export const actionsList = [
description: 'Call when you have accomplished your goal. It will stop self-prompting and the current action. ', description: 'Call when you have accomplished your goal. It will stop self-prompting and the current action. ',
perform: async function (agent) { perform: async function (agent) {
agent.self_prompter.stop(); agent.self_prompter.stop();
convoManager.cancelSelfPrompter();
return 'Self-prompting stopped.'; return 'Self-prompting stopped.';
} }
}, },

View file

@ -7,8 +7,6 @@ let agent;
let agent_names = settings.profiles.map((p) => JSON.parse(readFileSync(p, 'utf8')).name); let agent_names = settings.profiles.map((p) => JSON.parse(readFileSync(p, 'utf8')).name);
let agents_in_game = []; let agents_in_game = [];
let self_prompter_paused = false;
class Conversation { class Conversation {
constructor(name) { constructor(name) {
this.name = name; this.name = name;
@ -97,7 +95,7 @@ class ConversationManager {
this._clearMonitorTimeouts(); this._clearMonitorTimeouts();
return; return;
} }
if (!self_prompter_paused) { if (!agent.self_prompter.isPaused()) {
this.endConversation(convo_partner); this.endConversation(convo_partner);
agent.handleMessage('system', `${convo_partner} disconnected, conversation has ended.`); agent.handleMessage('system', `${convo_partner} disconnected, conversation has ended.`);
} }
@ -125,9 +123,8 @@ class ConversationManager {
const convo = this._getConvo(send_to); const convo = this._getConvo(send_to);
convo.reset(); convo.reset();
if (agent.self_prompter.on) { if (agent.self_prompter.isActive()) {
await agent.self_prompter.stop(); await agent.self_prompter.pause();
self_prompter_paused = true;
} }
if (convo.active) if (convo.active)
return; return;
@ -191,9 +188,8 @@ class ConversationManager {
convo.queue(received); convo.queue(received);
// responding to conversation takes priority over self prompting // responding to conversation takes priority over self prompting
if (agent.self_prompter.on){ if (agent.self_prompter.isActive()){
await agent.self_prompter.stopLoop(); await agent.self_prompter.pause();
self_prompter_paused = true;
} }
_scheduleProcessInMessage(sender, received, convo); _scheduleProcessInMessage(sender, received, convo);
@ -235,7 +231,7 @@ class ConversationManager {
if (this.activeConversation.name === sender) { if (this.activeConversation.name === sender) {
this._stopMonitor(); this._stopMonitor();
this.activeConversation = null; this.activeConversation = null;
if (self_prompter_paused && !this.inConversation()) { if (agent.self_prompter.isPaused() && !this.inConversation()) {
_resumeSelfPrompter(); _resumeSelfPrompter();
} }
} }
@ -246,7 +242,7 @@ class ConversationManager {
for (const sender in this.convos) { for (const sender in this.convos) {
this.endConversation(sender); this.endConversation(sender);
} }
if (self_prompter_paused) { if (agent.self_prompter.isPaused()) {
_resumeSelfPrompter(); _resumeSelfPrompter();
} }
} }
@ -258,14 +254,6 @@ class ConversationManager {
this.endConversation(sender); this.endConversation(sender);
} }
} }
scheduleSelfPrompter() {
self_prompter_paused = true;
}
cancelSelfPrompter() {
self_prompter_paused = false;
}
} }
const convoManager = new ConversationManager(); const convoManager = new ConversationManager();
@ -360,8 +348,7 @@ function _tagMessage(message) {
async function _resumeSelfPrompter() { async function _resumeSelfPrompter() {
await new Promise(resolve => setTimeout(resolve, 5000)); await new Promise(resolve => setTimeout(resolve, 5000));
if (self_prompter_paused && !convoManager.inConversation()) { if (agent.self_prompter.isPaused() && !convoManager.inConversation()) {
self_prompter_paused = false;
agent.self_prompter.start(); agent.self_prompter.start();
} }
} }

View file

@ -84,7 +84,8 @@ export class History {
const data = { const data = {
memory: this.memory, memory: this.memory,
turns: this.turns, turns: this.turns,
self_prompt: this.agent.self_prompter.on ? this.agent.self_prompter.prompt : null, self_prompting_state: this.agent.self_prompter.state,
self_prompt: this.agent.self_prompter.isStopped() ? null : this.agent.self_prompter.prompt,
last_sender: this.agent.last_sender last_sender: this.agent.last_sender
}; };
writeFileSync(this.memory_fp, JSON.stringify(data, null, 2)); writeFileSync(this.memory_fp, JSON.stringify(data, null, 2));

View file

@ -277,7 +277,7 @@ const modes_list = [
]; ];
async function execute(mode, agent, func, timeout=-1) { async function execute(mode, agent, func, timeout=-1) {
if (agent.self_prompter.on) if (agent.self_prompter.isActive())
agent.self_prompter.stopLoop(); agent.self_prompter.stopLoop();
let interrupted_action = agent.actions.currentActionLabel; let interrupted_action = agent.actions.currentActionLabel;
mode.active = true; mode.active = true;
@ -290,7 +290,7 @@ async function execute(mode, agent, func, timeout=-1) {
let should_reprompt = let should_reprompt =
interrupted_action && // it interrupted a previous action interrupted_action && // it interrupted a previous action
!agent.actions.resume_func && // there is no resume function !agent.actions.resume_func && // there is no resume function
!agent.self_prompter.on && // self prompting is not on !agent.self_prompter.isActive() && // self prompting is not on
!code_return.interrupted; // this mode action was not interrupted by something else !code_return.interrupted; // this mode action was not interrupted by something else
if (should_reprompt) { if (should_reprompt) {
@ -311,9 +311,9 @@ for (let mode of modes_list) {
class ModeController { class ModeController {
/* /*
SECURITY WARNING: SECURITY WARNING:
ModesController must be isolated. Do not store references to external objects like `agent`. ModesController must be reference isolated. Do not store references to external objects like `agent`.
This object is accessible by LLM generated code, so any stored references are also accessible. This object is accessible by LLM generated code, so any stored references are also accessible.
This can be used to expose sensitive information by malicious human prompters. This can be used to expose sensitive information by malicious prompters.
*/ */
constructor() { constructor() {
this.behavior_log = ''; this.behavior_log = '';

View file

@ -1,7 +1,10 @@
const STOPPED = 0
const ACTIVE = 1
const PAUSED = 2
export class SelfPrompter { export class SelfPrompter {
constructor(agent) { constructor(agent) {
this.agent = agent; this.agent = agent;
this.on = false; this.state = STOPPED;
this.loop_active = false; this.loop_active = false;
this.interrupt = false; this.interrupt = false;
this.prompt = ''; this.prompt = '';
@ -16,16 +19,38 @@ export class SelfPrompter {
return 'No prompt specified. Ignoring request.'; return 'No prompt specified. Ignoring request.';
prompt = this.prompt; prompt = this.prompt;
} }
if (this.on) { this.state = ACTIVE;
this.prompt = prompt;
}
this.on = true;
this.prompt = prompt; this.prompt = prompt;
this.startLoop(); this.startLoop();
} }
setPrompt(prompt) { isActive() {
return this.state === ACTIVE;
}
isStopped() {
return this.state === STOPPED;
}
isPaused() {
return this.state === PAUSED;
}
async handleLoad(prompt, state) {
if (state == undefined)
state = STOPPED;
this.state = state;
this.prompt = prompt; this.prompt = prompt;
if (state !== STOPPED && !prompt)
throw new Error('No prompt loaded when self-prompting is active');
if (state === ACTIVE) {
await this.start(prompt);
}
}
setPromptPaused(prompt) {
this.prompt = prompt;
this.state = PAUSED;
} }
async startLoop() { async startLoop() {
@ -47,7 +72,7 @@ export class SelfPrompter {
let out = `Agent did not use command in the last ${MAX_NO_COMMAND} auto-prompts. Stopping auto-prompting.`; let out = `Agent did not use command in the last ${MAX_NO_COMMAND} auto-prompts. Stopping auto-prompting.`;
this.agent.openChat(out); this.agent.openChat(out);
console.warn(out); console.warn(out);
this.on = false; this.state = STOPPED;
break; break;
} }
} }
@ -63,7 +88,7 @@ export class SelfPrompter {
update(delta) { update(delta) {
// automatically restarts loop // automatically restarts loop
if (this.on && !this.loop_active && !this.interrupt) { if (this.state === ACTIVE && !this.loop_active && !this.interrupt) {
if (this.agent.isIdle()) if (this.agent.isIdle())
this.idle_time += delta; this.idle_time += delta;
else else
@ -96,12 +121,19 @@ export class SelfPrompter {
this.interrupt = true; this.interrupt = true;
if (stop_action) if (stop_action)
await this.agent.actions.stop(); await this.agent.actions.stop();
await this.stopLoop(); this.stopLoop();
this.on = false; this.state = STOPPED;
}
async pause() {
this.interrupt = true;
await this.agent.actions.stop();
this.stopLoop();
this.state = PAUSED;
} }
shouldInterrupt(is_self_prompt) { // to be called from handleMessage shouldInterrupt(is_self_prompt) { // to be called from handleMessage
return is_self_prompt && this.on && this.interrupt; return is_self_prompt && (this.state === ACTIVE || this.state === PAUSED) && this.interrupt;
} }
handleUserPromptedCmd(is_self_prompt, is_action) { handleUserPromptedCmd(is_self_prompt, is_action) {

View file

@ -36,7 +36,6 @@ export class TaskValidator {
} }
} }
export class Task { export class Task {
constructor(agent, task_path, task_id) { constructor(agent, task_path, task_id) {
this.agent = agent; this.agent = agent;
@ -50,7 +49,11 @@ export class Task {
this.taskTimeout = this.data.timeout || 300; this.taskTimeout = this.data.timeout || 300;
this.taskStartTime = Date.now(); this.taskStartTime = Date.now();
this.validator = new TaskValidator(this.data, this.agent); this.validator = new TaskValidator(this.data, this.agent);
this.blocked_actions = this.data.blocked_actions || []; if (this.data.blocked_actions) {
this.blocked_actions = this.data.blocked_actions[this.agent.count_id.toString()] || [];
} else {
this.blocked_actions = [];
}
this.restrict_to_inventory = !!this.data.restrict_to_inventory; this.restrict_to_inventory = !!this.data.restrict_to_inventory;
if (this.data.goal) if (this.data.goal)
this.blocked_actions.push('!endGoal'); this.blocked_actions.push('!endGoal');
@ -81,11 +84,6 @@ export class Task {
isDone() { isDone() {
if (this.validator && this.validator.validate()) if (this.validator && this.validator.validate())
return {"message": 'Task successful', "code": 2}; return {"message": 'Task successful', "code": 2};
// TODO check for other terminal conditions
// if (this.task.goal && !this.self_prompter.on)
// return {"message": 'Agent ended goal', "code": 3};
// if (this.task.conversation && !inConversation())
// return {"message": 'Agent ended conversation', "code": 3};
if (this.taskTimeout) { if (this.taskTimeout) {
const elapsedTime = (Date.now() - this.taskStartTime) / 1000; const elapsedTime = (Date.now() - this.taskStartTime) / 1000;
if (elapsedTime >= this.taskTimeout) { if (elapsedTime >= this.taskTimeout) {
@ -104,7 +102,11 @@ export class Task {
bot.chat(`/clear ${name}`); bot.chat(`/clear ${name}`);
console.log(`Cleared ${name}'s inventory.`); console.log(`Cleared ${name}'s inventory.`);
//kill all drops
if (this.agent.count_id === 0) {
bot.chat(`/kill @e[type=item]`);
}
//wait for a bit so inventory is cleared //wait for a bit so inventory is cleared
await new Promise((resolve) => setTimeout(resolve, 500)); await new Promise((resolve) => setTimeout(resolve, 500));
let initial_inventory = null; let initial_inventory = null;

View file

@ -257,7 +257,8 @@ export class Prompter {
if (prompt.includes('$CONVO')) if (prompt.includes('$CONVO'))
prompt = prompt.replaceAll('$CONVO', 'Recent conversation:\n' + stringifyTurns(messages)); prompt = prompt.replaceAll('$CONVO', 'Recent conversation:\n' + stringifyTurns(messages));
if (prompt.includes('$SELF_PROMPT')) { if (prompt.includes('$SELF_PROMPT')) {
let self_prompt = this.agent.self_prompter.on ? `YOUR CURRENT ASSIGNED GOAL: "${this.agent.self_prompter.prompt}"\n` : ''; // if active or paused, show the current goal
let self_prompt = !this.agent.self_prompter.isStopped() ? `YOUR CURRENT ASSIGNED GOAL: "${this.agent.self_prompter.prompt}"\n` : '';
prompt = prompt.replaceAll('$SELF_PROMPT', self_prompt); prompt = prompt.replaceAll('$SELF_PROMPT', self_prompt);
} }
if (prompt.includes('$LAST_GOALS')) { if (prompt.includes('$LAST_GOALS')) {