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_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(); } }); }); } }