Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Code capability enhancement & Bot crash fix #272

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions bots/codeCheckTemplate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as skills from '../../../src/agent/library/skills.js';
import * as world from '../../../src/agent/library/world.js';
import Vec3 from 'vec3';

const log = skills.log;

export async function main(bot) {
/* CODE HERE */
log(bot, 'Code finished.');
}
25 changes: 25 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// eslint.config.js
import globals from "globals";
import pluginJs from "@eslint/js";

/** @type {import('eslint').Linter.Config[]} */
export default [
// First, import the recommended configuration
pluginJs.configs.recommended,

// Then override or customize specific rules
{
languageOptions: {
globals: globals.browser,
ecmaVersion: 2021,
sourceType: "module",
},
rules: {
"no-undef": "error", // Disallow the use of undeclared variables or functions.
"semi": ["error", "always"], // Require the use of semicolons at the end of statements.
"curly": "warn", // Enforce the use of curly braces around blocks of code.
"no-unused-vars": "off", // Disable warnings for unused variables.
"no-unreachable": "off", // Disable warnings for unreachable code.
},
},
];
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,10 @@
"scripts": {
"postinstall": "patch-package",
"start": "node main.js"
},
"devDependencies": {
"@eslint/js": "^9.13.0",
"eslint": "^9.13.0",
"globals": "^15.11.0"
}
}
2 changes: 1 addition & 1 deletion settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default
"show_bot_views": false, // show bot's view in browser at localhost:3000, 3001...

"allow_insecure_coding": false, // allows newAction command and model can write/run code on your computer. enable at own risk
"code_timeout_mins": 10, // minutes code is allowed to run. -1 for no timeout
"code_timeout_mins": 3, // minutes code is allowed to run. -1 for no timeout,set 3.Set 3 min to timely code adjustments

