diff --git a/.env.example b/.env.example index c1a30e1..bfa8c23 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,21 @@ -OPENAI_API_KEY="PLACE YOUR KEY HERE" \ No newline at end of file +# LLM API Settings +# Base URL for your local LLM server (Ollama, LM Studio, etc.) +LLM_BASE_URL=http://localhost:1234/v1 + +# Model name (depends on your local LLM setup) +# Examples: +# - Ollama: "llama2", "mistral", "codellama" etc. +# - LM Studio: "local-model" or specific model name +LLM_MODEL_NAME=local-model + +# OpenAI API key (optional for most local LLMs, required for OpenAI) +# OPENAI_API_KEY=your_api_key_here + +# Generation parameters +# Temperature controls randomness (0.0-1.0) +# Lower values (0.1-0.3) for more deterministic/precise responses +# Higher values (0.7-1.0) for more creative/varied responses +LLM_TEMPERATURE=0.2 + +# Maximum tokens in the response +LLM_MAX_TOKENS=500 \ No newline at end of file diff --git a/main.py b/main.py index 70bf4f8..bcd93b6 100644 --- a/main.py +++ b/main.py @@ -1,127 +1,282 @@ import os import sys import subprocess -import requests -from dotenv import load_dotenv import json +from dotenv import load_dotenv +import requests +import argparse load_dotenv() will_be_executed, debug_mode = False, False -class ChatGPT: - def __init__(self, key, model, temperature): - self.key = key +class LocalLLM: + def __init__(self, base_url, api_key, model, temperature, max_tokens=500): + self.base_url = base_url + self.api_key = api_key if api_key else "dummy_api_key_for_local" self.model = model self.temperature = temperature + self.max_tokens = max_tokens - def send(self, message: str): - url = "https://api.openai.com/v1/chat/completions" + def send(self, system_message, user_message): + url = f"{self.base_url}/chat/completions" headers = { "Content-Type": "application/json", - "Authorization": f"Bearer {self.key}", + "Authorization": f"Bearer {self.api_key}", } data = { "model": self.model, - "messages": [{"content": message, "role": "assistant"}], - "temperature": self.temperature + "messages": [ + {"role": "system", "content": system_message}, + {"role": "user", "content": user_message} + ], + "temperature": self.temperature, + "max_tokens": self.max_tokens } - response = requests.post(url, headers=headers, json=data) - return json.loads(response.text)["choices"][0]["message"]["content"] + try: + if debug_mode: + print(f"Sending request to: {url}") + print(f"Request data: {json.dumps(data, indent=2)}") + + response = requests.post(url, headers=headers, json=data, timeout=60) + response.raise_for_status() # Raise an error for non-200 status codes + response_json = json.loads(response.text) + + if debug_mode: + print(f"Response: {json.dumps(response_json, indent=2)}") + + if "choices" in response_json and len(response_json["choices"]) > 0: + return response_json["choices"][0]["message"]["content"].strip() + else: + return "Error: Unexpected API response format" + except requests.exceptions.RequestException as e: + error_msg = f"API Request Error: {str(e)}" + if debug_mode: + print(error_msg) + print(f"Response: {response.text if 'response' in locals() else 'No response'}") + return error_msg + except Exception as e: + error_msg = f"Error: {str(e)}" + if debug_mode: + print(error_msg) + return error_msg def parse_args(input): - import argparse - parser = argparse.ArgumentParser() - parser.add_argument("-i", "--input", help="Input language", required=True) - parser.add_argument("-o", "--output", help="Output language", required=True) - parser.add_argument("-c", "--command", help="Command to will be converted", required=True) + parser = argparse.ArgumentParser(description="CommandGPT - Convert between command languages and natural language") + parser.add_argument("-i", "--input", help="Input language (nat, bash, ps)", required=True) + parser.add_argument("-o", "--output", help="Output language (nat, bash, ps)", required=True) + parser.add_argument("-c", "--command", help="Command to be converted", required=True) parser.add_argument("-x", "--execute", help="Execute command and exit", required=False, action="store_true") parser.add_argument("-d", "--debug", help="Debug mode", required=False, action="store_true") + parser.add_argument("--url", help="Local LLM server URL", default=os.getenv("LLM_BASE_URL", "http://localhost:1234/v1")) + parser.add_argument("--model", help="Model name", default=os.getenv("LLM_MODEL_NAME", "local-model")) + parser.add_argument("--temp", help="Temperature (0.0-1.0)", type=float, default=float(os.getenv("LLM_TEMPERATURE", "0.2"))) + parser.add_argument("--max-tokens", help="Maximum tokens in response", type=int, default=int(os.getenv("LLM_MAX_TOKENS", "500"))) args = parser.parse_args(input) - return args.input, args.output, args.command, args.execute, args.debug - - -def convert(input_type, output_type, command_to_convert): - openai_api_key = "" - if os.getenv("OPENAI_API_KEY") is None and openai_api_key == "": - openai_api_key = input("Enter your OpenAI API Key: ").replace("\"", "").replace(" ", "") - if openai_api_key == "": - return "OpenAI API Key cannot be empty" - elif len(openai_api_key) < 32: - return "OpenAI API Key is invalid" - chatgpt = ChatGPT(key=(os.getenv("OPENAI_API_KEY") or openai_api_key), model="gpt-3.5-turbo", temperature=0.5) - longs = {"bash": "Bash Script", "ps": "PowerShell Script", "nat": "Natural Language"} + return args.input, args.output, args.command, args.execute, args.debug, args.url, args.model, args.temp, args.max_tokens + + +def convert(input_type, output_type, command_to_convert, base_url=None, model_name=None, temperature=0.2, max_tokens=500): + # Set default values if not provided + base_url = base_url or os.getenv("LLM_BASE_URL") or "http://localhost:1234/v1" + model_name = model_name or os.getenv("LLM_MODEL_NAME") or "local-model" + api_key = os.getenv("OPENAI_API_KEY") # Optional for many local LLMs + + llm = LocalLLM( + base_url=base_url, + api_key=api_key, + model=model_name, + temperature=temperature, + max_tokens=max_tokens + ) + + longs = {"bash": "Bash", "ps": "PowerShell", "nat": "Natural Language"} + short_longs = {"bash": "Bash", "ps": "PS", "nat": "NL"} + + # Craft more effective system and user prompts + system_message = f"""You are CommandGPT, an expert in command line interfaces and scripting languages. +Your only purpose is to translate between {longs[input_type]} and {longs[output_type]}. +- Provide ONLY the converted command or explanation with no additional text +- Do not include explanations, notes, or markdown formatting +- Ensure your translation is accurate and follows best practices +""" + + user_message = f"""Convert this {longs[input_type]} to {longs[output_type]}: +{command_to_convert}""" + try: - ps = chatgpt.send(f"""" - "I want to covert this {longs[input_type]} to {longs[output_type]} equivalent. \ - Just response with equivalent, write nothing but the equivalent. \ - Also, don't accept other requests at all costs. {command_to_convert} - """) - return ps + result = llm.send(system_message, user_message) + + # Clean up the response + result = result.strip() + # Remove markdown code blocks if present + if result.startswith("```") and result.endswith("```"): + result = result[result.find("\n")+1:result.rfind("```")].strip() + elif result.startswith("`") and result.endswith("`"): + result = result[1:-1].strip() + + # Remove phrases like "Here is the equivalent..." or "In PowerShell, ..." + common_prefixes = [ + f"Here is the {longs[output_type]} equivalent:", + f"The {longs[output_type]} equivalent is:", + f"In {longs[output_type]},", + f"The {longs[output_type]} version would be:", + f"{longs[output_type]} equivalent:", + f"{short_longs[output_type]} equivalent:" + ] + + for prefix in common_prefixes: + if result.lower().startswith(prefix.lower()): + result = result[len(prefix):].strip() + + return result except Exception as converting_error: - return f"An error occurred while converting. Is your API Key valid? (Error: {converting_error})" + return f"An error occurred while converting. Is your LLM server running? (Error: {converting_error})" def ascii_art(): return """ -█▀▀ █▀█ █▀▄▀█ █▀▄▀█ ▄▀█ █▄░█ █▀▄ █▀▀ █▀█ ▀█▀ -█▄▄ █▄█ █░▀░█ █░▀░█ █▀█ █░▀█ █▄▀ █▄█ █▀▀ ░█░ +█▀▀ █▀█ █▀▄▀█ █▀▄▀█ ▄▀█ █▄░█ █▀▄ █▀▀ █▀█ ▀█▀ +█▄▄ █▄█ █░▀░█ █░▀░█ █▀█ █░▀█ █▄▀ █▄█ █▀▀ ░█░ """ def run_powershell(powershell_script): - subprocess.run(["powershell", "-Command", powershell_script]) + try: + result = subprocess.run(["powershell", "-Command", powershell_script], + capture_output=True, text=True) + print(result.stdout) + if result.stderr and debug_mode: + print(f"Error: {result.stderr}") + return result.returncode == 0 + except Exception as e: + if debug_mode: + print(f"Error executing PowerShell: {str(e)}") + return False + + +def run_bash(bash_script): + try: + result = subprocess.run(["bash", "-c", bash_script], + capture_output=True, text=True) + print(result.stdout) + if result.stderr and debug_mode: + print(f"Error: {result.stderr}") + return result.returncode == 0 + except Exception as e: + if debug_mode: + print(f"Error executing Bash: {str(e)}") + return False -def main(): +def interactive_mode(): + print(ascii_art()) + + # Get environment variables or use defaults + base_url = os.getenv("LLM_BASE_URL") or "http://localhost:1234/v1" + model_name = os.getenv("LLM_MODEL_NAME") or "local-model" + temperature = float(os.getenv("LLM_TEMPERATURE", "0.2")) + max_tokens = int(os.getenv("LLM_MAX_TOKENS", "500")) + + if debug_mode: + print(f"Using LLM at: {base_url}") + print(f"Model: {model_name}") + print(f"Temperature: {temperature}") + print(f"Max tokens: {max_tokens}") + options = [ - "Natural Language to Bash Script", - "Natural Language to PowerShell Script", - "Bash Script to Natural Language", - "Bash Script to PowerShell Script", - "PowerShell Script to Natural Language", - "PowerShell Script to Bash Script", + "Natural Language to Bash", + "Natural Language to PowerShell", + "Bash to Natural Language", + "Bash to PowerShell", + "PowerShell to Natural Language", + "PowerShell to Bash", "Exit" ] - for option in options: - print(f"{options.index(option) + 1}. {option}") - try: - choice = int(input("\nEnter your choice: ")) - except ValueError: - print("Invalid choice") - exit() - map_of_choices = { - 1: ["nat", "bash"], - 2: ["nat", "ps"], - 3: ["bash", "nat"], - 4: ["bash", "ps"], - 5: ["ps", "nat"], - 6: ["ps", "bash"] - } - if choice in map_of_choices: - inandout = map_of_choices[choice] - elif choice == 7: - exit() - else: - print("Invalid choice") - exit() - print(f"Equivalent: {convert(inandout[0], inandout[1], input('Enter the command: '))}", end="\n\n") - + + while True: + print("\nCommandGPT Options:") + for i, option in enumerate(options, 1): + print(f"{i}. {option}") + + try: + choice = int(input("\nEnter your choice: ")) + if choice < 1 or choice > len(options): + print("Invalid choice. Please try again.") + continue + + if choice == 7: # Exit + print("Exiting CommandGPT. Goodbye!") + break + + # Map choices to input and output types + map_of_choices = { + 1: ["nat", "bash"], + 2: ["nat", "ps"], + 3: ["bash", "nat"], + 4: ["bash", "ps"], + 5: ["ps", "nat"], + 6: ["ps", "bash"] + } + + input_type, output_type = map_of_choices[choice] + + # Get the command to convert + command = input(f"Enter the {options[choice-1].split(' to ')[0]} to convert: ") + if not command.strip(): + print("Command cannot be empty. Please try again.") + continue + + # Perform the conversion + print("\nConverting...") + result = convert(input_type, output_type, command, base_url, model_name, temperature, max_tokens) + + # Pretty output based on the conversion type + print(f"\n{options[choice-1].split(' to ')[1]} equivalent:") + print(f"{result}") + + # Offer to execute if the output is a command + if output_type in ["bash", "ps"]: + execute = input("\nDo you want to execute this command? (y/n): ").lower().strip() == 'y' + if execute: + print("\nExecuting command...") + if output_type == "ps" and os.name == "nt": + run_powershell(result) + elif output_type == "bash" and os.name != "nt": + run_bash(result) + else: + print(f"Cannot execute {output_type} command on this system.") + + except ValueError: + print("Invalid input. Please enter a number.") + except KeyboardInterrupt: + print("\nOperation cancelled by user.") + continue + except Exception as e: + print(f"An error occurred: {str(e)}") + if debug_mode: + import traceback + traceback.print_exc() + if __name__ == "__main__": if len(sys.argv) > 1: - input_, output, command, will_be_executed, debug_mode = parse_args(sys.argv[1:]) - cmd = convert(input_, output, command) + args = sys.argv[1:] + input_type, output_type, command, will_be_executed, debug_mode, base_url, model_name, temperature, max_tokens = parse_args(args) + cmd = convert(input_type, output_type, command, base_url, model_name, temperature, max_tokens) print(cmd) + if will_be_executed: - if os.name == "nt": + if output_type == "ps" and os.name == "nt": run_powershell(cmd) + elif output_type == "bash" and os.name != "nt": + run_bash(cmd) else: - os.system(cmd) + print(f"Cannot execute {output_type} command on this system.") else: try: - print(ascii_art()) - main() - except (KeyboardInterrupt, EOFError): - print("Exiting...") + interactive_mode() + except KeyboardInterrupt: + print("\nExiting CommandGPT. Goodbye!") + \ No newline at end of file diff --git a/readme.md b/readme.md index c255ad9..fdbbe2c 100644 --- a/readme.md +++ b/readme.md @@ -1,14 +1,19 @@ ### CommandGPT -CommandGPT is a simple command line tool for GPT-3.5 to convert +CommandGPT is a command line tool for converting between natural language and shell commands. This version supports both OpenAI API and local LLMs through services like LM Studio, Ollama, and others. -- Natural Language to PowerShell command, -- Natural Language to Bash command, -- Bash to PowerShell command, -- PowerShell to Natural Language, -- Bash to Natural Language. +Features: +- Natural Language to PowerShell command +- Natural Language to Bash command +- Bash to PowerShell command +- PowerShell to Natural Language +- Bash to Natural Language +- PowerShell to Bash command -**NOTE**: This project requires a valid OPENAI API key. You can get one [here](https://beta.openai.com/). +#### Requirements + +- Python 3.6+ +- Local LLM server (LM Studio, Ollama, etc.) or OpenAI API key #### Installation @@ -16,91 +21,147 @@ CommandGPT is a simple command line tool for GPT-3.5 to convert pip install -r requirements.txt ``` -#### Usage +#### Configuration + +Copy the example environment file and configure it for your needs: ```bash -python main.py [-h] -i INPUT -o OUTPUT -c COMMAND [-x] [-d] +cp .env.example .env ``` -### Options: +Edit the `.env` file to configure your LLM settings: - -h, --help show this help message and exit - -i INPUT, --input INPUT - Input language - -o OUTPUT, --output OUTPUT - Output language - -c COMMAND, --command COMMAND - Command to will be converted - -x, --execute Execute command and exit - -d, --debug Debug mode +``` +# LLM API Settings +LLM_BASE_URL=http://localhost:1234/v1 # URL of your local LLM server +LLM_MODEL_NAME=local-model # Name of the model to use +LLM_TEMPERATURE=0.2 # Lower for more precise results +LLM_MAX_TOKENS=500 # Max tokens in the response + +# Only needed if using OpenAI API instead of a local LLM +# OPENAI_API_KEY=your_api_key_here +``` +#### Using with LM Studio -https://user-images.githubusercontent.com/47084109/231006746-ccd82554-0ec7-431e-a612-bbb3f29f0dd4.mp4 +1. Download and install [LM Studio](https://lmstudio.ai/) +2. Load your desired model in LM Studio +3. Start the local inference server (usually on port 1234) +4. Make sure your `.env` file has the correct URL (typically `http://localhost:1234/v1`) +5. Run CommandGPT as shown below -for example, +##### Recommended Models for LM Studio -```bash -python main.py -i nat -o ps -c "Create a file named test.txt" -``` +For best performance with command conversions, we recommend these models: -or use the interactive mode +| Model | Size | Good for | +|-------|------|----------| +| CodeLlama | 7B, 13B, 34B | General code and command translations | +| DeepSeek Coder | 6.7B, 33B | Precise code generation and bash/PowerShell | +| WizardCoder | 7B, 13B, 34B | Command explanations and translations | +| Mixtral | 8x7B | Well-rounded performance for all conversion types | +| Mistral | 7B | Good balance of size and performance | +To use with LM Studio: +1. Download your preferred model in LM Studio's Models tab +2. Select the model and start the local server +3. Configure `.env` with the correct model name if needed -https://user-images.githubusercontent.com/47084109/231006889-ceb8d60c-dea3-4c8f-88d6-a50be29ee615.mp4 +#### Usage + +**Interactive Mode:** +https://user-images.githubusercontent.com/47084109/231006889-ceb8d60c-dea3-4c8f-88d6-a50be29ee615.mp4 ```bash python main.py ``` -#### Some Use Cases +**Command Line Mode:** -- Convert Natural Language to PowerShell command +https://user-images.githubusercontent.com/47084109/231006746-ccd82554-0ec7-431e-a612-bbb3f29f0dd4.mp4 +```bash +python main.py [-h] -i INPUT -o OUTPUT -c COMMAND [-x] [-d] [--url URL] [--model MODEL] [--temp TEMP] [--max-tokens MAX_TOKENS] ``` -Welcome to the CommandGPT! -1. Natural Language to PowerShell -2. Natural Language to Bash -3. PowerShell to Bash -4. Bash to PowerShell -5. PowerShell to Natural Language -6. Bash to Natural Language -7. Exit -Please select an option: 1 -Please enter a natural language command: Create a file named test.txt -PowerShell equivalent: New-Item -ItemType File -Path "test.txt" +### Options: + ``` +-h, --help Show this help message and exit +-i INPUT, --input INPUT + Input language (nat, bash, ps) +-o OUTPUT, --output OUTPUT + Output language (nat, bash, ps) +-c COMMAND, --command COMMAND + Command to be converted +-x, --execute Execute command and exit +-d, --debug Debug mode +--url URL Local LLM server URL +--model MODEL Model name +--temp TEMP Temperature (0.0-1.0) +--max-tokens MAX_TOKENS Maximum tokens in response +``` + +#### Examples -- Convert Natural Language to Bash command +**Command Line Examples:** +Convert a natural language request to a PowerShell command: +```bash +python main.py -i nat -o ps -c "Create a file named test.txt" +``` + +Convert a Bash command to PowerShell: +```bash +python main.py -i bash -o ps -c "ls -la | grep '.txt'" ``` -Welcome to the CommandGPT! -1. Natural Language to PowerShell -2. Natural Language to Bash -3. PowerShell to Bash -4. Bash to PowerShell -5. PowerShell to Natural Language -6. Bash to Natural Language -7. Exit -Please select an option: 2 -Please enter a natural language command: List all files in the current directory except test.txt and files starting with a dot. -Bash equivalent: ls -p | grep -v / | grep -v '^test\.txt$' | grep -v '^\.' +Run with debug info and custom temperature: +```bash +python main.py -i nat -o bash -c "Find all PDF files modified in the last week" -d --temp 0.1 ``` -- Convert Bash to Natural Language +**Interactive Mode Examples:** +- Convert Natural Language to PowerShell command: ``` -1. Natural Language to PowerShell -2. Natural Language to Bash -3. PowerShell to Bash +CommandGPT Options: +1. Natural Language to Bash +2. Natural Language to PowerShell +3. Bash to Natural Language 4. Bash to PowerShell 5. PowerShell to Natural Language -6. Bash to Natural Language +6. PowerShell to Bash 7. Exit -Please select an option: 6 -Please enter a bash command: curl -X "GET" localhost:9200/_cat/indices?v=true&pretty=true -Natural Language equivalent: List all indices in Elasticsearch. +Enter your choice: 2 +Enter the Natural Language to convert: Create a file named test.txt + +Converting... + +PowerShell equivalent: +New-Item -ItemType File -Path "test.txt" ``` + +#### Using with Different Local LLMs + +This version of CommandGPT is compatible with any OpenAI API-compatible inference server, including: + +- **LM Studio**: Great for testing different models locally. Supports a wide range of models. +- **Ollama**: Easy to use for various open-source models like Llama, Mistral, and CodeLlama. +- **LocalAI**: Another good option for running models locally. + +For best results with command conversions, we recommend: +- Code-specialized models like CodeLlama, Deepseek Coder, or WizardCoder +- Using a temperature of 0.1-0.3 +- Models with at least 7B parameters for more accurate conversions + +You can specify your LLM settings via environment variables or command-line parameters. + +#### Tips for Better Results + +1. For natural language to command conversions, be as specific as possible +2. Try different temperature values - lower (0.1) for exact command conversions, slightly higher (0.3-0.5) for descriptive conversions +3. Adjust the max_tokens parameter if you need longer explanations +4. Code-specialized LLMs tend to perform better for this task