multiple commands execution and sanity checks

This commit is contained in:
tcsenpai 2024-08-08 01:56:47 +02:00
parent 740150434f
commit 54280090c0
3 changed files with 177 additions and 66 deletions

View File

@ -5,8 +5,10 @@ OllamaAgents is a TypeScript-based CLI application that provides a Linux command
## Features
- Convert natural language queries to Linux commands
- Generate single or multiple commands as needed
- Provide explanations for generated commands
- Offer cautions for potentially dangerous operations
- Optional command execution
- Colorful and interactive CLI interface
## Prerequisites
@ -37,15 +39,26 @@ OllamaAgents is a TypeScript-based CLI application that provides a Linux command
To start the application, run:
```
yarn start
```
`yarn start`
Once started, you can enter natural language queries or commands. The application will interpret your input and provide the corresponding Linux command, along with an explanation and any necessary cautions.
Once started, you can enter natural language queries or commands. The application will interpret your input and provide the corresponding Linux command(s), along with an explanation and any necessary cautions.
To execute a command, prefix your input with '!'. For example:
`!list files in current directory`
This will interpret the command, show you the Linux command(s) it plans to run, and then execute them, displaying the output.
Type 'exit' to quit the application.
## Warning and Disclaimer
**CAUTION:** This application can execute system commands. Be extremely careful when using the execution feature, especially with commands that modify your system or files. Always review the generated commands before execution and ensure you understand their effects.
The authors and contributors of OllamaAgents are not responsible for any damage or data loss caused by the execution of commands through this application. Use at your own risk.
## Development
This project uses TypeScript and is set up with TSX for running TypeScript files directly. The main entry point is `main.ts`.
@ -74,5 +87,6 @@ Contributions, issues, and feature requests are welcome. Feel free to check issu
## Acknowledgments
- Ollama for providing the underlying language model capabilities
- The TypeScript and Node.js communities for their excellent tools and libraries
- Ollama for providing the underlying language model capabilities and API
- Claude Sonnet 3.5 and Cursor IDE for providing such a great AI model and IDE
- All the enthusiasts who keep the open-source community alive

70
main.ts
View File

