mirror of
https://github.com/kolbytn/mindcraft.git
synced 2025-04-29 19:44:53 +02:00
225 lines
No EOL
8.9 KiB
JavaScript
225 lines
No EOL
8.9 KiB
JavaScript
import { writeFile, readFile, mkdirSync } from 'fs';
|
|
import settings from '../../settings.js';
|
|
import { makeCompartment } from './library/lockdown.js';
|
|
import * as skills from './library/skills.js';
|
|
import * as world from './library/world.js';
|
|
import { Vec3 } from 'vec3';
|
|
import {ESLint} from "eslint";
|
|
|
|
export class Coder {
|
|
constructor(agent) {
|
|
this.agent = agent;
|
|
this.file_counter = 0;
|
|
this.fp = '/bots/'+agent.name+'/action-code/';
|
|
this.code_template = '';
|
|
this.code_lint_template = '';
|
|
|
|
readFile('./bots/execTemplate.js', 'utf8', (err, data) => {
|
|
if (err) throw err;
|
|
this.code_template = data;
|
|
});
|
|
readFile('./bots/lintTemplate.js', 'utf8', (err, data) => {
|
|
if (err) throw err;
|
|
this.code_lint_template = data;
|
|
});
|
|
mkdirSync('.' + this.fp, { recursive: true });
|
|
}
|
|
|
|
async generateCode(agent_history) {
|
|
this.agent.bot.modes.pause('unstuck');
|
|
// this message history is transient and only maintained in this function
|
|
let messages = agent_history.getHistory();
|
|
messages.push({role: 'system', content: 'Code generation started. Write code in codeblock in your response:'});
|
|
|
|
const MAX_ATTEMPTS = 5;
|
|
const MAX_NO_CODE = 3;
|
|
|
|
let code = null;
|
|
let no_code_failures = 0;
|
|
for (let i=0; i<MAX_ATTEMPTS; i++) {
|
|
if (this.agent.bot.interrupt_code)
|
|
return null;
|
|
const messages_copy = JSON.parse(JSON.stringify(messages));
|
|
let res = await this.agent.prompter.promptCoding(messages_copy);
|
|
if (this.agent.bot.interrupt_code)
|
|
return null;
|
|
let contains_code = res.indexOf('```') !== -1;
|
|
if (!contains_code) {
|
|
if (res.indexOf('!newAction') !== -1) {
|
|
messages.push({
|
|
role: 'assistant',
|
|
content: res.substring(0, res.indexOf('!newAction'))
|
|
});
|
|
continue; // using newaction will continue the loop
|
|
}
|
|
|
|
if (no_code_failures >= MAX_NO_CODE) {
|
|
console.warn("Action failed, agent would not write code.");
|
|
return 'Action failed, agent would not write code.';
|
|
}
|
|
messages.push({
|
|
role: 'system',
|
|
content: 'Error: no code provided. Write code in codeblock in your response. ``` // example ```'}
|
|
);
|
|
console.warn("No code block generated. Trying again.");
|
|
no_code_failures++;
|
|
continue;
|
|
}
|
|
code = res.substring(res.indexOf('```')+3, res.lastIndexOf('```'));
|
|
const result = await this._stageCode(code);
|
|
const executionModule = result.func;
|
|
const lintResult = await this._lintCode(result.src_lint_copy);
|
|
if (lintResult) {
|
|
const message = 'Error: Code lint error:'+'\n'+lintResult+'\nPlease try again.';
|
|
console.warn("Linting error:"+'\n'+lintResult+'\n');
|
|
messages.push({ role: 'system', content: message });
|
|
continue;
|
|
}
|
|
if (!executionModule) {
|
|
console.warn("Failed to stage code, something is wrong.");
|
|
return 'Failed to stage code, something is wrong.';
|
|
}
|
|
|
|
try {
|
|
console.log('Executing code...');
|
|
await executionModule.main(this.agent.bot);
|
|
|
|
const code_output = this.agent.actions.getBotOutputSummary();
|
|
const summary = "Agent wrote this code: \n```" + this._sanitizeCode(code) + "```\nCode Output:\n" + code_output;
|
|
return summary;
|
|
} catch (e) {
|
|
if (this.agent.bot.interrupt_code)
|
|
return null;
|
|
|
|
console.warn('Generated code threw error: ' + e.toString());
|
|
console.warn('trying again...');
|
|
|
|
const code_output = this.agent.actions.getBotOutputSummary();
|
|
|
|
messages.push({
|
|
role: 'assistant',
|
|
content: res
|
|
});
|
|
messages.push({
|
|
role: 'system',
|
|
content: `Code Output:\n${code_output}\nCODE EXECUTION THREW ERROR: ${e.toString()}\n Please try again:`
|
|
});
|
|
}
|
|
}
|
|
return `Code generation failed after ${MAX_ATTEMPTS} attempts.`;
|
|
}
|
|
|
|
async _lintCode(code) {
|
|
let result = '#### CODE ERROR INFO ###\n';
|
|
// Extract everything in the code between the beginning of 'skills./world.' and the '('
|
|
const skillRegex = /(?:skills|world)\.(.*?)\(/g;
|
|
const skills = [];
|
|
let match;
|
|
while ((match = skillRegex.exec(code)) !== null) {
|
|
skills.push(match[1]);
|
|
}
|
|
const allDocs = await this.agent.prompter.skill_libary.getAllSkillDocs();
|
|
// check function exists
|
|
const missingSkills = skills.filter(skill => !!allDocs[skill]);
|
|
if (missingSkills.length > 0) {
|
|
result += 'These functions do not exist.\n';
|
|
result += '### FUNCTIONS NOT FOUND ###\n';
|
|
result += missingSkills.join('\n');
|
|
console.log(result)
|
|
return result;
|
|
}
|
|
|
|
const eslint = new ESLint();
|
|
const results = await eslint.lintText(code);
|
|
const codeLines = code.split('\n');
|
|
const exceptions = results.map(r => r.messages).flat();
|
|
|
|
if (exceptions.length > 0) {
|
|
exceptions.forEach((exc, index) => {
|
|
if (exc.line && exc.column ) {
|
|
const errorLine = codeLines[exc.line - 1]?.trim() || 'Unable to retrieve error line content';
|
|
result += `#ERROR ${index + 1}\n`;
|
|
result += `Message: ${exc.message}\n`;
|
|
result += `Location: Line ${exc.line}, Column ${exc.column}\n`;
|
|
result += `Related Code Line: ${errorLine}\n`;
|
|
}
|
|
});
|
|
result += 'The code contains exceptions and cannot continue execution.';
|
|
} else {
|
|
return null;//no error
|
|
}
|
|
|
|
return result ;
|
|
}
|
|
// write custom code to file and import it
|
|
// write custom code to file and prepare for evaluation
|
|
async _stageCode(code) {
|
|
code = this._sanitizeCode(code);
|
|
let src = '';
|
|
code = code.replaceAll('console.log(', 'log(bot,');
|
|
code = code.replaceAll('log("', 'log(bot,"');
|
|
|
|
console.log(`Generated code: """${code}"""`);
|
|
|
|
// this may cause problems in callback functions
|
|
code = code.replaceAll(';\n', '; if(bot.interrupt_code) {log(bot, "Code interrupted.");return;}\n');
|
|
for (let line of code.split('\n')) {
|
|
src += ` ${line}\n`;
|
|
}
|
|
let src_lint_copy = this.code_lint_template.replace('/* CODE HERE */', src);
|
|
src = this.code_template.replace('/* CODE HERE */', src);
|
|
|
|
let filename = this.file_counter + '.js';
|
|
// if (this.file_counter > 0) {
|
|
// let prev_filename = this.fp + (this.file_counter-1) + '.js';
|
|
// unlink(prev_filename, (err) => {
|
|
// console.log("deleted file " + prev_filename);
|
|
// if (err) console.error(err);
|
|
// });
|
|
// } commented for now, useful to keep files for debugging
|
|
this.file_counter++;
|
|
|
|
let write_result = await this._writeFilePromise('.' + this.fp + filename, src);
|
|
// This is where we determine the environment the agent's code should be exposed to.
|
|
// It will only have access to these things, (in addition to basic javascript objects like Array, Object, etc.)
|
|
// Note that the code may be able to modify the exposed objects.
|
|
const compartment = makeCompartment({
|
|
skills,
|
|
log: skills.log,
|
|
world,
|
|
Vec3,
|
|
});
|
|
const mainFn = compartment.evaluate(src);
|
|
|
|
if (write_result) {
|
|
console.error('Error writing code execution file: ' + result);
|
|
return null;
|
|
}
|
|
return { func:{main: mainFn}, src_lint_copy: src_lint_copy };
|
|
}
|
|
|
|
_sanitizeCode(code) {
|
|
code = code.trim();
|
|
const remove_strs = ['Javascript', 'javascript', 'js']
|
|
for (let r of remove_strs) {
|
|
if (code.startsWith(r)) {
|
|
code = code.slice(r.length);
|
|
return code;
|
|
}
|
|
}
|
|
return code;
|
|
}
|
|
|
|
_writeFilePromise(filename, src) {
|
|
// makes it so we can await this function
|
|
return new Promise((resolve, reject) => {
|
|
writeFile(filename, src, (err) => {
|
|
if (err) {
|
|
reject(err);
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
} |