diff --git a/bots/codeCheckTemplate.js b/bots/codeCheckTemplate.js new file mode 100644 index 0000000..77b5d97 --- /dev/null +++ b/bots/codeCheckTemplate.js @@ -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.'); +} \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..e1506fd --- /dev/null +++ b/eslint.config.js @@ -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. + }, + }, +]; diff --git a/package.json b/package.json index 97aa04a..1ab48a1 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/settings.js b/settings.js index b38dede..b090838 100644 --- a/settings.js +++ b/settings.js @@ -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 diff --git a/src/agent/action_manager.js b/src/agent/action_manager.js index 833f3c0..c016e70 100644 --- a/src/agent/action_manager.js +++ b/src/agent/action_manager.js @@ -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) { @@ -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; } diff --git a/src/agent/coder.js b/src/agent/coder.js index f4b7219..d418829 100644 --- a/src/agent/coder.js +++ b/src/agent/coder.js @@ -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) { @@ -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); @@ -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'; @@ -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.) @@ -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) { @@ -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}; @@ -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 }; } @@ -170,5 +225,4 @@ export class Coder { } return { success: false, message: null, interrupted: false, timedout: true }; } - } \ No newline at end of file diff --git a/src/agent/library/index.js b/src/agent/library/index.js index 677dc11..ae864b0 100644 --- a/src/agent/library/index.js +++ b/src/agent/library/index.js @@ -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; } diff --git a/src/agent/prompter.js b/src/agent/prompter.js index 107019a..bf81a10 100644 --- a/src/agent/prompter.js +++ b/src/agent/prompter.js @@ -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'; @@ -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; @@ -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; @@ -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); @@ -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')) diff --git a/src/models/qwen.js b/src/models/qwen.js index d3d7bec..d546298 100644 --- a/src/models/qwen.js +++ b/src/models/qwen.js @@ -49,12 +49,12 @@ export class Qwen { async embed(text) { if (!text || typeof text !== 'string') { - console.error('Invalid embedding input: text must be a non-empty string.'); + console.error('Invalid embedding input: text must be a non-empty string:', text); return 'Invalid embedding input: text must be a non-empty string.'; } const data = { - model: 'text-embedding-v2', + model: this.modelName, input: { texts: [text] }, parameters: { text_type: 'query' }, }; @@ -67,38 +67,68 @@ export class Qwen { try { const response = await this._makeHttpRequest(this.url, data); const embedding = response?.output?.embeddings?.[0]?.embedding; + return embedding || 'No embedding result received.'; } catch (err) { - console.error('Error occurred:', err); + console.log('Embed data:', data); + console.error('Embed error occurred:', err); return 'An error occurred, please try again.'; } } - async _makeHttpRequest(url, data) { + async _makeHttpRequest(url, data, maxRetries = 10) { const headers = { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json', }; - const response = await fetch(url, { - method: 'POST', - headers, - body: JSON.stringify(data), - }); - - if (!response.ok) { - const errorText = await response.text(); - console.error(`Request failed, status code ${response.status}: ${response.statusText}`); - console.error('Error response content:', errorText); - throw new Error(`Request failed, status code ${response.status}: ${response.statusText}`); - } - - const responseText = await response.text(); - try { - return JSON.parse(responseText); - } catch (err) { - console.error('Failed to parse response JSON:', err); - throw new Error('Invalid response JSON format.'); + let retryCount = 0; + + while (retryCount < maxRetries) { + try { + const response = await fetch(url, { + method: 'POST', + headers, + body: JSON.stringify(data), + }); + + if (response.ok) { + const responseText = await response.text(); + try { + //Task completed successfully + return JSON.parse(responseText); + } catch (err) { + console.error('Failed to parse response JSON:', err); + throw new Error('Invalid response JSON format.'); + } + } else { + const errorText = await response.text(); + + if (response.status === 429 || response.statusText.includes('Too Many Requests')) { + // Handle rate limiting + retryCount++; + if (retryCount >= maxRetries) { + console.error('Exceeded maximum retry attempts, unable to get request result.'); + throw new Error(`Request failed after ${maxRetries} retries due to rate limiting.`); + } + //Reached Qwen concurrency limit, waiting in queue + const waitTime = Math.random() * 1000; // Random wait between 0 to 1 seconds + await new Promise(resolve => setTimeout(resolve, waitTime)); + continue; // Retry the request + } else { + console.error(`Request failed, status code ${response.status}: ${response.statusText}`); + console.error('Error response content:', errorText); + throw new Error(`Request failed, status code ${response.status}: ${response.statusText}`); + } + } + } catch (err) { + // Handle network errors or other exceptions + console.error('Error occurred during HTTP request:', err); + throw err; // Re-throw the error to be handled by the caller + } } + // Exceeded maximum retries + console.error('Exceeded maximum retry attempts, unable to get request result.'); + throw new Error(`Request failed after ${maxRetries} retries.`); } }