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
This commit is contained in:
opencode committed 2026-03-24 10:15:51 +01:00
1 parent 32ed76e5be
commit 4a0342dcb7
2 files changed
+397 -18

No files matched your search

+381
View File
@@ -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)
+16 -18
View File
@@ -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)}")