commit a5eb09f40fb32df5de1cf447d11732c4b5ec07d3 Author: bake Date: Mon Jun 23 10:50:34 2025 -0500 Initial commit: Python-based local AI assistant - Add main GUI application (local_agent_ui.py) with customtkinter interface - Add Windows right-click context menu installer (install_right_click.py) - Add project documentation (CLAUDE.md) with setup and usage instructions - Add .gitignore for Python project files 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7f10c43 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Virtual environments +venv/ +env/ +ENV/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Model cache +.halp/ \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..046520c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,77 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a Python-based local AI assistant called "halp" that provides a GUI interface for running shell commands via an AI agent. The application uses a local language model (Yi-Coder-1.5B-Chat) to interpret user requests and execute shell commands accordingly. + +## Key Components + +- **local_agent_ui.py**: Main application with GUI interface using customtkinter, AI model integration, and shell command execution capabilities +- **install_right_click.py**: Windows registry installer that adds a right-click context menu option to launch the assistant from any folder + +## Dependencies + +The project requires these Python packages: +- customtkinter (GUI framework) +- torch (PyTorch for model inference) +- transformers (Hugging Face transformers library) +- pynput (keyboard hotkey detection) +- protobuf (protocol buffers for tokenizer) +- sentencepiece (tokenizer backend) + +### Installation +```bash +pip install customtkinter torch transformers pynput protobuf sentencepiece +``` + +## Development Commands + +### Running the Application +```bash +python3 local_agent_ui.py [optional_working_directory] +``` + +### Installing Right-Click Context Menu (Windows) +```bash +python3 install_right_click.py +``` + +### Uninstalling Right-Click Context Menu (Windows) +```bash +python3 install_right_click.py uninstall +``` + +## Architecture + +The application follows a multi-threaded architecture: +1. **Main UI Thread**: Handles the customtkinter GUI, message display, and user input +2. **AI Processing Thread**: Runs the language model inference and command execution logic +3. **Keyboard Listener**: Global hotkey detection (backtick key by default) for showing/hiding the window + +### Key Features +- **Global Hotkey**: Press backtick (`) to toggle window visibility +- **Context-Aware**: Launches with the current working directory as context +- **Command Execution**: AI agent can execute shell commands and observe results +- **Conversational Loop**: Multi-step reasoning with up to 5 iterations per task + +### AI Agent Workflow +1. User provides a task +2. AI generates reasoning and potential shell commands +3. Commands are executed with stdout/stderr captured +4. Results are fed back to the AI for next steps +5. Process continues until "DONE" or iteration limit reached + +## Configuration + +Key configuration constants in local_agent_ui.py: +- `MODEL_ID`: "01-ai/Yi-Coder-1.5B-Chat" (can be changed to other compatible models) +- `HOTKEY`: Backtick key for window toggle +- Iteration limit: 5 steps per task to prevent loops + +## Platform Notes + +- Designed primarily for Windows (uses Windows registry for context menu) +- Shell commands executed with `shell=True` for Windows compatibility +- Working directory context passed via command line arguments \ No newline at end of file diff --git a/install_right_click.py b/install_right_click.py new file mode 100644 index 0000000..1d326d1 --- /dev/null +++ b/install_right_click.py @@ -0,0 +1,53 @@ +pimport sys +import os +import winreg as reg + +# --- Configuration --- +# The text that will appear in the right-click menu +MENU_ITEM_NAME = "Open AI Assistant Here" + +# --- Main Logic --- +def install(): + try: + # Get the absolute path to the python executable and the main script + python_exe = sys.executable + script_path = os.path.abspath("local_agent_ui.py") + + # The command that will be executed when the menu item is clicked + # %V is a placeholder that Windows replaces with the directory you right-clicked in + command = f'"{python_exe}" "{script_path}" "%V"' + + # Registry path for the context menu on the background of a folder + key_path = r'Directory\\Background\\shell' + + # Create the main key for our menu item + with reg.CreateKey(reg.HKEY_CLASSES_ROOT, f'{key_path}\\{MENU_ITEM_NAME}') as key: + # Create the 'command' subkey and set its value to our command + with reg.CreateKey(key, 'command') as command_key: + reg.SetValue(command_key, None, reg.REG_SZ, command) + + print(f"Successfully installed '{MENU_ITEM_NAME}' context menu item.") + print("You can now right-click inside any folder to launch the assistant with that folder as context.") + + except Exception as e: + print(f"Error: {e}") + print("Please try running this script as an administrator.") + +def uninstall(): + try: + key_path = r'Directory\\Background\\shell' + reg.DeleteKey(reg.HKEY_CLASSES_ROOT, f'{key_path}\\{MENU_ITEM_NAME}\\command') + reg.DeleteKey(reg.HKEY_CLASSES_ROOT, f'{key_path}\\{MENU_ITEM_NAME}') + print(f"Successfully uninstalled '{MENU_ITEM_NAME}'.") + except FileNotFoundError: + print("Menu item not found. Nothing to uninstall.") + except Exception as e: + print(f"Error: {e}") + print("Please try running this script as an administrator.") + + +if __name__ == "__main__": + if len(sys.argv) > 1 and sys.argv[1] == 'uninstall': + uninstall() + else: + install() \ No newline at end of file diff --git a/local_agent_ui.py b/local_agent_ui.py new file mode 100644 index 0000000..ffaa489 --- /dev/null +++ b/local_agent_ui.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python +import customtkinter as ctk +import torch +from transformers import AutoModelForCausalLM, AutoTokenizer +import subprocess +import shlex +import threading +import sys +import os +from pynput import keyboard + +# --- Configuration --- +MODEL_ID = "01-ai/Yi-Coder-1.5B-Chat" +HOTKEY = keyboard.KeyCode.from_char('`') + +# --- 1. AI Model Loading (in a separate thread to not freeze UI) --- +print("Loading model... This may take a moment.") +tokenizer = AutoTokenizer.from_pretrained(MODEL_ID) +model = AutoModelForCausalLM.from_pretrained( + MODEL_ID, + torch_dtype=torch.bfloat16, + device_map="auto", +) +print("Model loaded successfully.") + +# --- 2. Tool: Shell Command Executor --- +def execute_shell_command(command: str, working_dir: str): + try: + # Use shell=True for better compatibility with Windows built-in commands + # and to handle complex commands without shlex. + result = subprocess.run( + command, + capture_output=True, + text=True, + shell=True, + cwd=working_dir + ) + return { + "stdout": result.stdout, + "stderr": result.stderr, + "returncode": result.returncode, + } + except Exception as e: + return {"stdout": "", "stderr": str(e), "returncode": 1} + +# --- 3. The Main Application Window --- +class ChatWindow(ctk.CTk): + def __init__(self, working_dir): + super().__init__() + + self.working_dir = working_dir + self.title(f"Local AI Assistant - CWD: {self.working_dir}") + self.geometry("700x500") + + self.grid_columnconfigure(0, weight=1) + self.grid_rowconfigure(0, weight=1) + + # Output Textbox + self.output_textbox = ctk.CTkTextbox(self, state="disabled", wrap="word") + self.output_textbox.grid(row=0, column=0, padx=10, pady=10, sticky="nsew") + + # Input Entry + self.input_entry = ctk.CTkEntry(self, placeholder_text="Type your task here and press Enter...") + self.input_entry.grid(row=1, column=0, padx=10, pady=10, sticky="ew") + self.input_entry.bind("", self.start_agent_task) + + self.is_minimized = True # Start hidden + self.withdraw() + + def add_message(self, message_type: str, content: str): + # This method ensures UI updates happen on the main thread + self.output_textbox.configure(state="normal") + self.output_textbox.insert("end", f"[{message_type}]\n{content}\n\n") + self.output_textbox.configure(state="disabled") + self.output_textbox.see("end") + + def toggle_visibility(self): + if self.is_minimized: + self.deiconify() # Show the window + self.attributes('-topmost', 1) # Bring to front + self.focus() + self.attributes('-topmost', 0) + else: + self.withdraw() # Hide the window + self.is_minimized = not self.is_minimized + + def start_agent_task(self, event=None): + task = self.input_entry.get() + if not task: + return + self.input_entry.delete(0, "end") + self.after(0, self.add_message, "User", task) + + # Run the agent in a separate thread to avoid freezing the UI + agent_thread = threading.Thread(target=self.run_agent, args=(task,)) + agent_thread.start() + + def run_agent(self, task: str): + system_prompt = f"""You are a helpful AI assistant that executes shell commands on Windows in the directory '{self.working_dir}'. +You can use the `execute_shell_command(command)` function. +Based on the output, decide the next step. +When finished, respond with "DONE" and a summary. + +Example: +User: List all files in the current folder. +Assistant: I need to list files. I will use the `dir` command. +dir + +{{"stdout": " Volume in drive C is OS...", "stderr": "", "returncode": 0}} + +I have listed the files. +DONE: I have listed the files and folders in the current directory.""" + + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": task}, + ] + + for _ in range(5): # Limit steps to prevent loops + self.after(0, self.add_message, "Agent", "Thinking...") + input_ids = tokenizer.apply_chat_template( + conversation=messages, tokenize=True, add_generation_prompt=True, return_tensors='pt' + ) + output_ids = model.generate(input_ids.to(model.device), max_new_tokens=200, pad_token_id=tokenizer.eos_token_id) + response = tokenizer.decode(output_ids[0][input_ids.shape[1]:], skip_special_tokens=True) + + self.after(0, self.add_message, "Agent Thought", response) + + if "DONE" in response: + break + + if "" in response and "" in response: + command = response.split("")[1].split("")[0].strip() + self.after(0, self.add_message, "Executing", command) + + result = execute_shell_command(command, self.working_dir) + observation_text = f'STDOUT:\n{result["stdout"]}\nSTDERR:\n{result["stderr"]}\nRETURN CODE: {result["returncode"]}' + self.after(0, self.add_message, "Observation", observation_text) + + messages.append({"role": "assistant", "content": response}) + messages.append({"role": "user", "content": f"\n{str(result)}\n"}) + else: + self.after(0, self.add_message, "Agent", "Could not determine a command. Stopping.") + break + +# --- 4. Main Execution Logic --- +def main(): + # Set the current working directory + # If launched from the context menu, sys.argv[1] will be the folder path + if len(sys.argv) > 1: + current_dir = sys.argv[1] + try: + os.chdir(current_dir) + except Exception as e: + print(f"Failed to change directory to {current_dir}: {e}") + current_dir = os.getcwd() # Fallback to script's dir + else: + current_dir = os.getcwd() + + print(f"Application starting in directory: {current_dir}") + + app = ChatWindow(working_dir=current_dir) + + def on_press(key): + if key == HOTKEY: + app.toggle_visibility() + + listener = keyboard.Listener(on_press=on_press) + listener.start() + + app.mainloop() + listener.stop() + +if __name__ == "__main__": + main() \ No newline at end of file