2025-02-08 22:41:07 -08:00
import { readFileSync , mkdirSync , writeFileSync } from 'fs' ;
import { Examples } from '../utils/examples.js' ;
import { getCommandDocs } from '../agent/commands/index.js' ;
import { SkillLibrary } from "../agent/library/skill_library.js" ;
import { stringifyTurns } from '../utils/text.js' ;
import { getCommand } from '../agent/commands/index.js' ;
import settings from '../../settings.js' ;
import { Gemini } from './gemini.js' ;
import { GPT } from './gpt.js' ;
import { Claude } from './claude.js' ;
import { Mistral } from './mistral.js' ;
import { ReplicateAPI } from './replicate.js' ;
import { Local } from './local.js' ;
import { Novita } from './novita.js' ;
import { GroqCloudAPI } from './groq.js' ;
import { HuggingFace } from './huggingface.js' ;
import { Qwen } from "./qwen.js" ;
import { Grok } from "./grok.js" ;
import { DeepSeek } from './deepseek.js' ;
2025-03-04 16:51:32 -08:00
import { Hyperbolic } from './hyperbolic.js' ;
import { GLHF } from './glhf.js' ;
2025-02-17 15:59:17 -06:00
import { OpenRouter } from './openrouter.js' ;
2025-02-08 22:41:07 -08:00
export class Prompter {
constructor ( agent , fp ) {
this . agent = agent ;
this . profile = JSON . parse ( readFileSync ( fp , 'utf8' ) ) ;
let default _profile = JSON . parse ( readFileSync ( './profiles/defaults/_default.json' , 'utf8' ) ) ;
let base _fp = settings . base _profile ;
let base _profile = JSON . parse ( readFileSync ( base _fp , 'utf8' ) ) ;
// first use defaults to fill in missing values in the base profile
for ( let key in default _profile ) {
if ( base _profile [ key ] === undefined )
base _profile [ key ] = default _profile [ key ] ;
}
// then use base profile to fill in missing values in the individual profile
for ( let key in base _profile ) {
if ( this . profile [ key ] === undefined )
this . profile [ key ] = base _profile [ key ] ;
}
// base overrides default, individual overrides base
this . convo _examples = null ;
this . coding _examples = null ;
let name = this . profile . name ;
this . cooldown = this . profile . cooldown ? this . profile . cooldown : 0 ;
this . last _prompt _time = 0 ;
this . awaiting _coding = false ;
// try to get "max_tokens" parameter, else null
let max _tokens = null ;
if ( this . profile . max _tokens )
max _tokens = this . profile . max _tokens ;
let chat _model _profile = this . _selectAPI ( this . profile . model ) ;
this . chat _model = this . _createModel ( chat _model _profile ) ;
if ( this . profile . code _model ) {
let code _model _profile = this . _selectAPI ( this . profile . code _model ) ;
this . code _model = this . _createModel ( code _model _profile ) ;
}
else {
this . code _model = this . chat _model ;
}
let embedding = this . profile . embedding ;
if ( embedding === undefined ) {
if ( chat _model _profile . api !== 'ollama' )
embedding = { api : chat _model _profile . api } ;
else
embedding = { api : 'none' } ;
}
else if ( typeof embedding === 'string' || embedding instanceof String )
embedding = { api : embedding } ;
console . log ( 'Using embedding settings:' , embedding ) ;
try {
if ( embedding . api === 'google' )
this . embedding _model = new Gemini ( embedding . model , embedding . url ) ;
else if ( embedding . api === 'openai' )
this . embedding _model = new GPT ( embedding . model , embedding . url ) ;
else if ( embedding . api === 'replicate' )
this . embedding _model = new ReplicateAPI ( embedding . model , embedding . url ) ;
else if ( embedding . api === 'ollama' )
this . embedding _model = new Local ( embedding . model , embedding . url ) ;
else if ( embedding . api === 'qwen' )
this . embedding _model = new Qwen ( embedding . model , embedding . url ) ;
else if ( embedding . api === 'mistral' )
this . embedding _model = new Mistral ( embedding . model , embedding . url ) ;
2025-02-17 13:13:45 -06:00
else if ( embedding . api === 'huggingface' )
this . embedding _model = new HuggingFace ( embedding . model , embedding . url ) ;
else if ( embedding . api === 'novita' )
this . embedding _model = new Novita ( embedding . model , embedding . url ) ;
2025-02-08 22:41:07 -08:00
else {
this . embedding _model = null ;
2025-02-17 13:13:45 -06:00
let embedding _name = embedding ? embedding . api : '[NOT SPECIFIED]'
console . warn ( 'Unsupported embedding: ' + embedding _name + '. Using word-overlap instead, expect reduced performance. Recommend using a supported embedding model. See Readme.' ) ;
2025-02-08 22:41:07 -08:00
}
}
catch ( err ) {
2025-02-17 13:13:45 -06:00
console . warn ( 'Warning: Failed to initialize embedding model:' , err . message ) ;
console . log ( 'Continuing anyway, using word-overlap instead.' ) ;
2025-02-08 22:41:07 -08:00
this . embedding _model = null ;
}
this . skill _libary = new SkillLibrary ( agent , this . embedding _model ) ;
mkdirSync ( ` ./bots/ ${ name } ` , { recursive : true } ) ;
writeFileSync ( ` ./bots/ ${ name } /last_profile.json ` , JSON . stringify ( this . profile , null , 4 ) , ( err ) => {
if ( err ) {
throw new Error ( 'Failed to save profile:' , err ) ;
}
console . log ( "Copy profile saved." ) ;
} ) ;
}
_selectAPI ( profile ) {
if ( typeof profile === 'string' || profile instanceof String ) {
profile = { model : profile } ;
}
if ( ! profile . api ) {
2025-03-01 13:40:24 -08:00
if ( profile . model . includes ( 'openrouter/' ) )
profile . api = 'openrouter' ; // must do first because shares names with other models
else if ( profile . model . includes ( 'ollama/' ) )
profile . api = 'ollama' ; // also must do early because shares names with other models
else if ( profile . model . includes ( 'gemini' ) )
2025-02-08 22:41:07 -08:00
profile . api = 'google' ;
else if ( profile . model . includes ( 'gpt' ) || profile . model . includes ( 'o1' ) || profile . model . includes ( 'o3' ) )
profile . api = 'openai' ;
else if ( profile . model . includes ( 'claude' ) )
profile . api = 'anthropic' ;
else if ( profile . model . includes ( 'huggingface/' ) )
profile . api = "huggingface" ;
else if ( profile . model . includes ( 'replicate/' ) )
profile . api = 'replicate' ;
else if ( profile . model . includes ( 'mistralai/' ) || profile . model . includes ( "mistral/" ) )
model _profile . api = 'mistral' ;
else if ( profile . model . includes ( "groq/" ) || profile . model . includes ( "groqcloud/" ) )
profile . api = 'groq' ;
2025-03-05 14:40:42 -08:00
else if ( profile . model . includes ( "glhf/" )
2025-03-04 16:51:32 -08:00
profile . api = 'glhf' ;
2025-03-05 14:40:42 -08:00
else if ( profile . model . includes ( "hyperbolic/" )
2025-03-04 16:51:32 -08:00
profile . api = 'hyperbolic' ;
2025-02-08 22:41:07 -08:00
else if ( profile . model . includes ( 'novita/' ) )
profile . api = 'novita' ;
else if ( profile . model . includes ( 'qwen' ) )
profile . api = 'qwen' ;
else if ( profile . model . includes ( 'grok' ) )
profile . api = 'xai' ;
else if ( profile . model . includes ( 'deepseek' ) )
profile . api = 'deepseek' ;
2025-03-01 13:40:24 -08:00
else
2025-03-04 16:51:32 -08:00
throw new Error ( 'Unknown model:' , profile . model , 'Did you check the name is correct?' ) ; // Asks the user if the name is correct
2025-02-08 22:41:07 -08:00
}
return profile ;
}
_createModel ( profile ) {
let model = null ;
if ( profile . api === 'google' )
model = new Gemini ( profile . model , profile . url , profile . params ) ;
else if ( profile . api === 'openai' )
model = new GPT ( profile . model , profile . url , profile . params ) ;
else if ( profile . api === 'anthropic' )
model = new Claude ( profile . model , profile . url , profile . params ) ;
else if ( profile . api === 'replicate' )
2025-02-17 15:59:17 -06:00
model = new ReplicateAPI ( profile . model . replace ( 'replicate/' , '' ) , profile . url , profile . params ) ;
2025-02-08 22:41:07 -08:00
else if ( profile . api === 'ollama' )
2025-03-01 13:40:24 -08:00
model = new Local ( profile . model . replace ( 'ollama/' , '' ) , profile . url , profile . params ) ;
2025-02-08 22:41:07 -08:00
else if ( profile . api === 'mistral' )
model = new Mistral ( profile . model , profile . url , profile . params ) ;
else if ( profile . api === 'groq' )
model = new GroqCloudAPI ( profile . model . replace ( 'groq/' , '' ) . replace ( 'groqcloud/' , '' ) , profile . url , profile . params ) ;
else if ( profile . api === 'huggingface' )
model = new HuggingFace ( profile . model , profile . url , profile . params ) ;
2025-03-04 16:51:32 -08:00
else if ( profile . api === 'glhf' )
model = new GLHF ( profile . model . replace ( 'glhf/' , '' ) , profile . url , profile . params ) ;
else if ( profile . api === 'hyperbolic' )
model = new Hyperbolic ( profile . model . replace ( 'hyperbolic/' , '' ) , profile . url , profile . params ) ;
2025-02-08 22:41:07 -08:00
else if ( profile . api === 'novita' )
model = new Novita ( profile . model . replace ( 'novita/' , '' ) , profile . url , profile . params ) ;
else if ( profile . api === 'qwen' )
model = new Qwen ( profile . model , profile . url , profile . params ) ;
else if ( profile . api === 'xai' )
model = new Grok ( profile . model , profile . url , profile . params ) ;
else if ( profile . api === 'deepseek' )
model = new DeepSeek ( profile . model , profile . url , profile . params ) ;
2025-02-17 15:59:17 -06:00
else if ( profile . api === 'openrouter' )
model = new OpenRouter ( profile . model . replace ( 'openrouter/' , '' ) , profile . url , profile . params ) ;
2025-02-08 22:41:07 -08:00
else
throw new Error ( 'Unknown API:' , profile . api ) ;
return model ;
}
getName ( ) {
return this . profile . name ;
}
getInitModes ( ) {
return this . profile . modes ;
}
async initExamples ( ) {
try {
this . convo _examples = new Examples ( this . embedding _model , settings . num _examples ) ;
this . coding _examples = new Examples ( this . embedding _model , settings . num _examples ) ;
// Wait for both examples to load before proceeding
await Promise . all ( [
this . convo _examples . load ( this . profile . conversation _examples ) ,
this . coding _examples . load ( this . profile . coding _examples ) ,
this . skill _libary . initSkillLibrary ( )
2025-02-17 16:37:54 -06:00
] ) . catch ( error => {
// Preserve error details
console . error ( 'Failed to initialize examples. Error details:' , error ) ;
console . error ( 'Stack trace:' , error . stack ) ;
throw error ;
} ) ;
2025-02-08 22:41:07 -08:00
console . log ( 'Examples initialized.' ) ;
} catch ( error ) {
console . error ( 'Failed to initialize examples:' , error ) ;
2025-02-17 16:37:54 -06:00
console . error ( 'Stack trace:' , error . stack ) ;
throw error ; // Re-throw with preserved details
2025-02-08 22:41:07 -08:00
}
}
async replaceStrings ( prompt , messages , examples = null , to _summarize = [ ] , last _goals = null ) {
prompt = prompt . replaceAll ( '$NAME' , this . agent . name ) ;
if ( prompt . includes ( '$STATS' ) ) {
let stats = await getCommand ( '!stats' ) . perform ( this . agent ) ;
prompt = prompt . replaceAll ( '$STATS' , stats ) ;
}
if ( prompt . includes ( '$INVENTORY' ) ) {
let inventory = await getCommand ( '!inventory' ) . perform ( this . agent ) ;
prompt = prompt . replaceAll ( '$INVENTORY' , inventory ) ;
}
if ( prompt . includes ( '$ACTION' ) ) {
prompt = prompt . replaceAll ( '$ACTION' , this . agent . actions . currentActionLabel ) ;
}
if ( prompt . includes ( '$COMMAND_DOCS' ) )
prompt = prompt . replaceAll ( '$COMMAND_DOCS' , getCommandDocs ( ) ) ;
if ( prompt . includes ( '$CODE_DOCS' ) ) {
const code _task _content = messages . slice ( ) . reverse ( ) . find ( msg =>
msg . role !== 'system' && msg . content . includes ( '!newAction(' )
) ? . content ? . match ( /!newAction\((.*?)\)/ ) ? . [ 1 ] || '' ;
prompt = prompt . replaceAll (
'$CODE_DOCS' ,
await this . skill _libary . getRelevantSkillDocs ( code _task _content , settings . relevant _docs _count )
) ;
}
if ( prompt . includes ( '$EXAMPLES' ) && examples !== null )
prompt = prompt . replaceAll ( '$EXAMPLES' , await examples . createExampleMessage ( messages ) ) ;
if ( prompt . includes ( '$MEMORY' ) )
prompt = prompt . replaceAll ( '$MEMORY' , this . agent . history . memory ) ;
if ( prompt . includes ( '$TO_SUMMARIZE' ) )
prompt = prompt . replaceAll ( '$TO_SUMMARIZE' , stringifyTurns ( to _summarize ) ) ;
if ( prompt . includes ( '$CONVO' ) )
prompt = prompt . replaceAll ( '$CONVO' , 'Recent conversation:\n' + stringifyTurns ( messages ) ) ;
if ( prompt . includes ( '$SELF_PROMPT' ) ) {
2025-02-27 21:22:40 -08:00
// 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 ` : '' ;
2025-02-08 22:41:07 -08:00
prompt = prompt . replaceAll ( '$SELF_PROMPT' , self _prompt ) ;
}
if ( prompt . includes ( '$LAST_GOALS' ) ) {
let goal _text = '' ;
for ( let goal in last _goals ) {
if ( last _goals [ goal ] )
goal _text += ` You recently successfully completed the goal ${ goal } . \n `
else
goal _text += ` You recently failed to complete the goal ${ goal } . \n `
}
prompt = prompt . replaceAll ( '$LAST_GOALS' , goal _text . trim ( ) ) ;
}
if ( prompt . includes ( '$BLUEPRINTS' ) ) {
if ( this . agent . npc . constructions ) {
let blueprints = '' ;
for ( let blueprint in this . agent . npc . constructions ) {
blueprints += blueprint + ', ' ;
}
prompt = prompt . replaceAll ( '$BLUEPRINTS' , blueprints . slice ( 0 , - 2 ) ) ;
}
}
// check if there are any remaining placeholders with syntax $<word>
let remaining = prompt . match ( /\$[A-Z_]+/g ) ;
if ( remaining !== null ) {
console . warn ( 'Unknown prompt placeholders:' , remaining . join ( ', ' ) ) ;
}
return prompt ;
}
async checkCooldown ( ) {
let elapsed = Date . now ( ) - this . last _prompt _time ;
if ( elapsed < this . cooldown && this . cooldown > 0 ) {
await new Promise ( r => setTimeout ( r , this . cooldown - elapsed ) ) ;
}
this . last _prompt _time = Date . now ( ) ;
}
async promptConvo ( messages ) {
this . most _recent _msg _time = Date . now ( ) ;
let current _msg _time = this . most _recent _msg _time ;
for ( let i = 0 ; i < 3 ; i ++ ) { // try 3 times to avoid hallucinations
await this . checkCooldown ( ) ;
if ( current _msg _time !== this . most _recent _msg _time ) {
return '' ;
}
let prompt = this . profile . conversing ;
prompt = await this . replaceStrings ( prompt , messages , this . convo _examples ) ;
let generation = await this . chat _model . sendRequest ( messages , prompt ) ;
// in conversations >2 players LLMs tend to hallucinate and role-play as other bots
// the FROM OTHER BOT tag should never be generated by the LLM
if ( generation . includes ( '(FROM OTHER BOT)' ) ) {
console . warn ( 'LLM hallucinated message as another bot. Trying again...' ) ;
continue ;
}
if ( current _msg _time !== this . most _recent _msg _time ) {
console . warn ( this . agent . name + ' received new message while generating, discarding old response.' ) ;
return '' ;
}
return generation ;
}
return '' ;
}
async promptCoding ( messages ) {
if ( this . awaiting _coding ) {
console . warn ( 'Already awaiting coding response, returning no response.' ) ;
return '```//no response```' ;
}
this . awaiting _coding = true ;
await this . checkCooldown ( ) ;
let prompt = this . profile . coding ;
prompt = await this . replaceStrings ( prompt , messages , this . coding _examples ) ;
let resp = await this . code _model . sendRequest ( messages , prompt ) ;
this . awaiting _coding = false ;
return resp ;
}
async promptMemSaving ( to _summarize ) {
await this . checkCooldown ( ) ;
let prompt = this . profile . saving _memory ;
prompt = await this . replaceStrings ( prompt , null , null , to _summarize ) ;
return await this . chat _model . sendRequest ( [ ] , prompt ) ;
}
async promptShouldRespondToBot ( new _message ) {
await this . checkCooldown ( ) ;
let prompt = this . profile . bot _responder ;
let messages = this . agent . history . getHistory ( ) ;
messages . push ( { role : 'user' , content : new _message } ) ;
prompt = await this . replaceStrings ( prompt , null , null , messages ) ;
let res = await this . chat _model . sendRequest ( [ ] , prompt ) ;
return res . trim ( ) . toLowerCase ( ) === 'respond' ;
}
async promptGoalSetting ( messages , last _goals ) {
let system _message = this . profile . goal _setting ;
system _message = await this . replaceStrings ( system _message , messages ) ;
let user _message = 'Use the below info to determine what goal to target next\n\n' ;
user _message += '$LAST_GOALS\n$STATS\n$INVENTORY\n$CONVO'
user _message = await this . replaceStrings ( user _message , messages , null , null , last _goals ) ;
let user _messages = [ { role : 'user' , content : user _message } ] ;
let res = await this . chat _model . sendRequest ( user _messages , system _message ) ;
let goal = null ;
try {
let data = res . split ( '```' ) [ 1 ] . replace ( 'json' , '' ) . trim ( ) ;
goal = JSON . parse ( data ) ;
} catch ( err ) {
console . log ( 'Failed to parse goal:' , res , err ) ;
}
if ( ! goal || ! goal . name || ! goal . quantity || isNaN ( parseInt ( goal . quantity ) ) ) {
console . log ( 'Failed to set goal:' , res ) ;
return null ;
}
goal . quantity = parseInt ( goal . quantity ) ;
return goal ;
}
}