Merge PR #360: improve agent CLI chat rendering and input experience
This commit is contained in:
commit
8fa52120b1
@ -459,11 +459,15 @@ That's it! Environment variables, model prefixing, config matching, and `nanobot
|
|||||||
| `nanobot onboard` | Initialize config & workspace |
|
| `nanobot onboard` | Initialize config & workspace |
|
||||||
| `nanobot agent -m "..."` | Chat with the agent |
|
| `nanobot agent -m "..."` | Chat with the agent |
|
||||||
| `nanobot agent` | Interactive chat mode |
|
| `nanobot agent` | Interactive chat mode |
|
||||||
|
| `nanobot agent --no-markdown` | Show plain-text replies |
|
||||||
|
| `nanobot agent --logs` | Show runtime logs during chat |
|
||||||
| `nanobot gateway` | Start the gateway |
|
| `nanobot gateway` | Start the gateway |
|
||||||
| `nanobot status` | Show status |
|
| `nanobot status` | Show status |
|
||||||
| `nanobot channels login` | Link WhatsApp (scan QR) |
|
| `nanobot channels login` | Link WhatsApp (scan QR) |
|
||||||
| `nanobot channels status` | Show channel status |
|
| `nanobot channels status` | Show channel status |
|
||||||
|
|
||||||
|
Interactive mode exits: `exit`, `quit`, `/exit`, `/quit`, `:q`, or `Ctrl+D`.
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><b>Scheduled Tasks (Cron)</b></summary>
|
<summary><b>Scheduled Tasks (Cron)</b></summary>
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,10 @@ import sys
|
|||||||
|
|
||||||
import typer
|
import typer
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
from rich.markdown import Markdown
|
||||||
|
from rich.panel import Panel
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
|
from rich.text import Text
|
||||||
|
|
||||||
from nanobot import __version__, __logo__
|
from nanobot import __version__, __logo__
|
||||||
|
|
||||||
@ -21,6 +24,7 @@ app = typer.Typer(
|
|||||||
)
|
)
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
EXIT_COMMANDS = {"exit", "quit", "/exit", "/quit", ":q"}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Lightweight CLI input: readline for arrow keys / history, termios for flush
|
# Lightweight CLI input: readline for arrow keys / history, termios for flush
|
||||||
@ -132,6 +136,28 @@ def _prompt_text() -> str:
|
|||||||
return "\001\033[1;34m\002You:\001\033[0m\002 "
|
return "\001\033[1;34m\002You:\001\033[0m\002 "
|
||||||
|
|
||||||
|
|
||||||
|
def _print_agent_response(response: str, render_markdown: bool) -> None:
|
||||||
|
"""Render assistant response with consistent terminal styling."""
|
||||||
|
content = response or ""
|
||||||
|
body = Markdown(content) if render_markdown else Text(content)
|
||||||
|
console.print()
|
||||||
|
console.print(
|
||||||
|
Panel(
|
||||||
|
body,
|
||||||
|
title=f"{__logo__} nanobot",
|
||||||
|
title_align="left",
|
||||||
|
border_style="cyan",
|
||||||
|
padding=(0, 1),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
|
def _is_exit_command(command: str) -> bool:
|
||||||
|
"""Return True when input should end interactive chat."""
|
||||||
|
return command.lower() in EXIT_COMMANDS
|
||||||
|
|
||||||
|
|
||||||
async def _read_interactive_input_async() -> str:
|
async def _read_interactive_input_async() -> str:
|
||||||
"""Read user input with arrow keys and history (runs input() in a thread)."""
|
"""Read user input with arrow keys and history (runs input() in a thread)."""
|
||||||
try:
|
try:
|
||||||
@ -410,17 +436,25 @@ def gateway(
|
|||||||
def agent(
|
def agent(
|
||||||
message: str = typer.Option(None, "--message", "-m", help="Message to send to the agent"),
|
message: str = typer.Option(None, "--message", "-m", help="Message to send to the agent"),
|
||||||
session_id: str = typer.Option("cli:default", "--session", "-s", help="Session ID"),
|
session_id: str = typer.Option("cli:default", "--session", "-s", help="Session ID"),
|
||||||
|
markdown: bool = typer.Option(True, "--markdown/--no-markdown", help="Render assistant output as Markdown"),
|
||||||
|
logs: bool = typer.Option(False, "--logs/--no-logs", help="Show nanobot runtime logs during chat"),
|
||||||
):
|
):
|
||||||
"""Interact with the agent directly."""
|
"""Interact with the agent directly."""
|
||||||
from nanobot.config.loader import load_config
|
from nanobot.config.loader import load_config
|
||||||
from nanobot.bus.queue import MessageBus
|
from nanobot.bus.queue import MessageBus
|
||||||
from nanobot.agent.loop import AgentLoop
|
from nanobot.agent.loop import AgentLoop
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
config = load_config()
|
config = load_config()
|
||||||
|
|
||||||
bus = MessageBus()
|
bus = MessageBus()
|
||||||
provider = _make_provider(config)
|
provider = _make_provider(config)
|
||||||
|
|
||||||
|
if logs:
|
||||||
|
logger.enable("nanobot")
|
||||||
|
else:
|
||||||
|
logger.disable("nanobot")
|
||||||
|
|
||||||
agent_loop = AgentLoop(
|
agent_loop = AgentLoop(
|
||||||
bus=bus,
|
bus=bus,
|
||||||
provider=provider,
|
provider=provider,
|
||||||
@ -430,17 +464,25 @@ def agent(
|
|||||||
restrict_to_workspace=config.tools.restrict_to_workspace,
|
restrict_to_workspace=config.tools.restrict_to_workspace,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Show spinner when logs are off (no output to miss); skip when logs are on
|
||||||
|
def _thinking_ctx():
|
||||||
|
if logs:
|
||||||
|
from contextlib import nullcontext
|
||||||
|
return nullcontext()
|
||||||
|
return console.status("[dim]nanobot is thinking...[/dim]", spinner="dots")
|
||||||
|
|
||||||
if message:
|
if message:
|
||||||
# Single message mode
|
# Single message mode
|
||||||
async def run_once():
|
async def run_once():
|
||||||
response = await agent_loop.process_direct(message, session_id)
|
with _thinking_ctx():
|
||||||
console.print(f"\n{__logo__} {response}")
|
response = await agent_loop.process_direct(message, session_id)
|
||||||
|
_print_agent_response(response, render_markdown=markdown)
|
||||||
|
|
||||||
asyncio.run(run_once())
|
asyncio.run(run_once())
|
||||||
else:
|
else:
|
||||||
# Interactive mode
|
# Interactive mode
|
||||||
_enable_line_editing()
|
_enable_line_editing()
|
||||||
console.print(f"{__logo__} Interactive mode (Ctrl+C to exit)\n")
|
console.print(f"{__logo__} Interactive mode (type [bold]exit[/bold] or [bold]Ctrl+C[/bold] to quit)\n")
|
||||||
|
|
||||||
# input() runs in a worker thread that can't be cancelled.
|
# input() runs in a worker thread that can't be cancelled.
|
||||||
# Without this handler, asyncio.run() would hang waiting for it.
|
# Without this handler, asyncio.run() would hang waiting for it.
|
||||||
@ -457,16 +499,29 @@ def agent(
|
|||||||
try:
|
try:
|
||||||
_flush_pending_tty_input()
|
_flush_pending_tty_input()
|
||||||
user_input = await _read_interactive_input_async()
|
user_input = await _read_interactive_input_async()
|
||||||
if not user_input.strip():
|
command = user_input.strip()
|
||||||
|
if not command:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
response = await agent_loop.process_direct(user_input, session_id)
|
if _is_exit_command(command):
|
||||||
console.print(f"\n{__logo__} {response}\n")
|
_save_history()
|
||||||
|
_restore_terminal()
|
||||||
|
console.print("\nGoodbye!")
|
||||||
|
break
|
||||||
|
|
||||||
|
with _thinking_ctx():
|
||||||
|
response = await agent_loop.process_direct(user_input, session_id)
|
||||||
|
_print_agent_response(response, render_markdown=markdown)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
_save_history()
|
_save_history()
|
||||||
_restore_terminal()
|
_restore_terminal()
|
||||||
console.print("\nGoodbye!")
|
console.print("\nGoodbye!")
|
||||||
break
|
break
|
||||||
|
except EOFError:
|
||||||
|
_save_history()
|
||||||
|
_restore_terminal()
|
||||||
|
console.print("\nGoodbye!")
|
||||||
|
break
|
||||||
|
|
||||||
asyncio.run(run_interactive())
|
asyncio.run(run_interactive())
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user