Add real-time streaming support for agent responses using Server-Sent Events (SSE). Responses now appear as they're generated instead of waiting for completion. - Add run_stream method to BaseAgent and ExploreAgentImpl - Add /agents/stream SSE endpoint to API router - Add run_agent_stream method to CLI client - Add --stream flag to chat and explore commands (enabled by default) - Use --no-stream for batch mode with spinner Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
309 lines
9.3 KiB
Python
309 lines
9.3 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Webber CLI - Client for the Webber API.
|
|
|
|
Usage:
|
|
webber-cli --help
|
|
webber-cli chat [OPTIONS]
|
|
webber-cli explore QUERY [OPTIONS]
|
|
"""
|
|
import asyncio
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import typer
|
|
from rich.live import Live
|
|
from rich.markdown import Markdown
|
|
from rich.panel import Panel
|
|
|
|
from webber_cli.client import WebberClient
|
|
from webber_cli.theme import get_console, get_theme
|
|
|
|
app = typer.Typer(
|
|
name="webber-cli",
|
|
help="CLI client for the Webber API",
|
|
no_args_is_help=True,
|
|
add_completion=False,
|
|
)
|
|
|
|
console = get_console()
|
|
|
|
# Default API URL (can be overridden via env or option)
|
|
# Development port is 8095, production is 8086
|
|
DEFAULT_API_URL = os.environ.get("WEBBER_API_URL", "http://localhost:8095")
|
|
|
|
|
|
def version_callback(value: bool) -> None:
|
|
"""Display version and exit."""
|
|
if value:
|
|
from cli import __version__
|
|
console.print(f"[title]webber-cli[/] version [success]{__version__}[/]")
|
|
raise typer.Exit()
|
|
|
|
|
|
@app.callback()
|
|
def main(
|
|
version: bool = typer.Option(
|
|
False,
|
|
"--version",
|
|
"-v",
|
|
callback=version_callback,
|
|
is_eager=True,
|
|
help="Show version and exit",
|
|
),
|
|
) -> None:
|
|
"""Webber CLI - Talk to the Webber API."""
|
|
pass
|
|
|
|
|
|
@app.command()
|
|
def chat(
|
|
directory: str = typer.Option(
|
|
".",
|
|
"--directory",
|
|
"-d",
|
|
help="Working directory for exploration",
|
|
),
|
|
api_url: str = typer.Option(
|
|
DEFAULT_API_URL,
|
|
"--api",
|
|
"-a",
|
|
help="Webber API URL",
|
|
),
|
|
agent: str = typer.Option(
|
|
"explore",
|
|
"--agent",
|
|
help="Agent to use",
|
|
),
|
|
stream: bool = typer.Option(
|
|
True,
|
|
"--stream/--no-stream",
|
|
"-s",
|
|
help="Stream responses in real-time",
|
|
),
|
|
) -> None:
|
|
"""
|
|
Start interactive chat session.
|
|
|
|
Connects to the Webber API backend for agent execution.
|
|
"""
|
|
working_dir = str(Path(directory).resolve())
|
|
|
|
if not Path(working_dir).exists():
|
|
console.print(f"[error]Error:[/] Directory not found: {working_dir}")
|
|
raise typer.Exit(1)
|
|
|
|
try:
|
|
asyncio.run(_chat_loop(api_url, agent, working_dir, stream))
|
|
except KeyboardInterrupt:
|
|
console.print("\n[dim]Goodbye![/]")
|
|
|
|
|
|
async def _chat_loop(
|
|
api_url: str, agent_type: str, working_dir: str, stream: bool = True
|
|
) -> None:
|
|
"""Interactive chat loop."""
|
|
theme = get_theme()
|
|
|
|
async with WebberClient(api_url) as client:
|
|
# Check API health
|
|
if not await client.health_check():
|
|
console.print(f"[error]Error:[/] Cannot connect to Webber API at {api_url}")
|
|
console.print("[dim]Make sure the server is running: ./wakeup.sh[/]")
|
|
return
|
|
|
|
# Get agent info
|
|
agent_info = await client.get_agent(agent_type)
|
|
if not agent_info:
|
|
console.print(f"[error]Error:[/] Unknown agent: {agent_type}")
|
|
agents = await client.list_agents()
|
|
console.print("[dim]Available agents:[/]")
|
|
for a in agents:
|
|
console.print(f" - {a.name}: {a.description}")
|
|
return
|
|
|
|
# Welcome message
|
|
console.print()
|
|
console.print(f"[title]Webber CLI[/] [dim]→ {api_url}[/]")
|
|
console.print(f"[dim]Working in:[/] [path]{working_dir}[/]")
|
|
console.print(f"[dim]Agent:[/] {agent_info.name} - {agent_info.description}")
|
|
mode = "streaming" if stream else "batch"
|
|
console.print(f"[dim]Mode:[/] {mode}")
|
|
console.print()
|
|
console.print("[dim]Type 'exit' to quit, 'clear' to clear screen.[/]")
|
|
console.print()
|
|
|
|
# Chat loop
|
|
while True:
|
|
try:
|
|
user_input = console.input("[prompt]>[/] ").strip()
|
|
|
|
if not user_input:
|
|
continue
|
|
|
|
if user_input.lower() in ("exit", "quit", "/exit", "/quit"):
|
|
console.print("[dim]Goodbye![/]")
|
|
break
|
|
|
|
if user_input.lower() in ("clear", "/clear"):
|
|
console.clear()
|
|
continue
|
|
|
|
if user_input.lower().startswith("cd "):
|
|
new_dir = user_input[3:].strip()
|
|
new_path = Path(new_dir).resolve()
|
|
if new_path.exists() and new_path.is_dir():
|
|
working_dir = str(new_path)
|
|
console.print(f"[info]Changed to:[/] [path]{working_dir}[/]")
|
|
else:
|
|
console.print(f"[error]Directory not found:[/] {new_dir}")
|
|
continue
|
|
|
|
console.print()
|
|
|
|
if stream:
|
|
# Stream response in real-time
|
|
full_response = ""
|
|
try:
|
|
async for chunk in client.run_agent_stream(
|
|
agent_type, user_input, working_dir
|
|
):
|
|
sys.stdout.write(chunk)
|
|
sys.stdout.flush()
|
|
full_response += chunk
|
|
console.print() # Newline after streaming
|
|
except Exception as e:
|
|
console.print(f"\n[error]Stream error:[/] {e}")
|
|
else:
|
|
# Batch mode with spinner
|
|
with console.status("[info]Thinking...[/]", spinner=theme.spinner):
|
|
result = await client.run_agent(agent_type, user_input, working_dir)
|
|
|
|
if result.success:
|
|
console.print(Markdown(result.response))
|
|
else:
|
|
console.print(f"[error]Error:[/] {result.error}")
|
|
|
|
console.print()
|
|
|
|
except KeyboardInterrupt:
|
|
console.print("\n[dim]Use 'exit' to quit.[/]")
|
|
|
|
except Exception as e:
|
|
console.print(f"[error]Error:[/] {e}")
|
|
|
|
|
|
@app.command()
|
|
def explore(
|
|
query: str = typer.Argument(..., help="What to search for"),
|
|
directory: str = typer.Option(
|
|
".",
|
|
"--directory",
|
|
"-d",
|
|
help="Working directory",
|
|
),
|
|
api_url: str = typer.Option(
|
|
DEFAULT_API_URL,
|
|
"--api",
|
|
"-a",
|
|
help="Webber API URL",
|
|
),
|
|
stream: bool = typer.Option(
|
|
True,
|
|
"--stream/--no-stream",
|
|
"-s",
|
|
help="Stream responses in real-time",
|
|
),
|
|
) -> None:
|
|
"""
|
|
One-shot codebase exploration.
|
|
|
|
Sends a query to the Webber API and displays the result.
|
|
"""
|
|
working_dir = str(Path(directory).resolve())
|
|
|
|
if not Path(working_dir).exists():
|
|
console.print(f"[error]Error:[/] Directory not found: {working_dir}")
|
|
raise typer.Exit(1)
|
|
|
|
asyncio.run(_explore(api_url, query, working_dir, stream))
|
|
|
|
|
|
async def _explore(
|
|
api_url: str, query: str, working_dir: str, stream: bool = True
|
|
) -> None:
|
|
"""Execute exploration query."""
|
|
theme = get_theme()
|
|
|
|
async with WebberClient(api_url) as client:
|
|
# Check API health
|
|
if not await client.health_check():
|
|
console.print(f"[error]Error:[/] Cannot connect to Webber API at {api_url}")
|
|
console.print("[dim]Make sure the server is running: ./wakeup.sh[/]")
|
|
return
|
|
|
|
console.print(f"[dim]Exploring:[/] [path]{working_dir}[/]")
|
|
console.print(f"[dim]Query:[/] {query}")
|
|
console.print()
|
|
|
|
if stream:
|
|
# Stream response in real-time
|
|
try:
|
|
async for chunk in client.run_agent_stream("explore", query, working_dir):
|
|
sys.stdout.write(chunk)
|
|
sys.stdout.flush()
|
|
console.print() # Newline after streaming
|
|
except Exception as e:
|
|
console.print(f"\n[error]Stream error:[/] {e}")
|
|
else:
|
|
# Batch mode with spinner
|
|
with console.status("[info]Searching...[/]", spinner=theme.spinner):
|
|
result = await client.run_agent("explore", query, working_dir)
|
|
|
|
if result.success:
|
|
console.print(Panel(
|
|
Markdown(result.response),
|
|
title="[success]Findings[/]",
|
|
border_style=theme.colors.border_success,
|
|
))
|
|
else:
|
|
console.print(Panel(
|
|
f"[error]{result.error}[/]",
|
|
title="[error]Error[/]",
|
|
border_style=theme.colors.border_error,
|
|
))
|
|
|
|
|
|
@app.command()
|
|
def status(
|
|
api_url: str = typer.Option(
|
|
DEFAULT_API_URL,
|
|
"--api",
|
|
"-a",
|
|
help="Webber API URL",
|
|
),
|
|
) -> None:
|
|
"""Check API status and list available agents."""
|
|
asyncio.run(_status(api_url))
|
|
|
|
|
|
async def _status(api_url: str) -> None:
|
|
"""Check API status."""
|
|
async with WebberClient(api_url) as client:
|
|
console.print(f"[dim]API URL:[/] {api_url}")
|
|
|
|
if await client.health_check():
|
|
console.print("[success]Status:[/] Connected")
|
|
|
|
agents = await client.list_agents()
|
|
console.print(f"\n[dim]Available agents ({len(agents)}):[/]")
|
|
for agent in agents:
|
|
console.print(f" [info]{agent.name}[/]: {agent.description}")
|
|
else:
|
|
console.print("[error]Status:[/] Cannot connect")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app()
|