mirror of
https://github.com/HeyPuter/puter.git
synced 2026-02-21 14:09:13 -06:00
dev: ai command tool use / function calling (#1194)
* Enhanced ai command to perfom other commands * Enhance AI Command in Puter's shell * Enahanced ai command to use tooling/function calling * Fixed circular dependency and added list function to Builtincommand * Fixed circular dependency and system prompt
This commit is contained in:
@@ -47,6 +47,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@heyputer/putility": "^1.0.2",
|
||||
"dedent": "^1.5.3",
|
||||
"javascript-time-ago": "^2.5.11",
|
||||
"json-colorizer": "^3.0.1",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
import { Exit } from './coreutil_lib/exit.js';
|
||||
|
||||
|
||||
export default {
|
||||
name: 'ai',
|
||||
usage: 'ai PROMPT',
|
||||
@@ -41,7 +42,46 @@ export default {
|
||||
await ctx.externs.err.write('ai: prompt must be wrapped in quotes\n');
|
||||
throw new Exit(1);
|
||||
}
|
||||
|
||||
const tools = [];
|
||||
|
||||
const commands = await ctx.externs.commandProvider.list();
|
||||
|
||||
for (const command of commands) {
|
||||
if (command.args && command.args.options) {
|
||||
const parameters = {
|
||||
type: "object",
|
||||
properties: {},
|
||||
required: []
|
||||
};
|
||||
|
||||
for (const [optName, opt] of Object.entries(command.args.options)) {
|
||||
parameters.properties[optName] = {
|
||||
type: opt.type === 'boolean' ? 'boolean' : 'string',
|
||||
description: opt.description,
|
||||
default: opt.default
|
||||
};
|
||||
}
|
||||
|
||||
if (command.args.allowPositionals) {
|
||||
parameters.properties.path = {
|
||||
type: "string",
|
||||
description: "Path or name to operate on"
|
||||
};
|
||||
parameters.required.push("path");
|
||||
}
|
||||
|
||||
tools.push({
|
||||
type: "function",
|
||||
function: {
|
||||
name: command.name,
|
||||
description: command.description,
|
||||
parameters: parameters,
|
||||
strict: true
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
const { drivers } = ctx.platform;
|
||||
const { chatHistory } = ctx.plugins;
|
||||
|
||||
@@ -54,21 +94,15 @@ export default {
|
||||
...chatHistory.get_messages(),
|
||||
{
|
||||
role: 'system',
|
||||
content: `You are a helpful AI assistant that helps users with shell commands.
|
||||
When a user asks to perform an action:
|
||||
1. If the action requires a command, wrap ONLY the command between %%% markers
|
||||
2. Keep the command simple and on a single line
|
||||
3. Do not ask for confirmation
|
||||
Example:
|
||||
User: "create a directory named test"
|
||||
You: "Creating directory 'test'
|
||||
%%%mkdir test%%%"`
|
||||
content: `You are a helpful AI assistant that helps users with shell commands. Use the provided tools to execute commands. `
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt,
|
||||
}
|
||||
],
|
||||
tools: tools,
|
||||
stream: true
|
||||
};
|
||||
|
||||
console.log('THESE ARE THE MESSAGES', a_args.messages);
|
||||
@@ -79,63 +113,97 @@ export default {
|
||||
args: a_args,
|
||||
});
|
||||
|
||||
const resobj = JSON.parse(await result.text(), null, 2);
|
||||
|
||||
if ( resobj.success !== true ) {
|
||||
await ctx.externs.err.write('request failed\n');
|
||||
await ctx.externs.err.write(resobj);
|
||||
return;
|
||||
}
|
||||
|
||||
const message = resobj?.result?.message?.content;
|
||||
|
||||
if ( ! message ) {
|
||||
await ctx.externs.err.write('message not found in response\n');
|
||||
await ctx.externs.err.write(result);
|
||||
return;
|
||||
}
|
||||
|
||||
const responseText = await result.text();
|
||||
const lines = responseText.split('\n').filter(line => line.trim());
|
||||
|
||||
chatHistory.add_message(resobj?.result?.message);
|
||||
|
||||
const commandMatch = message.match(/%%%(.*?)%%%/);
|
||||
|
||||
if (commandMatch) {
|
||||
const commandToExecute = commandMatch[1].trim();
|
||||
const cleanMessage = message.replace(/%%%(.*?)%%%/, '');
|
||||
|
||||
await ctx.externs.out.write(cleanMessage + '\n');
|
||||
|
||||
await ctx.externs.out.write(`Execute command: '${commandToExecute}' (y/n): `);
|
||||
|
||||
let fullMessage = '';
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
let line, done;
|
||||
const next_line = async () => {
|
||||
({ value: line, done } = await ctx.externs.in_.read());
|
||||
const chunk = JSON.parse(line);
|
||||
|
||||
if (chunk.type === 'text') {
|
||||
fullMessage += chunk.text;
|
||||
await ctx.externs.out.write(chunk.text);
|
||||
}
|
||||
|
||||
else if (chunk.type === 'tool_use' && chunk.name) {
|
||||
const args = chunk.input;
|
||||
const command = await ctx.externs.commandProvider.lookup(chunk.name);
|
||||
|
||||
await next_line();
|
||||
if (command) {
|
||||
let cmdString = chunk.name;
|
||||
|
||||
if (command.args && command.args.options) {
|
||||
for (const [optName, value] of Object.entries(args)) {
|
||||
if (optName !== 'path' && value === true) {
|
||||
cmdString += ` --${optName}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const inputString = new TextDecoder().decode(line);
|
||||
const response = (inputString ?? '').trim().toLowerCase();
|
||||
if (args.path) {
|
||||
cmdString += ` ${args.path}`;
|
||||
}
|
||||
|
||||
console.log('processed response', {response});
|
||||
await ctx.externs.out.write(`\nExecuting: ${cmdString}\n`);
|
||||
await ctx.externs.out.write('Proceed? (y/n): ');
|
||||
|
||||
if (!response.startsWith('y')) {
|
||||
await ctx.externs.out.write('\nCommand execution cancelled\n');
|
||||
return;
|
||||
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(cmdString);
|
||||
|
||||
await drivers.call({
|
||||
interface: 'puter-chat-completion',
|
||||
method: 'complete',
|
||||
args: {
|
||||
messages: [
|
||||
...chatHistory.get_messages(),
|
||||
{
|
||||
role: "tool",
|
||||
tool_call_id: chunk.id,
|
||||
content: `Command executed successfully: ${cmdString}`
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
fullMessage += `Command executed successfully: ${cmdString}`;
|
||||
} catch(error) {
|
||||
await ctx.externs.err.write(`Error executing command: ${error.message}\n`);
|
||||
fullMessage += `Failed to execute command: ${error.message}`;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
await ctx.externs.out.write('Operation cancelled.\n');
|
||||
fullMessage += 'Operation cancelled';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await ctx.externs.out.write('\n');
|
||||
await ctx.shell.runPipeline(commandToExecute);
|
||||
await ctx.externs.out.write(`Command executed: ${commandToExecute}\n`);
|
||||
} catch (error) {
|
||||
await ctx.externs.err.write(`Error executing command: ${error.message}\n`);
|
||||
return;
|
||||
await ctx.externs.err.write(`Error parsing chunk: ${error.message}\n`);
|
||||
throw new Exit(1);
|
||||
}
|
||||
} else {
|
||||
await ctx.externs.out.write(message + '\n');
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
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
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -23,12 +23,6 @@ export const CreateChatHistoryPlugin = ctx => {
|
||||
content:
|
||||
'You are running inside the Puter terminal via the `ai` command. Refer to yourself as Puter Terminal AI.',
|
||||
},
|
||||
{
|
||||
role: 'system',
|
||||
content:
|
||||
// note: this really doesn't work at all; GPT is effectively incapable of following this instruction.
|
||||
'You can provide commands to the user by prefixing a line in your response with %%%. The user will then be able to run the command by accepting confirmation.',
|
||||
},
|
||||
{
|
||||
role: 'system',
|
||||
content:
|
||||
|
||||
@@ -36,4 +36,12 @@ export class BuiltinCommandProvider {
|
||||
return Object.keys(builtins)
|
||||
.filter(commandName => commandName.startsWith(query));
|
||||
}
|
||||
|
||||
async list() {
|
||||
return Object.entries(builtins).map(([name, command]) => ({
|
||||
name,
|
||||
...command
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -54,4 +54,15 @@ export class CompositeCommandProvider {
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
async list() {
|
||||
const results = [];
|
||||
for (const provider of this.providers) {
|
||||
if (typeof provider.list === 'function') {
|
||||
const commands = await provider.list();
|
||||
results.push(...commands);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user