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() { 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) => { 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); this.messagesSent.push(...messagesToSend); const call = { model: 'gpt-5-nano', input: messagesToSend, text: { format: { type: 'text' }, verbosity: 'low' }, reasoning: { effort: 'medium', summary: 'detailed' }, tools: Object.values(toolsByFile).map(t => t.def), store: true, previous_response_id: this.previousResponseId } 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) => { 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,'\n','',limitedArgs,'\n','',JSON.stringify(result).slice(0,100),'...'); 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('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;