@ -1,41 +1,33 @@
// Import required modules and classes
import { OllamaAPI } from './ollamapi';
import dotenv from 'dotenv';
import readline from 'readline';
import * as term from 'terminal-kit';
// Load environment variables
dotenv.config();
// Initialize OllamaAPI with the URL from environment variables
const ollama_url = process.env.OLLAMA_URL as string;
const systemPrompt = `You are a Linux command interpreter. Your task is to convert natural language queries or commands into appropriate Linux commands. Always respond with a valid JSON object containing the following keys:
1. 'commands': An array of strings, each representing a Linux command to execute. If multiple commands are needed, list them in the order they should be executed.
2. 'explanation': A brief explanation of what the command(s) do (string).
3. 'caution': Any warnings or cautions about using the command(s), if applicable (string or null).
Guidelines:
- Provide a single command when possible, but use multiple commands or command chains (using pipes | or && ) when necessary to achieve the desired result.
- If suggesting multiple commands, explain how they work together.
- Always use existing and working Linux commands.
- If you cannot interpret the input or if it's not applicable to Linux, return a JSON object with an 'error' key explaining the issue.
Do not include any text outside of the JSON structure in your response.
The produced JSON should be valid and parseable by JSON.parse() in JavaScript. For such reason, you should not include \` and \`\`\`json in your response, or similar syntax.`;
const ollama = new OllamaAPI(ollama_url, 'llama3.1', systemPrompt);
const ollama = new OllamaAPI(ollama_url, 'llama3.1');
// Create readline interface for user input
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
async function main() {
// Display welcome message
term.terminal.bold.cyan('Welcome to the Linux Command Assistant. Type \'exit\' to quit.\n\n');
while (true) {
// Prompt user for input
term.terminal.green('Enter your command or question: \n');
term.terminal.bold("Remember to prefix your command with '!' to execute commands.\n");
const input = await new Promise<string>(resolve => rl.question('', resolve));
term.terminal('\n');
// Check if user wants to exit
if (input.toLowerCase() === 'exit') {
term.terminal.yellow('Goodbye!\n');
rl.close();
@ -43,23 +35,48 @@ async function main() {
}
try {
const response = await ollama.chat(input);
const parsedResponse = JSON.parse(response);
// Check if the command should be executed
const executeCommand = input.startsWith('!');
const cleanInput = executeCommand ? input.slice(1) : input;
// Send request to OllamaAPI
const response = await ollama.chat(cleanInput, executeCommand);
if (parsedResponse.error) {
term.terminal.red('Error: ').white(parsedResponse.error + '\n');
if (response.error) {
// Display error if any
term.terminal.red('Error: ').white(response.error + '\n');
} else {
// Display commands
term.terminal.bold.blue('Command(s):\n\n');
parsedResponse.commands.forEach((cmd: string, index: number) => {
response.commands.forEach((cmd: string) => {
term.terminal.white(`${cmd}\n`);
});
term.terminal('\n\n');
term.terminal.bold.magenta('Explanation: ').white(parsedResponse.explanation + '\n');
if (parsedResponse.caution) {
term.terminal.bold.yellow('Caution: ').white(parsedResponse.caution + '\n');
term.terminal('\n');
// Display explanation
term.terminal.bold.magenta('Explanation: ').white(response.explanation + '\n');
// Display caution if any
if (response.caution) {
term.terminal.bold.yellow('Caution: ').white(response.caution + '\n');
}
// Display execution output or error if command was executed
if (executeCommand) {
if (response.execution_results) {
term.terminal.bold.green('Execution Results:\n');
response.execution_results.forEach(result => {
term.terminal.bold.blue(`Command: ${result.command}\n`);
term.terminal.white(`Output:\n${result.output}\n\n`);
});
}
if (response.error) {
term.terminal.bold.red('Execution Error:\n').white(response.error + '\n');
}
}
}
} catch (error) {
// Display any unexpected errors
term.terminal.red('An error occurred: ').white((error as Error).message + '\n');
}
@ -67,4 +84,5 @@ async function main() {
}
}
// Start the application
main();

View File

@ -1,69 +1,148 @@
import axios from 'axios';
import { exec } from 'child_process';
/**
* Represents a message in the chat conversation.
*/
interface Message {
role: 'system' | 'user' | 'assistant';
content: string;
}
/**
* Represents the parsed response from the Ollama API.
*/
interface ParsedResponse {
commands: string[];
explanation: string;
caution: string | null;
error?: string;
execution_results?: { command: string, output: string }[];
}
/**
* OllamaAPI class for interacting with the Ollama API and executing Linux commands.
*/
export class OllamaAPI {
private baseURL: string;
private model: string;
private memory: Message[] = [];
private systemPrompt: string;
constructor(ollamaURL: string, model: string = 'llama2', systemPrompt: string = '') {
/**
* Creates an instance of OllamaAPI.
* @param {string} ollamaURL - The base URL for the Ollama API.
* @param {string} [model='llama2'] - The model to use for chat completions.
*/
constructor(ollamaURL: string, model: string = 'llama2') {
this.baseURL = ollamaURL;
this.model = model;
this.systemPrompt = systemPrompt;
if (systemPrompt) {
this.memory.push({ role: 'system', content: systemPrompt });
}
this.systemPrompt = `You are a Linux command interpreter. Your task is to convert natural language queries or commands into appropriate Linux commands. Always respond with a valid JSON object containing the following keys:
1. 'commands': An array of strings, each representing a Linux command to execute. If multiple commands are needed, list them in the order they should be executed.
2. 'explanation': A brief explanation of what the command(s) do (string).
3. 'caution': Any warnings or cautions about using the command(s), if applicable (string or null).
Guidelines:
- Provide a single command when possible, but use multiple commands or command chains (using pipes | or && ) when necessary to achieve the desired result.
- If suggesting multiple commands, explain how they work together.
- Always use existing and working Linux commands.
- If you cannot interpret the input or if it's not applicable to Linux, return a JSON object with an 'error' key explaining the issue.
Important: Do not include any text outside of the JSON structure in your response. Do not use markdown formatting, code block delimiters, or any other syntax that is not part of the JSON object. The response should be a raw JSON object that can be directly parsed by JSON.parse() in JavaScript.
The response should start with '{' and end with '}' without any additional characters before or after.`;
}
async chat(prompt: string): Promise<string> {
/**
* Sends a chat request to the Ollama API and optionally executes the returned commands.
* @param {string} prompt - The user's input prompt.
* @param {boolean} [executeCommands=false] - Whether to execute the returned commands.
* @returns {Promise<ParsedResponse>} The parsed response from the API.
*/
async chat(prompt: string, executeCommands: boolean = false): Promise<ParsedResponse> {
try {
//this.memory.push({ role: 'user', content: prompt });
let systemAndPrompt: Message[] = []
systemAndPrompt.push({ role: 'system', content: this.systemPrompt })
systemAndPrompt.push({ role: 'user', content: prompt })
const messages: Message[] = [
{ role: 'system', content: this.systemPrompt },
{ role: 'user', content: prompt }
];
const response = await axios.post(`${this.baseURL}/api/chat`, {
model: this.model,
messages: systemAndPrompt,
messages: messages,
stream: false
});
const assistantResponse = response.data.message.content;
//this.memory.push({ role: 'assistant', content: assistantResponse });
return assistantResponse;
const parsedResponse = this.parseResponse(assistantResponse);
if (executeCommands && parsedResponse.commands.length > 0) {
try {
parsedResponse.execution_results = await this.executeCommands(parsedResponse.commands);
} catch (execError) {
parsedResponse.error = (execError as Error).message;
}
}
return parsedResponse;
} catch (error) {
console.error('Error in chat:', error);
throw error;
}
}
async complete(prompt: string): Promise<string> {
/**
* Parses the response from the Ollama API.
* @param {string} response - The raw response from the API.
* @returns {ParsedResponse} The parsed response object.
* @private
*/
private parseResponse(response: string): ParsedResponse {
try {
const fullPrompt = this.memory.map(m => `${m.role}: ${m.content}`).join('\n') + `\nuser: ${prompt}\nassistant:`;
const response = await axios.post(`${this.baseURL}/api/generate`, {
model: this.model,
prompt: fullPrompt,
stream: false
});
const assistantResponse = response.data.response;
//this.memory.push({ role: 'user', content: prompt });
//this.memory.push({ role: 'assistant', content: assistantResponse });
return assistantResponse;
// Remove markdown code block delimiters if present
const cleanedResponse = response.replace(/^```json\s*|\s*```$/g, '').trim();
const parsedResponse = JSON.parse(cleanedResponse);
return {
commands: parsedResponse.commands || [],
explanation: parsedResponse.explanation || '',
caution: parsedResponse.caution || null,
error: parsedResponse.error
};
} catch (error) {
console.error('Error in complete:', error);
throw error;
console.error('Error parsing response:', error);
return {
commands: [],
explanation: '',
caution: null,
error: 'Failed to parse the response from the AI model.'
};
}
}
clearMemory() {
this.memory = this.systemPrompt ? [{ role: 'system', content: this.systemPrompt }] : [];
}
setSystemPrompt(prompt: string) {
this.systemPrompt = prompt;
this.clearMemory();
/**
* Executes the given Linux commands separately.
* @param {string[]} commands - An array of Linux commands to execute.
* @returns {Promise<{command: string, output: string}[]>} An array of objects containing each command and its output.
* @private
*/
private async executeCommands(commands: string[]): Promise<{ command: string, output: string }[]> {
const results = [];
for (const command of commands) {
try {
const output = await new Promise<string>((resolve, reject) => {
exec(command, (error, stdout, stderr) => {
if (error) {
reject(`Error: ${error.message}`);
return;
}
if (stderr) {
reject(`Error: ${stderr}`);
return;
}
resolve(stdout);
});
});
results.push({ command, output });
} catch (error) {
results.push({ command, output: `Error: ${error}` });
}
}
return results;
}
}