199 lines
8.0 KiB
JavaScript
199 lines
8.0 KiB
JavaScript
import OpenAI from 'openai';
|
|
import 'dotenv/config';
|
|
import EventEmitter from 'events';
|
|
import path from 'path';
|
|
import fs from 'fs/promises';
|
|
import { fileURLToPath } from 'node:url';
|
|
import chalk from 'chalk';
|
|
|
|
async function loadTools() {
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const toolsDir = path.join(__dirname, "tools");
|
|
const dirents = await fs.readdir(toolsDir, { withFileTypes: true });
|
|
const toolEntries = await Promise.all(
|
|
dirents
|
|
.filter((dirent) => dirent.isFile() && dirent.name.endsWith(".js"))
|
|
.map(async (dirent) => {
|
|
const fileName = dirent.name.replace(/\.js$/, "");
|
|
const module = await import(`file://${path.join(toolsDir, dirent.name)}`);
|
|
return [fileName, { def: module.default, run: module.run }];
|
|
})
|
|
);
|
|
return Object.fromEntries(toolEntries);
|
|
}
|
|
const toolsByFile = await loadTools();
|
|
|
|
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
|
|
|
const systemprompt = {"role": "developer", "content": [ { "type": "input_text","text":
|
|
|
|
`You are a helpful assistant.`
|
|
|
|
}]};
|
|
|
|
if (!Array.fromAsync) {
|
|
Array.fromAsync = async function fromAsync(asyncIterable) {
|
|
const array = [];
|
|
for await (const item of asyncIterable) {
|
|
array.push(item);
|
|
}
|
|
return array;
|
|
};
|
|
}
|
|
|
|
class ModelDialog {
|
|
constructor(options) {
|
|
this.options = options;
|
|
this.messages = [systemprompt];
|
|
this.messagesSent = [];
|
|
this.isActive = false;
|
|
this.currentStream = null;
|
|
this.previousResponseId = null;
|
|
this.emitter = new EventEmitter();
|
|
this.inputTokens = {};
|
|
this.outputTokens = {};
|
|
this.cachedTokens = {};
|
|
this.lastDebouncedUpdate = 0;
|
|
};
|
|
|
|
handleUsage = (usage, model) => {
|
|
if (typeof this.inputTokens[model] !== 'number') this.inputTokens[model] = 0;
|
|
if (typeof this.outputTokens[model] !== 'number') this.outputTokens[model] = 0;
|
|
if (typeof this.cachedTokens[model] !== 'number') this.cachedTokens[model] = 0;
|
|
this.inputTokens[model] += usage.input_tokens - usage.input_tokens_details.cached_tokens;
|
|
this.outputTokens[model] += usage.output_tokens;
|
|
this.cachedTokens[model] += usage.input_tokens_details.cached_tokens;
|
|
}
|
|
|
|
on = (event, callback) => {
|
|
const debounceTime = 1000; // 1 second
|
|
|
|
const debouncedCallback = (...args) => {
|
|
const now = Date.now();
|
|
if (now - this.lastDebouncedUpdate >= debounceTime) {
|
|
this.lastDebouncedUpdate = now;
|
|
callback(...args);
|
|
}
|
|
};
|
|
|
|
this.emitter.on(event, debouncedCallback);
|
|
}
|
|
|
|
interrogate = async (prompt) => {
|
|
if(this.isActive) return;
|
|
this.isActive = true;
|
|
|
|
this.messages.push({"role": "user", "content": [ {"type": "input_text","text": prompt }]});
|
|
|
|
const outputs = [];
|
|
|
|
do{
|
|
const messagesToSend = this.messages.splice(0);
|
|
console.log(chalk.blue('sending messages:'),messagesToSend.length);
|
|
//console.log(chalk.blue('messages:'),JSON.stringify(messagesToSend,null,2));
|
|
this.messagesSent.push(...messagesToSend);
|
|
|
|
const model = this.options.model || 'gpt-5-mini';
|
|
if(!['gpt-5', 'gpt-5-mini', 'gpt-5-nano', 'gpt-4.1', 'gpt-4.1-mini'].includes(model)){
|
|
throw new Error('Invalid model: ' + model);
|
|
}
|
|
|
|
const call = {
|
|
model: model,
|
|
input: messagesToSend,
|
|
text: { format: { type: 'text' } },
|
|
tools: Object.values(toolsByFile).map(t => t.def),
|
|
store: true,
|
|
previous_response_id: this.previousResponseId,
|
|
parallel_tool_calls: true,
|
|
include: ['reasoning.encrypted_content']
|
|
}
|
|
if(model.startsWith('gpt-5')){
|
|
call.reasoning = { effort: 'low', summary: 'detailed' };
|
|
//call.text.format.verbosity = 'low';
|
|
}
|
|
|
|
|
|
this.currentStream = openai.responses.stream(call);
|
|
|
|
this.currentStream.on('response.created', (event) => {
|
|
this.previousResponseId = event.response.id;
|
|
});
|
|
|
|
const deltas = [];
|
|
this.currentStream.on('response.output_text.delta', (event) => {
|
|
deltas.push(event.delta);
|
|
this.emitter.emit('outputUpdate', deltas.join(''));
|
|
});
|
|
|
|
const reasoningDeltas = [];
|
|
this.currentStream.on('response.reasoning_summary_text.delta', (event) => {
|
|
if(!reasoningDeltas[event.summary_index]) reasoningDeltas[event.summary_index] = [];
|
|
reasoningDeltas[event.summary_index].push(event.delta);
|
|
this.emitter.emit('reasoningUpdate', reasoningDeltas[event.summary_index].join(''));
|
|
});
|
|
|
|
this.currentStream.on('response.reasoning_summary_text.done', (event) => {
|
|
//console.log(event);
|
|
});
|
|
this.currentStream.on('response.function_call_arguments.delta', (event) => {
|
|
process.stdout.write(chalk.yellow(event.delta));
|
|
});
|
|
this.currentStream.on('response.function_call_arguments.done', (event) => {
|
|
process.stdout.write("\n");
|
|
});
|
|
|
|
|
|
this.currentStream.on('response.completed', async (event) => {
|
|
//console.log(chalk.blue('response completed:'),event.response.usage);
|
|
this.handleUsage(event.response.usage, event.response.model);
|
|
outputs.push(...event.response.output);
|
|
|
|
for(const toolCall of event.response.output.filter(i => i.type === 'function_call')){
|
|
// Limit the 'arguments' field to 400 characters for logging
|
|
const limitedArgs = typeof toolCall.arguments === 'string'
|
|
? (toolCall.arguments.length > 400 ? toolCall.arguments.slice(0, 400) + '...[truncated]' : toolCall.arguments)
|
|
: toolCall.arguments;
|
|
|
|
const tool = toolsByFile[toolCall.name];
|
|
let args;
|
|
try{
|
|
args = JSON.parse(toolCall.arguments);
|
|
} catch(e){
|
|
console.error(chalk.red('Error parsing arguments:'), e, toolCall.arguments);
|
|
this.messages.push({
|
|
type: "function_call_output",
|
|
call_id: toolCall.call_id,
|
|
output: {error: 'Exception in parsing arguments', exception: e},
|
|
});
|
|
continue;
|
|
}
|
|
const result = await tool.run(args);
|
|
console.log(chalk.green('function call result:'),'<toolCall.name>',toolCall.name,'</toolCall.name>\n','<args>',limitedArgs,'</args>\n','<result>',JSON.stringify(result).slice(0,100),'...</result>');
|
|
this.messages.push({
|
|
type: "function_call_output",
|
|
call_id: toolCall.call_id,
|
|
output: JSON.stringify(result),
|
|
});
|
|
}
|
|
});
|
|
|
|
await Array.fromAsync(this.currentStream);
|
|
|
|
console.log(chalk.green('Tico'),[Object.values(this.inputTokens),Object.values(this.cachedTokens),Object.values(this.outputTokens)]);
|
|
console.log(chalk.green('Do we need to loop? messages in array = '),this.messages.length)
|
|
|
|
} while(this.messages.length > 0);
|
|
|
|
this.isActive = false;
|
|
this.lastDebouncedUpdate = 0;
|
|
return {
|
|
output: outputs.filter(i => i.type === 'message').map(i => i.content[0].text) ,
|
|
reasoning: outputs.filter(i => i.type === 'reasoning').map(i => i.summary.map(j => j.text).join('\n')),
|
|
inputTokens: this.inputTokens, outputTokens: this.outputTokens, cachedTokens: this.cachedTokens
|
|
};
|
|
}
|
|
}
|
|
|
|
export default ModelDialog;
|