- 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
382 lines
13 KiB
Python
382 lines
13 KiB
Python
"""
|
||
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)
|