134 lines
3.9 KiB
Python
134 lines
3.9 KiB
Python
import time
|
|
from pathlib import Path
|
|
|
|
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
|
|
from fastapi.responses import RedirectResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
|
from .config import ServerSettings
|
|
from .grid import GridManager
|
|
from .models import (
|
|
ActionPayload,
|
|
GridDescriptor,
|
|
GridInitRequest,
|
|
GridPlanRequest,
|
|
GridRefreshRequest,
|
|
)
|
|
from .planner import GridPlanner
|
|
from .streamer import ScreenshotStreamer
|
|
|
|
|
|
settings = ServerSettings()
|
|
manager = GridManager(settings)
|
|
planner = GridPlanner()
|
|
streamer = ScreenshotStreamer()
|
|
|
|
app = FastAPI(
|
|
title="Clickthrough",
|
|
description="Grid-aware surface that lets an agent plan clicks, drags, and typing on a fake screenshot",
|
|
version="0.3.0",
|
|
)
|
|
|
|
client_dir = Path(__file__).resolve().parent.parent / "client"
|
|
if client_dir.exists():
|
|
app.mount("/ui", StaticFiles(directory=str(client_dir), html=True), name="ui")
|
|
|
|
|
|
@app.get("/")
|
|
async def root():
|
|
if client_dir.exists():
|
|
return RedirectResponse("/ui/")
|
|
return {"status": "ok", "grid_count": manager.grid_count}
|
|
|
|
|
|
@app.get("/health")
|
|
def health_check() -> dict[str, str]:
|
|
return {"status": "ok", "grid_count": str(manager.grid_count)}
|
|
|
|
|
|
@app.post("/grid/init", response_model=GridDescriptor)
|
|
def init_grid(request: GridInitRequest) -> GridDescriptor:
|
|
grid = manager.create_grid(request)
|
|
return grid.describe()
|
|
|
|
|
|
@app.post("/grid/action")
|
|
def apply_action(payload: ActionPayload):
|
|
try:
|
|
grid = manager.get_grid(payload.grid_id)
|
|
except KeyError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
return grid.apply_action(payload)
|
|
|
|
|
|
@app.get("/grid/{grid_id}/summary")
|
|
def grid_summary(grid_id: str):
|
|
try:
|
|
grid = manager.get_grid(grid_id)
|
|
except KeyError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
descriptor = grid.describe()
|
|
return {
|
|
"grid_id": grid_id,
|
|
"summary": planner.describe(descriptor),
|
|
"details": grid.summary(),
|
|
"descriptor": descriptor,
|
|
}
|
|
|
|
|
|
@app.get("/grid/{grid_id}/history")
|
|
def grid_history(grid_id: str):
|
|
try:
|
|
history = manager.get_history(grid_id)
|
|
except KeyError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
return {"grid_id": grid_id, "history": history}
|
|
|
|
|
|
@app.post("/grid/{grid_id}/plan")
|
|
def plan_grid(grid_id: str, request: GridPlanRequest):
|
|
try:
|
|
grid = manager.get_grid(grid_id)
|
|
except KeyError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
descriptor = grid.describe()
|
|
payload = planner.build_payload(
|
|
descriptor,
|
|
action=request.action,
|
|
preferred_label=request.preferred_label,
|
|
text=request.text,
|
|
comment=request.comment,
|
|
)
|
|
result = grid.preview_action(payload)
|
|
return {"plan": payload.model_dump(), "result": result, "descriptor": descriptor}
|
|
|
|
|
|
@app.post("/grid/{grid_id}/refresh")
|
|
async def refresh_grid(grid_id: str, payload: GridRefreshRequest):
|
|
try:
|
|
grid = manager.get_grid(grid_id)
|
|
except KeyError as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
grid.update_screenshot(payload.screenshot_base64, payload.memo)
|
|
descriptor = grid.describe()
|
|
await streamer.broadcast(
|
|
grid_id,
|
|
{
|
|
"grid_id": grid_id,
|
|
"timestamp": time.time(),
|
|
"descriptor": descriptor,
|
|
"screenshot_base64": payload.screenshot_base64,
|
|
},
|
|
)
|
|
return {"status": "updated", "grid_id": grid_id}
|
|
|
|
|
|
@app.websocket("/stream/screenshots")
|
|
async def stream_screenshots(websocket: WebSocket, grid_id: str | None = None):
|
|
key = await streamer.connect(websocket, grid_id)
|
|
try:
|
|
while True:
|
|
await websocket.receive_text()
|
|
except WebSocketDisconnect:
|
|
streamer.disconnect(websocket, key)
|