mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 19:43:15 +00:00
179 lines
6.7 KiB
Python
179 lines
6.7 KiB
Python
|
|
"""Server-side, read-only Currents.dev tools for e2e test investigation.
|
||
|
|
|
||
|
|
Credentials are per-user (encrypted at rest in the user-credentials Store
|
||
|
|
namespace). The tools run in the LangGraph server process and call the
|
||
|
|
Currents REST API directly — the sandbox never holds a Currents key.
|
||
|
|
|
||
|
|
The surface is intentionally read-only: fetch runs, instances, test results,
|
||
|
|
and list projects so the agent can dig into e2e test failures including
|
||
|
|
screenshots and DOM snapshots.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import logging
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
import httpx
|
||
|
|
from langchain_core.tools import BaseTool, StructuredTool
|
||
|
|
|
||
|
|
from ..dashboard.user_credentials import CURRENTS_API_BASE, get_currents_api_key
|
||
|
|
|
||
|
|
logger = logging.getLogger(__name__)
|
||
|
|
|
||
|
|
_TIMEOUT = httpx.Timeout(30.0, connect=10.0)
|
||
|
|
|
||
|
|
|
||
|
|
def _headers(api_key: str) -> dict[str, str]:
|
||
|
|
return {
|
||
|
|
"Authorization": f"Bearer {api_key}",
|
||
|
|
"Accept": "application/json",
|
||
|
|
"Accept-Encoding": "gzip",
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
async def _get(path: str, api_key: str, **params: Any) -> dict[str, Any]:
|
||
|
|
url = f"{CURRENTS_API_BASE}{path}"
|
||
|
|
async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
|
||
|
|
resp = await client.get(url, headers=_headers(api_key), params=params)
|
||
|
|
resp.raise_for_status()
|
||
|
|
return resp.json()
|
||
|
|
|
||
|
|
|
||
|
|
def _make_tools(api_key: str) -> list[BaseTool]:
|
||
|
|
async def currents_list_projects(
|
||
|
|
limit: int = 10,
|
||
|
|
starting_after: str | None = None,
|
||
|
|
) -> dict[str, Any]:
|
||
|
|
"""List Currents.dev projects for your organization.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
limit: Maximum number of items to return (default 10, max 50).
|
||
|
|
starting_after: Cursor for pagination.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Dictionary with project list, or an error message.
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
params: dict[str, Any] = {"limit": max(1, min(limit, 50))}
|
||
|
|
if starting_after:
|
||
|
|
params["starting_after"] = starting_after
|
||
|
|
return await _get("/projects", api_key, **params)
|
||
|
|
except Exception as e: # noqa: BLE001
|
||
|
|
logger.warning("currents_list_projects failed", exc_info=True)
|
||
|
|
return {"success": False, "error": f"{type(e).__name__}: {e}"}
|
||
|
|
|
||
|
|
async def currents_get_run(run_id: str) -> dict[str, Any]:
|
||
|
|
"""Get a single Currents.dev test run by ID with full details.
|
||
|
|
|
||
|
|
Use this to inspect a specific e2e test run including specs,
|
||
|
|
screenshots, video URLs, and test stats.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
run_id: The Currents run ID (e.g. "run_abc123").
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Dictionary with the run details, or an error message.
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
return await _get(f"/runs/{run_id}", api_key)
|
||
|
|
except Exception as e: # noqa: BLE001
|
||
|
|
logger.warning("currents_get_run failed", exc_info=True)
|
||
|
|
return {"success": False, "error": f"{type(e).__name__}: {e}"}
|
||
|
|
|
||
|
|
async def currents_find_run(
|
||
|
|
project_id: str,
|
||
|
|
ci_build_id: str | None = None,
|
||
|
|
branch: str | None = None,
|
||
|
|
) -> dict[str, Any]:
|
||
|
|
"""Find the most recent completed Currents.dev run matching criteria.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
project_id: The Currents project ID (e.g. "proj_abc123").
|
||
|
|
ci_build_id: Optional CI build ID to find an exact run.
|
||
|
|
branch: Optional branch name or prefix (append * for prefix match).
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Dictionary with the run details, or an error message.
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
params: dict[str, Any] = {"projectId": project_id}
|
||
|
|
if ci_build_id:
|
||
|
|
params["ciBuildId"] = ci_build_id
|
||
|
|
if branch:
|
||
|
|
params["branch"] = branch
|
||
|
|
return await _get("/runs/find", api_key, **params)
|
||
|
|
except Exception as e: # noqa: BLE001
|
||
|
|
logger.warning("currents_find_run failed", exc_info=True)
|
||
|
|
return {"success": False, "error": f"{type(e).__name__}: {e}"}
|
||
|
|
|
||
|
|
async def currents_list_project_runs(
|
||
|
|
project_id: str,
|
||
|
|
limit: int = 10,
|
||
|
|
status: str | None = None,
|
||
|
|
branch: str | None = None,
|
||
|
|
starting_after: str | None = None,
|
||
|
|
ending_before: str | None = None,
|
||
|
|
) -> dict[str, Any]:
|
||
|
|
"""List runs for a Currents.dev project with optional filters.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
project_id: The Currents project ID.
|
||
|
|
limit: Maximum number of runs to return (default 10, max 50).
|
||
|
|
status: Optional status filter: PASSED, FAILED, RUNNING, FAILING.
|
||
|
|
branch: Optional branch filter (append * for prefix match).
|
||
|
|
starting_after: Cursor for pagination (next page).
|
||
|
|
ending_before: Cursor for pagination (previous page).
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Dictionary with a list of runs, or an error message.
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
params: dict[str, Any] = {"limit": max(1, min(limit, 50))}
|
||
|
|
if status:
|
||
|
|
params["status"] = status
|
||
|
|
if branch:
|
||
|
|
params["branches[]"] = branch
|
||
|
|
if starting_after:
|
||
|
|
params["starting_after"] = starting_after
|
||
|
|
if ending_before:
|
||
|
|
params["ending_before"] = ending_before
|
||
|
|
return await _get(f"/projects/{project_id}/runs", api_key, **params)
|
||
|
|
except Exception as e: # noqa: BLE001
|
||
|
|
logger.warning("currents_list_project_runs failed", exc_info=True)
|
||
|
|
return {"success": False, "error": f"{type(e).__name__}: {e}"}
|
||
|
|
|
||
|
|
async def currents_get_instance(instance_id: str) -> dict[str, Any]:
|
||
|
|
"""Get a single Currents.dev spec file execution instance by ID.
|
||
|
|
|
||
|
|
An instance represents one spec file's execution within a run,
|
||
|
|
including detailed test results, errors, and attempt history.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
instance_id: The Currents instance ID (e.g. "inst_abc123").
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Dictionary with the instance details, or an error message.
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
return await _get(f"/instances/{instance_id}", api_key)
|
||
|
|
except Exception as e: # noqa: BLE001
|
||
|
|
logger.warning("currents_get_instance failed", exc_info=True)
|
||
|
|
return {"success": False, "error": f"{type(e).__name__}: {e}"}
|
||
|
|
|
||
|
|
return [
|
||
|
|
StructuredTool.from_function(coroutine=currents_list_projects),
|
||
|
|
StructuredTool.from_function(coroutine=currents_get_run),
|
||
|
|
StructuredTool.from_function(coroutine=currents_find_run),
|
||
|
|
StructuredTool.from_function(coroutine=currents_list_project_runs),
|
||
|
|
StructuredTool.from_function(coroutine=currents_get_instance),
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
async def load_currents_tools(login: str) -> list[BaseTool]:
|
||
|
|
"""Return read-only Currents tools when the user has connected Currents."""
|
||
|
|
api_key = await get_currents_api_key(login)
|
||
|
|
if not api_key:
|
||
|
|
return []
|
||
|
|
return _make_tools(api_key)
|