mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-02 07:23:15 +00:00
* Run reviewer eval in a GitHub Action; make dashboard a read-only progress view The dashboard launched the eval as a subprocess inside the serving deployment worker, so a container recycle killed long runs and discarded results that had already completed server-side. Move the harness to a workflow_dispatch Action (run on prod). run_eval now publishes status/progress/log-tail to the LangGraph store record the dashboard reads, so /admin/evals stays a live view; a killed Action surfaces as failed via the stale-heartbeat reconcile. * reviewer_eval workflow: pass inputs via env, no shell interpolation Addresses the reviewer finding: workflow_dispatch string inputs were interpolated into the run: block (limit unquoted), allowing shell injection in a job holding LANGSMITH/ANTHROPIC keys. Pass inputs through env and reference quoted "$VARS"; validate limit is numeric and build its flag in bash. --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
218 lines
6.5 KiB
TypeScript
218 lines
6.5 KiB
TypeScript
import { Navigate, createFileRoute } from "@tanstack/react-router"
|
|
import { useQuery } from "@tanstack/react-query"
|
|
import { useEffect, useRef, useState } from "react"
|
|
|
|
import type { ReviewerEvalStatus } from "@/lib/api"
|
|
import { AppShell, SettingsSection } from "@/components/AppShell"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Skeleton } from "@/components/ui/skeleton"
|
|
import { api } from "@/lib/api"
|
|
import { useSession } from "@/lib/session"
|
|
|
|
export const Route = createFileRoute("/admin_/evals")({ component: ReviewerEvalPage })
|
|
|
|
function ReviewerEvalPage() {
|
|
const session = useSession()
|
|
|
|
if (session.isLoading) {
|
|
return (
|
|
<main className="p-6">
|
|
<Skeleton className="h-64 w-full" />
|
|
</main>
|
|
)
|
|
}
|
|
if (!session.data) return <Navigate to="/login" />
|
|
if (!session.data.is_admin) return <Navigate to="/my-settings" />
|
|
|
|
return (
|
|
<AppShell
|
|
user={session.data}
|
|
title="Reviewer eval"
|
|
description="Triggered from the Reviewer eval GitHub Action (run it on the prod branch). Progress streams here live."
|
|
backTo={{ to: "/admin", label: "Back to Admin" }}
|
|
>
|
|
<ReviewerEvalStatusSection />
|
|
<ReviewerEvalLogs />
|
|
</AppShell>
|
|
)
|
|
}
|
|
|
|
function useReviewerEvalStatus() {
|
|
return useQuery({
|
|
queryKey: ["reviewerEval"],
|
|
queryFn: api.getReviewerEval,
|
|
refetchInterval: (query) =>
|
|
query.state.data?.status === "running" ? 5000 : false,
|
|
})
|
|
}
|
|
|
|
function ReviewerEvalStatusSection() {
|
|
const status = useReviewerEvalStatus()
|
|
return (
|
|
<SettingsSection
|
|
title="Current run"
|
|
description="Status and resolved configuration for the latest reviewer eval run."
|
|
>
|
|
<ReviewerEvalStatusView data={status.data ?? null} />
|
|
</SettingsSection>
|
|
)
|
|
}
|
|
|
|
function progressLabel(data: ReviewerEvalStatus): string | null {
|
|
if (!data.progress) return null
|
|
const { completed, total } = data.progress
|
|
return `${completed} / ${total ?? "?"}`
|
|
}
|
|
|
|
function ReviewerEvalStatusView({ data }: { data: ReviewerEvalStatus | null }) {
|
|
if (!data) {
|
|
return (
|
|
<div className="p-4 text-xs text-muted-foreground">
|
|
Loading reviewer eval status…
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const config = data.config_snapshot
|
|
return (
|
|
<div className="grid gap-2 p-4 text-xs text-muted-foreground sm:grid-cols-2">
|
|
<StatusLine label="Status" value={data.status} strong />
|
|
<StatusLine label="Progress" value={progressLabel(data)} />
|
|
<StatusLine label="Run name" value={data.run_name ?? config?.experiment_prefix} />
|
|
<StatusLine label="Dataset" value={config?.dataset_name} />
|
|
<StatusLine label="Limit" value={data.limit ? String(data.limit) : "full dataset"} />
|
|
<StatusLine label="Model" value={config?.model_id} />
|
|
<StatusLine label="Effort" value={config?.reasoning_effort} />
|
|
<StatusLine label="Score mode" value={config?.score_mode} />
|
|
<StatusLine label="Threshold" value={config?.severity_threshold} />
|
|
<StatusLine label="Cap" value={config ? String(config.cap) : null} />
|
|
<StatusLine label="LangSmith project" value={data.langsmith_project} />
|
|
<StatusLine label="Triggered by" value={data.created_by} />
|
|
{data.started_at && (
|
|
<StatusLine
|
|
label="Started"
|
|
value={new Date(data.started_at).toLocaleString()}
|
|
/>
|
|
)}
|
|
{data.finished_at && (
|
|
<StatusLine
|
|
label="Finished"
|
|
value={new Date(data.finished_at).toLocaleString()}
|
|
/>
|
|
)}
|
|
{data.experiment_url && (
|
|
<a
|
|
href={data.experiment_url}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="underline hover:text-foreground"
|
|
>
|
|
View experiment in LangSmith
|
|
</a>
|
|
)}
|
|
{data.github_run_url && (
|
|
<a
|
|
href={data.github_run_url}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="underline hover:text-foreground"
|
|
>
|
|
View GitHub run
|
|
</a>
|
|
)}
|
|
{data.error && <span className="text-destructive">{data.error}</span>}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function StatusLine({
|
|
label,
|
|
value,
|
|
strong = false,
|
|
}: {
|
|
label: string
|
|
value: string | null | undefined
|
|
strong?: boolean
|
|
}) {
|
|
return (
|
|
<span>
|
|
{label}:{" "}
|
|
<span className={strong ? "font-medium text-foreground" : ""}>
|
|
{value || "—"}
|
|
</span>
|
|
</span>
|
|
)
|
|
}
|
|
|
|
function ReviewerEvalLogs() {
|
|
const status = useReviewerEvalStatus()
|
|
const logTail = status.data?.log_tail ?? null
|
|
const running = status.data?.status === "running"
|
|
|
|
const scrollRef = useRef<HTMLPreElement>(null)
|
|
const [follow, setFollow] = useState(true)
|
|
const [copied, setCopied] = useState(false)
|
|
|
|
useEffect(() => {
|
|
if (follow && scrollRef.current) {
|
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight
|
|
}
|
|
}, [logTail, follow])
|
|
|
|
const copyLogs = async () => {
|
|
if (!logTail) return
|
|
await navigator.clipboard.writeText(logTail)
|
|
setCopied(true)
|
|
window.setTimeout(() => setCopied(false), 1500)
|
|
}
|
|
|
|
return (
|
|
<SettingsSection
|
|
title="Output"
|
|
description="Live tail of the eval output. Each PR logs start and finish lines while the run is active."
|
|
action={
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => void copyLogs()}
|
|
disabled={!logTail}
|
|
>
|
|
{copied ? "Copied" : "Copy logs"}
|
|
</Button>
|
|
<label className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
<input
|
|
type="checkbox"
|
|
checked={follow}
|
|
onChange={(e) => setFollow(e.target.checked)}
|
|
/>
|
|
Follow
|
|
</label>
|
|
</div>
|
|
}
|
|
>
|
|
<div className="p-4">
|
|
{logTail ? (
|
|
<pre
|
|
ref={scrollRef}
|
|
onScroll={(e) => {
|
|
const el = e.currentTarget
|
|
const atBottom =
|
|
el.scrollHeight - el.scrollTop - el.clientHeight < 24
|
|
setFollow(atBottom)
|
|
}}
|
|
className="max-h-[28rem] overflow-auto whitespace-pre-wrap break-words rounded-md bg-muted/50 p-3 font-mono text-xs text-foreground"
|
|
>
|
|
{logTail}
|
|
</pre>
|
|
) : (
|
|
<p className="text-xs text-muted-foreground">
|
|
{running
|
|
? "Waiting for output…"
|
|
: "No output yet. Trigger the Reviewer eval GitHub Action to see logs here."}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</SettingsSection>
|
|
)
|
|
}
|