Files
webber/webber-cli/webber_cli/client.py
T
jpmschweitzerandClaude Opus 4.5 f6256363a2 feat: add streaming responses to API and CLI
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>
2026-01-11 12:58:54 +01:00

190 lines
5.7 KiB
Python

"""
Webber API client.
Communicates with the Webber API backend for agent execution.
"""
import json
import httpx
from collections.abc import AsyncIterator
from dataclasses import dataclass
from typing import Any
@dataclass
class AgentResponse:
"""Response from agent execution."""
response: str
agent_type: str
success: bool
error: str | None = None
@dataclass
class AgentInfo:
"""Information about an available agent."""
name: str
description: str
class WebberClient:
"""
Client for the Webber API.
Usage:
client = WebberClient("http://localhost:8086")
response = await client.run_agent("explore", "find python files", "/path/to/project")
"""
def __init__(
self,
base_url: str = "http://localhost:8086",
api_key: str | None = None,
timeout: float = 120.0,
):
"""
Initialize the Webber client.
Args:
base_url: Webber API URL
api_key: Optional API key for authentication
timeout: Request timeout in seconds
"""
self.base_url = base_url.rstrip("/")
self.api_key = api_key
self.timeout = timeout
self._client: httpx.AsyncClient | None = None
async def _get_client(self) -> httpx.AsyncClient:
"""Get or create the HTTP client."""
if self._client is None or self._client.is_closed:
headers = {}
if self.api_key:
headers["X-API-Key"] = self.api_key
self._client = httpx.AsyncClient(
base_url=self.base_url,
headers=headers,
timeout=self.timeout,
)
return self._client
async def close(self) -> None:
"""Close the HTTP client."""
if self._client and not self._client.is_closed:
await self._client.aclose()
self._client = None
async def health_check(self) -> bool:
"""Check if the API is healthy."""
try:
client = await self._get_client()
response = await client.get("/health")
return response.status_code == 200
except httpx.RequestError:
return False
async def list_agents(self) -> list[AgentInfo]:
"""List available agents."""
client = await self._get_client()
response = await client.get("/agents/")
response.raise_for_status()
data = response.json()
return [AgentInfo(**a) for a in data.get("agents", [])]
async def get_agent(self, agent_type: str) -> AgentInfo | None:
"""Get information about a specific agent."""
client = await self._get_client()
response = await client.get(f"/agents/{agent_type}")
if response.status_code == 404:
return None
response.raise_for_status()
return AgentInfo(**response.json())
async def run_agent(
self,
agent_type: str,
prompt: str,
working_dir: str = ".",
) -> AgentResponse:
"""
Run an agent with the given prompt.
Args:
agent_type: Type of agent (e.g., "explore")
prompt: User prompt/query
working_dir: Working directory for the agent
Returns:
AgentResponse with the result
"""
client = await self._get_client()
response = await client.post(
"/agents/run",
json={
"agent_type": agent_type,
"prompt": prompt,
"working_dir": working_dir,
},
)
response.raise_for_status()
data = response.json()
return AgentResponse(
response=data.get("response", ""),
agent_type=data.get("agent_type", agent_type),
success=data.get("success", True),
error=data.get("error"),
)
async def run_agent_stream(
self,
agent_type: str,
prompt: str,
working_dir: str = ".",
) -> AsyncIterator[str]:
"""
Run an agent with streaming response.
Args:
agent_type: Type of agent (e.g., "explore")
prompt: User prompt/query
working_dir: Working directory for the agent
Yields:
Text chunks as they arrive
"""
# Use a fresh client for streaming with longer timeout
async with httpx.AsyncClient(
base_url=self.base_url,
timeout=httpx.Timeout(300.0, connect=10.0),
) as client:
async with client.stream(
"POST",
"/agents/stream",
json={
"agent_type": agent_type,
"prompt": prompt,
"working_dir": working_dir,
},
) as response:
response.raise_for_status()
async for line in response.aiter_lines():
if line.startswith("data: "):
try:
data = json.loads(line[6:])
event = data.get("event")
if event == "chunk":
yield data.get("data", "")
elif event == "error":
raise Exception(data.get("data", "Unknown error"))
elif event == "done":
break
except json.JSONDecodeError:
continue
async def __aenter__(self) -> "WebberClient":
"""Async context manager entry."""
return self
async def __aexit__(self, *args: Any) -> None:
"""Async context manager exit."""
await self.close()