"max_messages": 15, // max number of messages to keep in context
"max_commands": -1, // max number of commands to use in a response. -1 for no limit
Expand Down
6 changes: 4 additions & 2 deletions src/agent/action_manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,9 @@ export class ActionManager {
console.error("Code execution triggered catch: " + err);
await this.stop();

let message = this._getBotOutputSummary() + '!!Code threw exception!! Error: ' + err;
err = err.toString();
let relevant_skill_docs = await this.agent.prompter.getRelevantSkillDocs(err,5);
let message = this._getBotOutputSummary() + '!!Code threw exception!! Error: ' + err+'\n'+relevant_skill_docs;
let interrupted = this.agent.bot.interrupt_code;
this.agent.clearBotLogs();
if (!interrupted && !this.agent.coder.generating) {
Expand All @@ -131,7 +133,7 @@ export class ActionManager {
First outputs:\n${output.substring(0, MAX_OUT / 2)}\n...skipping many lines.\nFinal outputs:\n ${output.substring(output.length - MAX_OUT / 2)}`;
}
else {
output = 'Code output:\n' + output;
output = 'Code output:\n' + output.toString();
}
return output;
}
Expand Down
72 changes: 63 additions & 9 deletions src/agent/coder.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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) {
Expand All @@ -12,15 +13,62 @@ export class Coder {
this.fp = '/bots/'+agent.name+'/action-code/';
this.generating = false;
this.code_template = '';
this.code_chack_template = '';

readFile('./bots/template.js', 'utf8', (err, data) => {
if (err) throw err;
this.code_template = data;
});

readFile('./bots/codeCheckTemplate.js', 'utf8', (err, data) => {
if (err) throw err;
this.code_chack_template = data;
});
mkdirSync('.' + this.fp, { recursive: true });
}

async checkCode(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.getRelevantSkillDocs();
//Check if the function exists
const missingSkills = skills.filter(skill => !allDocs.includes(skill));
if (missingSkills.length > 0) {
result += 'These functions do not exist. Please modify the correct function name and try again.\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);
Expand All @@ -35,6 +83,7 @@ export class Coder {
for (let line of code.split('\n')) {
src += ` ${line}\n`;
}
let src_check_copy = this.code_chack_template.replace('/* CODE HERE */', src);
src = this.code_template.replace('/* CODE HERE */', src);

let filename = this.file_counter + '.js';
Expand All @@ -46,7 +95,7 @@ export class Coder {
// });
// } 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.)
Expand All @@ -63,8 +112,7 @@ export class Coder {
console.error('Error writing code execution file: ' + result);
return null;
}

return { main: mainFn };
return { func:{main: mainFn}, src_check_copy: src_check_copy };
}

sanitizeCode(code) {
Expand Down Expand Up @@ -140,8 +188,15 @@ export class Coder {
continue;
}
code = res.substring(res.indexOf('```')+3, res.lastIndexOf('```'));

const executionModuleExports = await this.stageCode(code);
const result = await this.stageCode(code);
const executionModuleExports = result.func;
let src_check_copy = result.src_check_copy;
const analysisResult = await this.checkCode(src_check_copy);
if (analysisResult) {
const message = 'Error: Code syntax error. Please try again:'+'\n'+analysisResult+'\n'+await this.agent.prompter.getRelevantSkillDocs(analysisResult,3);
messages.push({ role: 'system', content: message });
continue;
}
if (!executionModuleExports) {
agent_history.add('system', 'Failed to stage code, something is wrong.');
return {success: false, message: null, interrupted: false, timedout: false};
Expand All @@ -152,10 +207,10 @@ export class Coder {
}, { timeout: settings.code_timeout_mins });
if (code_return.interrupted && !code_return.timedout)
return { success: false, message: null, interrupted: true, timedout: false };
console.log("Code generation result:", code_return.success, code_return.message);
console.log("Code generation result:", code_return.success, code_return.message.toString());

if (code_return.success) {
const summary = "Summary of newAction\nAgent wrote this code: \n```" + this.sanitizeCode(code) + "```\nCode Output:\n" + code_return.message;
const summary = "Summary of newAction\nAgent wrote this code: \n```" + this.sanitizeCode(code) + "```\nCode Output:\n" + code_return.message.toString();
return { success: true, message: summary, interrupted: false, timedout: false };
}

Expand All @@ -170,5 +225,4 @@ export class Coder {
}
return { success: false, message: null, interrupted: false, timedout: true };
}

}
19 changes: 10 additions & 9 deletions src/agent/library/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,21 @@ import * as world from './world.js';


export function docHelper(functions, module_name) {
let docstring = '';
let docArray = [];
for (let skillFunc of functions) {
let str = skillFunc.toString();
if (str.includes('/**')){
docstring += module_name+'.'+skillFunc.name;
docstring += str.substring(str.indexOf('/**')+3, str.indexOf('**/')) + '\n';
if (str.includes('/**')) {
let docEntry = `${module_name}.${skillFunc.name}\n`;
docEntry += str.substring(str.indexOf('/**') + 3, str.indexOf('**/')).trim();
docArray.push(docEntry);
}
}
return docstring;
return docArray;
}

export function getSkillDocs() {
let docstring = "\n*SKILL DOCS\nThese skills are javascript functions that can be called when writing actions and skills.\n";
docstring += docHelper(Object.values(skills), 'skills');
docstring += docHelper(Object.values(world), 'world');
return docstring + '*\n';
let docArray = [];
docArray = docArray.concat(docHelper(Object.values(skills), 'skills'));
docArray = docArray.concat(docHelper(Object.values(world), 'world'));
return docArray;
}
77 changes: 64 additions & 13 deletions src/agent/prompter.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { readFileSync, mkdirSync, writeFileSync} from 'fs';
import { Examples } from '../utils/examples.js';
import { getCommandDocs } from './commands/index.js';
import { getSkillDocs } from './library/index.js';
import { stringifyTurns } from '../utils/text.js';
import { getCommand } from './commands/index.js';
import {mkdirSync, readFileSync, writeFileSync} from 'fs';
import {Examples} from '../utils/examples.js';
import {getCommand, getCommandDocs} from './commands/index.js';
import {getSkillDocs} from './library/index.js';
import {stringifyTurns} from '../utils/text.js';
import {cosineSimilarity} from '../utils/math.js';

import { Gemini } from '../models/gemini.js';
import { GPT } from '../models/gpt.js';
Expand All @@ -20,7 +20,8 @@ export class Prompter {
this.profile = JSON.parse(readFileSync(fp, 'utf8'));
this.convo_examples = null;
this.coding_examples = null;

this.skill_docs_embeddings = {};

let name = this.profile.name;
let chat = this.profile.model;
this.cooldown = this.profile.cooldown ? this.profile.cooldown : 0;
Expand Down Expand Up @@ -127,13 +128,19 @@ export class Prompter {
try {
this.convo_examples = new Examples(this.embedding_model);
this.coding_examples = new Examples(this.embedding_model);
const [convoResult, codingResult] = await Promise.allSettled([

const results = await Promise.allSettled([
this.convo_examples.load(this.profile.conversation_examples),
this.coding_examples.load(this.profile.coding_examples)
this.coding_examples.load(this.profile.coding_examples),
...getSkillDocs().map(async (doc) => {
let func_name_desc = doc.split('\n').slice(0, 2).join('');
this.skill_docs_embeddings[doc] = await this.embedding_model.embed(func_name_desc);
})
]);

// Handle potential failures
// Handle potential failures for conversation and coding examples
const [convoResult, codingResult, ...skillDocResults] = results;

if (convoResult.status === 'rejected') {
console.error('Failed to load conversation examples:', convoResult.reason);
throw convoResult.reason;
Expand All @@ -142,12 +149,43 @@ export class Prompter {
console.error('Failed to load coding examples:', codingResult.reason);
throw codingResult.reason;
}
skillDocResults.forEach((result, index) => {
if (result.status === 'rejected') {
console.error(`Failed to load skill doc ${index + 1}:`, result.reason);
}
});

} catch (error) {
console.error('Failed to initialize examples:', error);
throw error;
}
}

async getRelevantSkillDocs(message, select_num) {
let latest_message_embedding = '';
if(message) //message is not empty, get the relevant skill docs, else return all skill docs
latest_message_embedding = await this.embedding_model.embed(message);

let skill_doc_similarities = Object.keys(this.skill_docs_embeddings)
.map(doc_key => ({
doc_key,
similarity_score: cosineSimilarity(latest_message_embedding, this.skill_docs_embeddings[doc_key])
}))
.sort((a, b) => b.similarity_score - a.similarity_score);

let length = skill_doc_similarities.length;
if (typeof select_num !== 'number' || isNaN(select_num) || select_num < 0) {
select_num = length;
} else {
select_num = Math.min(Math.floor(select_num), length);
}
let selected_docs = skill_doc_similarities.slice(0, select_num);
let relevant_skill_docs = '#### RELEVENT DOCS INFO ###\nThe following functions are listed in descending order of relevance.\n';
relevant_skill_docs += 'SkillDocs:\n'
relevant_skill_docs += selected_docs.map(doc => `${doc.doc_key}`).join('\n### ');
return relevant_skill_docs;
}

async replaceStrings(prompt, messages, examples=null, to_summarize=[], last_goals=null) {
prompt = prompt.replaceAll('$NAME', this.agent.name);

Expand All @@ -161,8 +199,21 @@ export class Prompter {
}
if (prompt.includes('$COMMAND_DOCS'))
prompt = prompt.replaceAll('$COMMAND_DOCS', getCommandDocs());
if (prompt.includes('$CODE_DOCS'))
prompt = prompt.replaceAll('$CODE_DOCS', getSkillDocs());
if (prompt.includes('$CODE_DOCS')) {
// Find the most recent non-system message containing '!newAction('
let code_task_content = messages.slice().reverse().find(msg =>
msg.role !== 'system' && msg.content.includes('!newAction(')
)?.content || '';

// Extract content between '!newAction(' and ')'
const match = code_task_content.match(/!newAction\((.*?)\)/);
code_task_content = match ? match[1] : '';

prompt = prompt.replaceAll(
'$CODE_DOCS',
await this.getRelevantSkillDocs(code_task_content, 5)
);
}
if (prompt.includes('$EXAMPLES') && examples !== null)
prompt = prompt.replaceAll('$EXAMPLES', await examples.createExampleMessage(messages));
if (prompt.includes('$MEMORY'))
Expand Down
Loading