164 lines
5.2 KiB
JavaScript
164 lines
5.2 KiB
JavaScript
#!/usr/bin/env node
|
|
import 'dotenv/config';
|
|
import OpenAI from 'openai';
|
|
|
|
import { promises as fs } from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
import path from "node:path";
|
|
import { resourceUsage } from 'node:process';
|
|
|
|
|
|
function renderUsage(usage) {
|
|
const inputTokens = usage.input_tokens - usage.input_tokens_details.cached_tokens;
|
|
const cacheTokens = usage.input_tokens_details.cached_tokens;
|
|
const outputToken = usage.output_tokens;
|
|
console.log('renderUsage', inputTokens, cacheTokens, outputToken);
|
|
}
|
|
|
|
function printIndented(indentNum, ...args) {
|
|
const indent = ' '.repeat(indentNum);
|
|
const output = args.map(arg => {
|
|
if (typeof arg === 'string') return arg;
|
|
try {
|
|
return JSON.stringify(arg, null, 2);
|
|
} catch {
|
|
return String(arg);
|
|
}
|
|
}).join(' ');
|
|
// Indent every line
|
|
console.log(output.split('\n').map(line => indent + line).join('\n'));
|
|
}
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
|
async function loadTools() {
|
|
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);
|
|
}
|
|
|
|
streamOnce(new OpenAI({ apiKey: process.env.OPENAI_API_KEY }), 'Zeig mir die Dateiein in / und lege index.html an mit dummydaten, kurz');
|
|
|
|
|
|
let counter = 0;
|
|
async function streamOnce(openai, userText) {
|
|
const toolsByFile = await loadTools();
|
|
let previousResponseId;
|
|
|
|
let input = [
|
|
{ "role": "developer", "content": [ {"type": "input_text","text": `You are an interactive CLI AI assistant. Follow the user's instructions.
|
|
If a tool is available and relevant, plan to use it.
|
|
Be explicit when information is undefined.
|
|
Do not silently fall back: surface errors.
|
|
|
|
Prefer concise answers.
|
|
|
|
Developer rules:
|
|
- Null tells the truth. If data is missing/undefined, say so; do not invent values.
|
|
- In development, never hide errors; include warnings if using fallbacks.
|
|
|
|
Behavior:
|
|
- Answer succinctly.
|
|
- Ask for clarification when the user input is ambiguous.
|
|
- Output plain text suitable for a terminal.
|
|
` }] },
|
|
{"role": "user", "content": [ { "type": "input_text", "text": userText } ]},
|
|
]
|
|
|
|
while(input.length > 0){
|
|
console.log('input:', input.length);
|
|
|
|
const call = {
|
|
model: 'gpt-5-mini',
|
|
input: input,
|
|
text: { format: { type: 'text' }, verbosity: 'low' },
|
|
reasoning: { effort: 'low', summary: 'detailed' },
|
|
tools: Object.values(toolsByFile).map(t => t.def),
|
|
store: true,
|
|
}
|
|
if(previousResponseId) call.previous_response_id = previousResponseId;
|
|
|
|
console.log("------NEW OPENAI CALL--------------"
|
|
,"\n",counter++,"\n",'----INPUT-----------------'
|
|
,"\n",call.input.map(i => JSON.stringify(i)),"\n",
|
|
'--------CALL-------------',"\n");
|
|
const stream = await openai.responses.stream(call);
|
|
stream.on('response.created', (event) => {
|
|
previousResponseId = event.response.id;
|
|
});
|
|
stream.on('response.reasoning_summary_text.delta', (event) => {
|
|
////process.stdout.write(event.delta);
|
|
});
|
|
stream.on('response.reasoning_summary_text.done', () => {
|
|
process.stdout.write('\n');
|
|
//clear on next delta
|
|
});
|
|
|
|
stream.on('response.output_text.delta', (event) => {
|
|
////process.stdout.write(event.delta);
|
|
});
|
|
|
|
|
|
stream.on('response.output_item.added', (event) => {
|
|
if(event.item && event.item.type === 'function_call'){
|
|
//console.log('function call:', event.item);
|
|
}
|
|
});
|
|
stream.on('response.function_call_arguments.delta', (event) => {
|
|
////process.stdout.write(event.delta);
|
|
});
|
|
|
|
const functionCalls = [];
|
|
|
|
stream.on('response.output_item.done', async (event) => {
|
|
if(event.item && event.item.type === 'function_call'){
|
|
const id = event.item.call_id;
|
|
const name = event.item.name;
|
|
let args = {};
|
|
try {
|
|
args = JSON.parse(event.item.arguments);
|
|
} catch (e){
|
|
console.error('Error parsing arguments:', e, event.item.arguments);
|
|
}
|
|
console.log('function call:', id, name);
|
|
functionCalls.push({ id, name, args, promise: toolsByFile[name].run(args) });
|
|
}
|
|
});
|
|
|
|
stream.on('response.completed', async (event) => {
|
|
printIndented(10, 'response.completed',
|
|
renderUsage(event.response.usage),
|
|
"Result:",event.response.output.filter(i => i.type === 'message').map(i => i.content[0].text)
|
|
);
|
|
});
|
|
|
|
await Array.fromAsync(stream);
|
|
input=[];
|
|
|
|
for (const call of functionCalls) {
|
|
try {
|
|
const result = await call.promise;
|
|
input.push({
|
|
type: "function_call_output",
|
|
call_id: call.id,
|
|
output: JSON.stringify(result),
|
|
})
|
|
printIndented(10,'function call result:',result);
|
|
} catch (err) {
|
|
console.error('Error in function call:', call.name, err);
|
|
}
|
|
}
|
|
}
|
|
|
|
//console.log('OPENAI STREAM FINISHED');
|
|
}
|