Adapt `ai` command to use tool use / function calling
Recently the AI command gained the ability to run commands. If we used the streaming mode of the puter-chat-completion driver interface it would be possible to the AI command to take multiple steps and command execution might be slightly more reliable.
For more details, refer to this previous issue
Hello @KernelDeimos can I work on this Issue?
@KernelDeimos i would like to continue with this issue as well.
@ntwari-bruce gets first dibs on this for working on #1156, but @Sashank-Singh if you'd like to work with Ntwari on this and he says it's okay then I can assign both of you
Yes, I would like to work on it too. Thanks @KernelDeimos
@KernelDeimos yes you can assign both of us
Thanks for confirming, I've assigned both of you
Hello @kernelDeimos,
I hope you’re doing well. I’ve been testing an implementation where I focused on adding a single command, "mkdir", as a starting point to validate my approach. I wanted your thoughts on it and see if you have suggestions for alternative methods or improvements.
I’m currently using the tools I defined in my setup, and I’ve attached the code below for your review.
`
let fullMessage = '';
for (const line of lines) {
try {
const chunk = JSON.parse(line);
if (chunk.type === 'text') {
fullMessage += chunk.text;
await ctx.externs.out.write(chunk.text);
}
if (chunk.type === 'tool_use') {
if (chunk.name === 'mkdir') {
const args = chunk.input;
await ctx.externs.out.write(`\nCreating directory: ${args.path}\n`);
await ctx.externs.out.write('Proceed? (y/n): ');
let { value: line } = await ctx.externs.in_.read();
const inputString = new TextDecoder().decode(line);
const response = inputString.trim().toLowerCase();
await ctx.externs.out.write('\n');
if (response.startsWith('y')) {
try {
await ctx.shell.runPipeline(`mkdir ${args.parents ? '-p' : ''} ${args.path}`);
await drivers.call({
interface: 'puter-chat-completion',
method: 'complete',
args: {
messages: [
...chatHistory.get_messages(),
{
role: "tool",
tool_call_id: chunk.id,
content: `Directory '${args.path}' created successfully`
}
]
}
});
fullMessage += `Directory '${args.path}' created successfully`;
} catch(error) {
await ctx.externs.out.write('\n');
await ctx.externs.err.write(`Error creating directory: ${error.message}\n`);
fullMessage += `Failed to create directory: ${error.message}`;
return;
}
} else {
await ctx.externs.out.write('\nOperation cancelled.\n');
fullMessage += 'Operation cancelled';
}
}
}
} catch (error) {
await ctx.externs.err.write(`Error parsing chunk: ${error.message}\n`);
throw new Exit(1);
}
}
await ctx.externs.out.write('\n');
if (!fullMessage) {
await ctx.externs.err.write('message not found in response\n');
return;
}
chatHistory.add_message({
role: 'assistant',
content: fullMessage
});
`
@KernelDeimos
This is the tool i used.
const tools = [{ type: "function", function: { name: "mkdir", description: "Create a new directory", parameters: { type: "object", properties: { path: { type: "string", description: "Directory name or path to create" }, parents: { type: "boolean", description: "Create parent directories if they don't exitst", default: false } }, required: ["path"] }, strict: true } }]
The schema of each command would be better as metadata on the command itself (for builtin commands at least, external commands would need a different approach but we can consider that to be out of scope for now). Then you should be able to lookup commands and generate a list of tools. It might require exposing the command provider like I did with the shell.