From 4a0342dcb73e0052a8bf00d1bacbf682b4aa2a3f Mon Sep 17 00:00:00 2001 From: opencode Date: Tue, 24 Mar 2026 10:15:51 +0100 Subject: [PATCH] feat: aggiungi EnhancedCLI con prompt_toolkit - Nuovo modulo enhanced_cli.py con UI moderna - Colors: user (cyan), agent (green), system (yellow), error (red) - History persistente in .langcode/.cli_history - Autocomplete comandi (/help, /status, /clear, /theme, /exit) - Supporto temi: default, dark, light - Multiline input - Integrato in langcode.py chat mode --- enhanced_cli.py | 381 ++++++++++++++++++++++++++++++++++++++++++++++++ langcode.py | 34 ++--- 2 files changed, 397 insertions(+), 18 deletions(-) create mode 100644 enhanced_cli.py diff --git a/enhanced_cli.py b/enhanced_cli.py new file mode 100644 index 0000000..2764e71 --- /dev/null +++ b/enhanced_cli.py @@ -0,0 +1,381 @@ +""" +Enhanced CLI for LangCode Agent +Provides modern terminal UI with colors, history, and autocomplete +""" + +import sys +import os +from typing import Optional, List, Dict, Any +from datetime import datetime + +# prompt_toolkit imports +from prompt_toolkit import PromptSession, HTML +from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.styles import Style +from prompt_toolkit.completion import WordCompleter, NestedCompleter +from prompt_toolkit.history import FileHistory +from prompt_toolkit.auto_suggest import AutoSuggestFromHistory +from prompt_toolkit.formatted_text import FormattedText + +# Color palette +class Colors: + """ANSI color codes for terminal output""" + # Styles + BOLD = "\033[1m" + DIM = "\033[2m" + ITALIC = "\033[3m" + RESET = "\033[0m" + + # Colors + BLACK = "\033[30m" + RED = "\033[31m" + GREEN = "\033[32m" + YELLOW = "\033[33m" + BLUE = "\033[34m" + MAGENTA = "\033[35m" + CYAN = "\033[36m" + WHITE = "\033[37m" + + # Background + BG_BLACK = "\033[40m" + BG_BLUE = "\033[44m" + BG_CYAN = "\033[46m" + + # Bright colors + BRIGHT_BLACK = "\033[90m" + BRIGHT_RED = "\033[91m" + BRIGHT_GREEN = "\033[92m" + BRIGHT_YELLOW = "\033[93m" + BRIGHT_BLUE = "\033[94m" + BRIGHT_MAGENTA = "\033[95m" + BRIGHT_CYAN = "\033[96m" + BRIGHT_WHITE = "\033[97m" + + +class Theme: + """Color themes for the CLI""" + + DEFAULT = { + "user": "ansicyan", # Cyan for user input + "agent": "ansigreen", # Green for agent response + "system": "ansiyellow", # Yellow for system messages + "error": "ansired", # Red for errors + "info": "ansiblue", # Blue for info + "muted": "ansiblack", # Black for muted text + "prompt": "ansimagenta", # Magenta for prompt + } + + DARK = { + "user": "cyan", + "agent": "lightgreen", + "system": "yellow", + "error": "lightred", + "info": "lightblue", + "muted": "gray", + "prompt": "magenta", + } + + LIGHT = { + "user": "blue", + "agent": "darkgreen", + "system": "darkyellow", + "error": "darkred", + "info": "darkblue", + "muted": "gray", + "prompt": "darkmagenta", + } + + +def create_style(theme_name: str = "default") -> Style: + """Create prompt_toolkit style from theme""" + theme = getattr(Theme, theme_name.upper(), Theme.DEFAULT) + + style_dict = { + "user": theme["user"], + "agent": theme["agent"], + "system": theme["system"], + "error": theme["error"], + "info": theme["info"], + "muted": theme["muted"], + "prompt": theme["prompt"], + } + + return Style.from_dict(style_dict) + + +class EnhancedCLI: + """ + Enhanced Command Line Interface for LangCode Agent. + + Features: + - Colored input/output + - Command history (persistent) + - Autocomplete for commands + - Multi-line input support + - Themes (light/dark) + """ + + def __init__( + self, + history_file: str = "./.langcode/.cli_history", + theme: str = "default", + welcome_message: Optional[str] = None + ): + """ + Initialize Enhanced CLI. + + Args: + history_file: Path to persist command history + theme: Color theme (default, dark, light) + welcome_message: Initial message to show + """ + self.history_file = history_file + self.theme = theme + self.welcome_message = welcome_message or self._default_welcome() + + # Ensure history directory exists + os.makedirs(os.path.dirname(history_file) or ".", exist_ok=True) + + # Command definitions + self.commands = { + "/help": "Mostra aiuto", + "/status": "Stato sistema", + "/clear": "Pulisci schermo", + "/theme": "Cambia tema (default/dark/light)", + "/history": "Mostra cronologia", + "/exit": "Esci", + "/quit": "Esci", + } + + # Create completer for autocomplete + self.completer = NestedCompleter.from_nested_dict({ + cmd: None for cmd in self.commands.keys() + }) + + # Create key bindings + self.key_bindings = self._create_key_bindings() + + # Create prompt session + self.session = self._create_session() + + # Current theme name + self.current_theme = theme + + # Output welcome message + msg = welcome_message if welcome_message else self._default_welcome() + self._print_system(msg) + + def _default_welcome(self) -> str: + """Generate default welcome message""" + return f""" +{Colors.CYAN}{Colors.BOLD}╔═══════════════════════════════════════════════════════════╗{Colors.RESET} +║ {Colors.BRIGHT_GREEN}{Colors.BOLD}LangCode Agent - Enhanced CLI{Colors.RESET} ║ +║ ║ +║ {Colors.YELLOW}Comandi:{Colors.RESET} ║ +║ {Colors.CYAN}/help{Colors.RESET} - Mostra aiuto {Colors.CYAN}/clear{Colors.RESET} - Pulisci schermo ║ +║ {Colors.CYAN}/status{Colors.RESET} - Stato sistema {Colors.CYAN}/theme{Colors.RESET} - Cambia tema ║ +║ {Colors.CYAN}/exit{Colors.RESET} - Esci ║ +║ ║ +╚═══════════════════════════════════════════════════════════════════╝{Colors.RESET} +""" + + def _create_key_bindings(self) -> KeyBindings: + """Create custom key bindings""" + kb = KeyBindings() + + @kb.add("c-c") + def _(event): + """Handle Ctrl+C - cancel input""" + event.app.exit(exception=KeyboardInterrupt) + + return kb + + def _create_session(self) -> PromptSession: + """Create prompt session with history and autocomplete""" + return PromptSession( + history=FileHistory(self.history_file), + completer=self.completer, + key_bindings=self.key_bindings, + auto_suggest=AutoSuggestFromHistory(), + multiline=True, + ) + + def _get_bottom_bar(self, cli) -> List: + """Bottom toolbar showing theme and shortcuts""" + return [ + ("class:muted", f" Theme: {self.current_theme} "), + ("class:muted", " | "), + ("class:muted", "Ctrl+C: exit | Tab: autocomplete | ↑↓: history "), + ] + + def _create_prompt_tokens(self) -> FormattedText: + """Create prompt tokens with colors""" + timestamp = datetime.now().strftime("%H:%M") + return FormattedText([ + ("class:prompt", f"❯ "), + ("class:user", f"TUO "), + ("class:muted", f"[{timestamp}] "), + ]) + + def _print_system(self, message: str) -> None: + """Print system message (in yellow)""" + print(f"{Colors.YELLOW}{message}{Colors.RESET}") + + def _print_user(self, message: str) -> None: + """Print user input (in cyan)""" + print(f"{Colors.CYAN}👤 TUO:{Colors.RESET} {message}") + + def _print_agent(self, message: str) -> None: + """Print agent response (in green)""" + # Handle multi-line messages + lines = message.split("\n") + for i, line in enumerate(lines): + if i == 0: + print(f"{Colors.GREEN}🤖 AGENTE:{Colors.RESET} {line}") + else: + print(f"{Colors.GREEN} {Colors.RESET} {line}") + + def _print_error(self, message: str) -> None: + """Print error message (in red)""" + print(f"{Colors.RED}❌ ERRORE:{Colors.RESET} {message}") + + def _print_info(self, message: str) -> None: + """Print info message (in blue)""" + print(f"{Colors.BLUE}ℹ️ INFO:{Colors.RESET} {message}") + + def _handle_command(self, command: str) -> Optional[bool]: + """ + Handle special commands (/command). + + Returns: + True if command was handled and we should exit + False if command was handled but we should continue + None if command was not recognized + """ + cmd_lower = command.lower().strip() + + if cmd_lower == "/help": + self._print_system(""" +╔═══════════════════════════════════════════════════════════════╗ +║ COMANDI DISPONIBILI ║ +╠═══════════════════════════════════════════════════════════════╣ +║ /help - Mostra questo messaggio ║ +║ /status - Mostra stato del sistema ║ +║ /clear - Pulisci lo schermo ║ +║ /theme - Cicla tra i temi (default/dark/light) ║ +║ /history - Mostra cronologia comandi ║ +║ /exit - Esci dall'agente ║ +║ ║ +║ Per messaggi normali, scrivi direttamente la tua domanda. ║ +╚═══════════════════════════════════════════════════════════════╝ +""") + return None + + elif cmd_lower == "/status": + self._print_info(f"Theme: {self.current_theme}") + self._print_info(f"History file: {self.history_file}") + return None + + elif cmd_lower == "/clear": + os.system("clear" if os.name != "nt" else "cls") + return None + + elif cmd_lower == "/theme": + themes = ["default", "dark", "light"] + current_idx = themes.index(self.current_theme) if self.current_theme in themes else 0 + next_theme = themes[(current_idx + 1) % len(themes)] + self.current_theme = next_theme + self._print_info(f"Tema cambiato a: {next_theme}") + return None + + elif cmd_lower in ["/exit", "/quit"]: + return True + + elif cmd_lower == "/history": + try: + with open(self.history_file, "r") as f: + lines = f.readlines() + if lines: + self._print_system(f"Ultimi {min(10, len(lines))} comandi:") + for i, line in enumerate(lines[-10:], 1): + print(f" {i}. {line.strip()}") + else: + self._print_info("Nessuna cronologia") + except FileNotFoundError: + self._print_info("Nessuna cronologia") + return None + + return None # Command not recognized + + def prompt(self) -> str: + """Get input from user with enhanced UI""" + try: + text = self.session.prompt( + self._create_prompt_tokens(), + style=create_style(self.current_theme) + ) + return text + except KeyboardInterrupt: + return "/exit" + except EOFError: + return "/exit" + + def run(self, agent_callback): + """ + Run the interactive CLI loop. + + Args: + agent_callback: Function that takes user input and returns agent response + """ + self._print_system(self.welcome_message) + + while True: + try: + # Get user input + user_input = self.prompt().strip() + + if not user_input: + continue + + # Show what user typed + self._print_user(user_input) + + # Check for commands + if user_input.startswith("/"): + should_exit = self._handle_command(user_input) + if should_exit: + break + continue + + # Call agent and show response + try: + response = agent_callback(user_input) + self._print_agent(response) + except Exception as e: + self._print_error(f"Errore: {str(e)}") + + except KeyboardInterrupt: + self._print_system("\n👋 Arrivederci!") + break + except EOFError: + break + + self._print_system("Sessione terminata.") + + +# Factory function for easy use +def create_enhanced_cli( + history_file: str = "./.langcode/.cli_history", + theme: str = "default" +) -> EnhancedCLI: + """Create an EnhancedCLI instance""" + return EnhancedCLI(history_file=history_file, theme=theme) + + +if __name__ == "__main__": + # Test the CLI + def test_callback(text): + return f"Hai detto: {text}" + + cli = create_enhanced_cli() + cli.run(test_callback) diff --git a/langcode.py b/langcode.py index 3e02be7..2ff1f0c 100755 --- a/langcode.py +++ b/langcode.py @@ -30,9 +30,10 @@ VERSION = "1.0.0" def cmd_chat(args): - """Avvia l'agent in modalità chat interattiva""" + """Avvia l'agent in modalità chat interattiva con EnhancedCLI""" from main import create_langcode_agent, process_command from langchain_core.messages import HumanMessage + from enhanced_cli import create_enhanced_cli print("🚀 Inizializzo LangCode Agent...") @@ -53,34 +54,31 @@ def cmd_chat(args): print(" /exit - Esci dall'agente") print("\n💡 Per chattare, scrivi semplicemente il tuo messaggio.") - while True: - try: - user_input = input("\n👤 You: ").strip() - except (EOFError, KeyboardInterrupt): - print("\n👋 Arrivederci!") - break - - if not user_input: - continue - + # Callback per processare i messaggi con l'agente + def agent_callback(user_input: str) -> str: # Check if it's a command (starts with /) if process_command(user_input): - continue - + return "Comando eseguito." + # Otherwise, treat it as a chat message try: result = agent.invoke({"messages": [HumanMessage(content=user_input)]}) # Extract response - support both simple agent (messages[-1]) and tool agent (response) if "response" in result: - response_text = result["response"] + return result["response"] else: ai_message = result["messages"][-1] - response_text = ai_message.content if hasattr(ai_message, 'content') else str(ai_message) - - print(f"\n🤖 LangCode: {response_text}") + return ai_message.content if hasattr(ai_message, 'content') else str(ai_message) except Exception as e: - print(f"\n❌ Errore: {str(e)}") + return f"Errore: {str(e)}" + + # Usa EnhancedCLI con history e autocomplete + cli = create_enhanced_cli( + history_file="./.langcode/.cli_history", + theme="default" + ) + cli.run(agent_callback) except Exception as e: print(f"❌ Inizializzazione fallita: {str(e)}")