* Add readline support to interactive command line interfaces - Implement readline history, navigation, and editing for CLI, API, and RAG chat modes - Create shared InteractiveSession class to consolidate readline functionality - Add command history persistence across sessions with separate files per context - Support built-in commands: help, clear, history, quit/exit - Enable arrow key navigation and command editing in all interactive modes * Improvements based on feedback
190 lines
5.9 KiB
Python
190 lines
5.9 KiB
Python
"""
|
|
Interactive session utilities for LEANN applications.
|
|
|
|
Provides shared readline functionality and command handling across
|
|
CLI, API, and RAG example interactive modes.
|
|
"""
|
|
|
|
import atexit
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Callable, Optional
|
|
|
|
# Try to import readline with fallback for Windows
|
|
try:
|
|
import readline
|
|
|
|
HAS_READLINE = True
|
|
except ImportError:
|
|
# Windows doesn't have readline by default
|
|
HAS_READLINE = False
|
|
readline = None
|
|
|
|
|
|
class InteractiveSession:
|
|
"""Manages interactive session with optional readline support and common commands."""
|
|
|
|
def __init__(
|
|
self,
|
|
history_name: str,
|
|
prompt: str = "You: ",
|
|
welcome_message: str = "",
|
|
):
|
|
"""
|
|
Initialize interactive session with optional readline support.
|
|
|
|
Args:
|
|
history_name: Name for history file (e.g., "cli", "api_chat")
|
|
(ignored if readline not available)
|
|
prompt: Input prompt to display
|
|
welcome_message: Message to show when starting session
|
|
|
|
Note:
|
|
On systems without readline (e.g., Windows), falls back to basic input()
|
|
with limited functionality (no history, no line editing).
|
|
"""
|
|
self.history_name = history_name
|
|
self.prompt = prompt
|
|
self.welcome_message = welcome_message
|
|
self._setup_complete = False
|
|
|
|
def setup_readline(self):
|
|
"""Setup readline with history support (if available)."""
|
|
if self._setup_complete:
|
|
return
|
|
|
|
if not HAS_READLINE:
|
|
# Readline not available (likely Windows), skip setup
|
|
self._setup_complete = True
|
|
return
|
|
|
|
# History file setup
|
|
history_dir = Path.home() / ".leann" / "history"
|
|
history_dir.mkdir(parents=True, exist_ok=True)
|
|
history_file = history_dir / f"{self.history_name}.history"
|
|
|
|
# Load history if exists
|
|
try:
|
|
readline.read_history_file(str(history_file))
|
|
readline.set_history_length(1000)
|
|
except FileNotFoundError:
|
|
pass
|
|
|
|
# Save history on exit
|
|
atexit.register(readline.write_history_file, str(history_file))
|
|
|
|
# Optional: Enable vi editing mode (commented out by default)
|
|
# readline.parse_and_bind("set editing-mode vi")
|
|
|
|
self._setup_complete = True
|
|
|
|
def _show_help(self):
|
|
"""Show available commands."""
|
|
print("Commands:")
|
|
print(" quit/exit/q - Exit the chat")
|
|
print(" help - Show this help message")
|
|
print(" clear - Clear screen")
|
|
print(" history - Show command history")
|
|
|
|
def _show_history(self):
|
|
"""Show command history."""
|
|
if not HAS_READLINE:
|
|
print(" History not available (readline not supported on this system)")
|
|
return
|
|
|
|
history_length = readline.get_current_history_length()
|
|
if history_length == 0:
|
|
print(" No history available")
|
|
return
|
|
|
|
for i in range(history_length):
|
|
item = readline.get_history_item(i + 1)
|
|
if item:
|
|
print(f" {i + 1}: {item}")
|
|
|
|
def get_user_input(self) -> Optional[str]:
|
|
"""
|
|
Get user input with readline support.
|
|
|
|
Returns:
|
|
User input string, or None if EOF (Ctrl+D)
|
|
"""
|
|
try:
|
|
return input(self.prompt).strip()
|
|
except KeyboardInterrupt:
|
|
print("\n(Use 'quit' to exit)")
|
|
return "" # Return empty string to continue
|
|
except EOFError:
|
|
print("\nGoodbye!")
|
|
return None
|
|
|
|
def run_interactive_loop(self, handler_func: Callable[[str], None]):
|
|
"""
|
|
Run the interactive loop with a custom handler function.
|
|
|
|
Args:
|
|
handler_func: Function to handle user input that's not a built-in command
|
|
Should accept a string and handle the user's query
|
|
"""
|
|
self.setup_readline()
|
|
|
|
if self.welcome_message:
|
|
print(self.welcome_message)
|
|
|
|
while True:
|
|
user_input = self.get_user_input()
|
|
|
|
if user_input is None: # EOF (Ctrl+D)
|
|
break
|
|
|
|
if not user_input: # Empty input or KeyboardInterrupt
|
|
continue
|
|
|
|
# Handle built-in commands
|
|
command = user_input.lower()
|
|
if command in ["quit", "exit", "q"]:
|
|
print("Goodbye!")
|
|
break
|
|
elif command == "help":
|
|
self._show_help()
|
|
elif command == "clear":
|
|
os.system("clear" if os.name != "nt" else "cls")
|
|
elif command == "history":
|
|
self._show_history()
|
|
else:
|
|
# Regular user input - pass to handler
|
|
try:
|
|
handler_func(user_input)
|
|
except Exception as e:
|
|
print(f"Error: {e}")
|
|
|
|
|
|
def create_cli_session(index_name: str) -> InteractiveSession:
|
|
"""Create an interactive session for CLI usage."""
|
|
return InteractiveSession(
|
|
history_name=index_name,
|
|
prompt="\nYou: ",
|
|
welcome_message="LEANN Assistant ready! Type 'quit' to exit, 'help' for commands\n"
|
|
+ "=" * 40,
|
|
)
|
|
|
|
|
|
def create_api_session() -> InteractiveSession:
|
|
"""Create an interactive session for API chat."""
|
|
return InteractiveSession(
|
|
history_name="api_chat",
|
|
prompt="You: ",
|
|
welcome_message="Leann Chat started (type 'quit' to exit, 'help' for commands)\n"
|
|
+ "=" * 40,
|
|
)
|
|
|
|
|
|
def create_rag_session(app_name: str, data_description: str) -> InteractiveSession:
|
|
"""Create an interactive session for RAG examples."""
|
|
return InteractiveSession(
|
|
history_name=f"{app_name}_rag",
|
|
prompt="You: ",
|
|
welcome_message=f"[Interactive Mode] Chat with your {data_description} data!\nType 'quit' or 'exit' to stop, 'help' for commands.\n"
|
|
+ "=" * 40,
|
|
)
|