Implement interrupt handling for agent and CLI input and persistent prompt line at bottom of CLI :)
- Enhanced the AIAgent class to support interrupt requests, allowing for graceful interruption of ongoing tasks and processing of new messages. - Updated the HermesCLI to manage user input in a persistent manner, enabling real-time interruption of the agent's conversation. - Introduced a mechanism in the GatewayRunner to handle incoming messages while an agent is running, allowing for immediate response to user commands. - Improved overall user experience by providing feedback during interruptions and ensuring that pending messages are processed correctly.
This commit is contained in:
200
cli.py
200
cli.py
@@ -33,6 +33,15 @@ from prompt_toolkit.history import FileHistory
|
||||
from prompt_toolkit.styles import Style as PTStyle
|
||||
from prompt_toolkit.formatted_text import HTML
|
||||
from prompt_toolkit.patch_stdout import patch_stdout
|
||||
from prompt_toolkit.application import Application, get_app
|
||||
from prompt_toolkit.buffer import Buffer
|
||||
from prompt_toolkit.layout import Layout, HSplit, Window, FormattedTextControl
|
||||
from prompt_toolkit.layout.processors import BeforeInput
|
||||
from prompt_toolkit.widgets import TextArea
|
||||
from prompt_toolkit.key_binding import KeyBindings
|
||||
import asyncio
|
||||
import threading
|
||||
import queue
|
||||
|
||||
# Load environment variables first
|
||||
from dotenv import load_dotenv
|
||||
@@ -1284,17 +1293,52 @@ class HermesCLI:
|
||||
print("─" * 60, flush=True)
|
||||
|
||||
try:
|
||||
# Run the conversation
|
||||
result = self.agent.run_conversation(
|
||||
user_message=message,
|
||||
conversation_history=self.conversation_history[:-1], # Exclude the message we just added
|
||||
)
|
||||
# Run the conversation with interrupt monitoring
|
||||
result = None
|
||||
|
||||
def run_agent():
|
||||
nonlocal result
|
||||
result = self.agent.run_conversation(
|
||||
user_message=message,
|
||||
conversation_history=self.conversation_history[:-1], # Exclude the message we just added
|
||||
)
|
||||
|
||||
# Start agent in background thread
|
||||
agent_thread = threading.Thread(target=run_agent)
|
||||
agent_thread.start()
|
||||
|
||||
# Monitor for new input in the pending queue while agent runs
|
||||
interrupt_msg = None
|
||||
while agent_thread.is_alive():
|
||||
# Check if there's new input in the queue (from the persistent input area)
|
||||
if hasattr(self, '_pending_input'):
|
||||
try:
|
||||
interrupt_msg = self._pending_input.get(timeout=0.1)
|
||||
if interrupt_msg:
|
||||
print(f"\n⚡ New message detected, interrupting...")
|
||||
self.agent.interrupt(interrupt_msg)
|
||||
break
|
||||
except:
|
||||
pass # Queue empty or timeout, continue waiting
|
||||
else:
|
||||
# Fallback if no queue (shouldn't happen)
|
||||
agent_thread.join(0.1)
|
||||
|
||||
agent_thread.join() # Ensure agent thread completes
|
||||
|
||||
# Update history with full conversation
|
||||
self.conversation_history = result.get("messages", self.conversation_history)
|
||||
self.conversation_history = result.get("messages", self.conversation_history) if result else self.conversation_history
|
||||
|
||||
# Get the final response
|
||||
response = result.get("final_response", "")
|
||||
response = result.get("final_response", "") if result else ""
|
||||
|
||||
# Handle interrupt - check if we were interrupted
|
||||
pending_message = None
|
||||
if result and result.get("interrupted"):
|
||||
pending_message = result.get("interrupt_message") or interrupt_msg
|
||||
# Add indicator that we were interrupted
|
||||
if response and pending_message:
|
||||
response = response + "\n\n---\n_[Interrupted - processing new message]_"
|
||||
|
||||
if response:
|
||||
# Use simple print for compatibility with prompt_toolkit's patch_stdout
|
||||
@@ -1307,6 +1351,11 @@ class HermesCLI:
|
||||
print()
|
||||
print("─" * 60)
|
||||
|
||||
# If we have a pending message from interrupt, process it immediately
|
||||
if pending_message:
|
||||
print(f"\n📨 Processing: '{pending_message[:50]}{'...' if len(pending_message) > 50 else ''}'")
|
||||
return self.chat(pending_message) # Recursive call to handle the new message
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
@@ -1345,22 +1394,101 @@ class HermesCLI:
|
||||
return None
|
||||
|
||||
def run(self):
|
||||
"""Run the interactive CLI loop with fixed input at bottom."""
|
||||
"""Run the interactive CLI loop with persistent input at bottom."""
|
||||
self.show_banner()
|
||||
|
||||
# These Rich prints work fine BEFORE patch_stdout
|
||||
self.console.print("[#FFF8DC]Welcome to Hermes Agent! Type your message or /help for commands.[/]")
|
||||
self.console.print()
|
||||
|
||||
# Use patch_stdout to ensure all output appears above the input prompt
|
||||
with patch_stdout():
|
||||
while True:
|
||||
# State for async operation
|
||||
self._agent_running = False
|
||||
self._pending_input = queue.Queue()
|
||||
self._should_exit = False
|
||||
|
||||
# Create a persistent input area using prompt_toolkit Application
|
||||
input_buffer = Buffer()
|
||||
|
||||
# Key bindings for the input area
|
||||
kb = KeyBindings()
|
||||
|
||||
@kb.add('enter')
|
||||
def handle_enter(event):
|
||||
"""Handle Enter key - submit input."""
|
||||
text = event.app.current_buffer.text.strip()
|
||||
if text:
|
||||
# Store the input
|
||||
self._pending_input.put(text)
|
||||
# Clear the buffer
|
||||
event.app.current_buffer.reset()
|
||||
|
||||
@kb.add('c-c')
|
||||
def handle_ctrl_c(event):
|
||||
"""Handle Ctrl+C - interrupt or exit."""
|
||||
if self._agent_running and self.agent:
|
||||
print("\n⚡ Interrupting agent...")
|
||||
self.agent.interrupt()
|
||||
else:
|
||||
self._should_exit = True
|
||||
event.app.exit()
|
||||
|
||||
@kb.add('c-d')
|
||||
def handle_ctrl_d(event):
|
||||
"""Handle Ctrl+D - exit."""
|
||||
self._should_exit = True
|
||||
event.app.exit()
|
||||
|
||||
# Create the input area widget
|
||||
input_area = TextArea(
|
||||
height=1,
|
||||
prompt='❯ ',
|
||||
style='class:input-area',
|
||||
multiline=False,
|
||||
wrap_lines=False,
|
||||
)
|
||||
|
||||
# Create a status line that shows when agent is working
|
||||
def get_status_text():
|
||||
if self._agent_running:
|
||||
return [('class:status', ' 🔄 Agent working... (type to interrupt) ')]
|
||||
return [('class:status', '')]
|
||||
|
||||
status_window = Window(
|
||||
content=FormattedTextControl(get_status_text),
|
||||
height=1,
|
||||
)
|
||||
|
||||
# Layout with status and input at bottom
|
||||
layout = Layout(
|
||||
HSplit([
|
||||
Window(height=0), # Spacer that expands
|
||||
status_window,
|
||||
input_area,
|
||||
])
|
||||
)
|
||||
|
||||
# Style for the application
|
||||
style = PTStyle.from_dict({
|
||||
'input-area': '#FFF8DC',
|
||||
'status': 'bg:#333333 #FFD700',
|
||||
})
|
||||
|
||||
# Create the application
|
||||
app = Application(
|
||||
layout=layout,
|
||||
key_bindings=kb,
|
||||
style=style,
|
||||
full_screen=False,
|
||||
mouse_support=False,
|
||||
)
|
||||
|
||||
# Background thread to process inputs and run agent
|
||||
def process_loop():
|
||||
while not self._should_exit:
|
||||
try:
|
||||
user_input = self.get_input()
|
||||
|
||||
if user_input is None:
|
||||
print("\nGoodbye! ⚕")
|
||||
break
|
||||
# Check for pending input with timeout
|
||||
try:
|
||||
user_input = self._pending_input.get(timeout=0.1)
|
||||
except queue.Empty:
|
||||
continue
|
||||
|
||||
if not user_input:
|
||||
continue
|
||||
@@ -1368,16 +1496,38 @@ class HermesCLI:
|
||||
# Check for commands
|
||||
if user_input.startswith("/"):
|
||||
if not self.process_command(user_input):
|
||||
print("\nGoodbye! ⚕")
|
||||
break
|
||||
self._should_exit = True
|
||||
# Schedule app exit
|
||||
if app.is_running:
|
||||
app.exit()
|
||||
continue
|
||||
|
||||
# Regular chat message
|
||||
self.chat(user_input)
|
||||
# Regular chat - run agent
|
||||
self._agent_running = True
|
||||
app.invalidate() # Refresh status line
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\nInterrupted. Type /quit to exit.")
|
||||
continue
|
||||
try:
|
||||
self.chat(user_input)
|
||||
finally:
|
||||
self._agent_running = False
|
||||
app.invalidate() # Refresh status line
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
# Start processing thread
|
||||
process_thread = threading.Thread(target=process_loop, daemon=True)
|
||||
process_thread.start()
|
||||
|
||||
# Run the application with patch_stdout for proper output handling
|
||||
try:
|
||||
with patch_stdout():
|
||||
app.run()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
pass
|
||||
finally:
|
||||
self._should_exit = True
|
||||
print("\nGoodbye! ⚕")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user