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:
1 parent
32ed76e5be
commit
4a0342dcb7
2 files changed
+397
-18
No files matched your search
+381
@@ -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
@@ -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)}")
|
||||
|
||||
Reference in new issue
Block a user