Improve benchmark case details and score chart scaling (#146)
Co-authored-by: Yaowei Zheng <hiyouga@buaa.edu.cn>
This commit is contained in:
@@ -1292,6 +1292,8 @@ export interface BenchmarksResponse {
|
||||
benchmarks: BenchmarkSummary[];
|
||||
}
|
||||
|
||||
export type CaseMaterial = "statement" | "rubric";
|
||||
|
||||
/** Public Benchmark Case metadata. Rubric and Gold content are never included. */
|
||||
export interface BenchmarkCaseSummary {
|
||||
id: string;
|
||||
|
||||
@@ -4,16 +4,72 @@
|
||||
* GET /api/projects/:p/agents/:a/benchmarks/:benchmarkId/cases
|
||||
* GET /api/projects/:p/agents/:a/benchmarks/:benchmarkId/cases/:caseId/files
|
||||
* GET /api/projects/:p/agents/:a/benchmarks/:benchmarkId/cases/:caseId/files/content
|
||||
* GET /api/projects/:p/agents/:a/benchmarks/:benchmarkId/cases/:caseId/rubric/files
|
||||
* GET /api/projects/:p/agents/:a/benchmarks/:benchmarkId/cases/:caseId/rubric/files/content
|
||||
* Returns the Agent's Benchmark list (title/description from benchmark_config.toml)
|
||||
* along with the evaluations[] from scoreboard.yaml.
|
||||
*/
|
||||
import { Hono } from "hono";
|
||||
import { Hono, type Context } from "hono";
|
||||
import type { AppEnv } from "../../auth/middleware.js";
|
||||
import type { AppDeps } from "../../app.js";
|
||||
import type { CaseMaterial } from "../../api/types.js";
|
||||
import { requireValidId } from "../validate.js";
|
||||
|
||||
const TEXT_PREVIEW_BYTES = 256 * 1024;
|
||||
|
||||
function listCaseFiles(deps: AppDeps, material: CaseMaterial) {
|
||||
return async (c: Context<AppEnv>) => {
|
||||
const projectId = requireValidId(c, "projectId");
|
||||
const agentId = requireValidId(c, "agentId");
|
||||
const benchmarkId = requireValidId(c, "benchmarkId");
|
||||
const caseId = requireValidId(c, "caseId");
|
||||
deps.projectService.requireProjectAccess(c.var.user.userId, projectId);
|
||||
await deps.agentConfigService.requireExists(projectId, agentId);
|
||||
return c.json(
|
||||
await deps.benchmarks.listCaseFiles(
|
||||
projectId,
|
||||
agentId,
|
||||
benchmarkId,
|
||||
caseId,
|
||||
c.req.query("path") ?? "",
|
||||
material,
|
||||
),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function readCaseFile(deps: AppDeps, material: CaseMaterial) {
|
||||
return async (c: Context<AppEnv>) => {
|
||||
const projectId = requireValidId(c, "projectId");
|
||||
const agentId = requireValidId(c, "agentId");
|
||||
const benchmarkId = requireValidId(c, "benchmarkId");
|
||||
const caseId = requireValidId(c, "caseId");
|
||||
deps.projectService.requireProjectAccess(c.var.user.userId, projectId);
|
||||
await deps.agentConfigService.requireExists(projectId, agentId);
|
||||
const download = c.req.query("download") === "1";
|
||||
const boundedPreview = !download && c.req.query("preview") === "1";
|
||||
const { data, fileName, contentType, scriptable, truncated } =
|
||||
await deps.benchmarks.readCaseFile(
|
||||
projectId,
|
||||
agentId,
|
||||
benchmarkId,
|
||||
caseId,
|
||||
c.req.query("path") ?? "",
|
||||
material,
|
||||
boundedPreview ? { maxBytes: TEXT_PREVIEW_BYTES } : undefined,
|
||||
);
|
||||
return new Response(new Uint8Array(data), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": !download && scriptable ? "text/plain; charset=utf-8" : contentType,
|
||||
"Content-Disposition": `${download ? "attachment" : "inline"}; filename*=UTF-8''${encodeURIComponent(fileName)}`,
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
...(truncated ? { "X-Content-Truncated": "1" } : {}),
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function benchmarksRoutes(deps: AppDeps): Hono<AppEnv> {
|
||||
const app = new Hono<AppEnv>();
|
||||
|
||||
@@ -34,52 +90,10 @@ export function benchmarksRoutes(deps: AppDeps): Hono<AppEnv> {
|
||||
return c.json(await deps.benchmarks.listCases(projectId, agentId, benchmarkId));
|
||||
});
|
||||
|
||||
app.get("/:benchmarkId/cases/:caseId/files", async (c) => {
|
||||
const projectId = requireValidId(c, "projectId");
|
||||
const agentId = requireValidId(c, "agentId");
|
||||
const benchmarkId = requireValidId(c, "benchmarkId");
|
||||
const caseId = requireValidId(c, "caseId");
|
||||
deps.projectService.requireProjectAccess(c.var.user.userId, projectId);
|
||||
await deps.agentConfigService.requireExists(projectId, agentId);
|
||||
return c.json(
|
||||
await deps.benchmarks.listCaseFiles(
|
||||
projectId,
|
||||
agentId,
|
||||
benchmarkId,
|
||||
caseId,
|
||||
c.req.query("path") ?? "",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
app.get("/:benchmarkId/cases/:caseId/files/content", async (c) => {
|
||||
const projectId = requireValidId(c, "projectId");
|
||||
const agentId = requireValidId(c, "agentId");
|
||||
const benchmarkId = requireValidId(c, "benchmarkId");
|
||||
const caseId = requireValidId(c, "caseId");
|
||||
deps.projectService.requireProjectAccess(c.var.user.userId, projectId);
|
||||
await deps.agentConfigService.requireExists(projectId, agentId);
|
||||
const download = c.req.query("download") === "1";
|
||||
const boundedPreview = !download && c.req.query("preview") === "1";
|
||||
const { data, fileName, contentType, scriptable, truncated } =
|
||||
await deps.benchmarks.readCaseFile(
|
||||
projectId,
|
||||
agentId,
|
||||
benchmarkId,
|
||||
caseId,
|
||||
c.req.query("path") ?? "",
|
||||
boundedPreview ? { maxBytes: TEXT_PREVIEW_BYTES } : undefined,
|
||||
);
|
||||
return new Response(new Uint8Array(data), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": !download && scriptable ? "text/plain; charset=utf-8" : contentType,
|
||||
"Content-Disposition": `${download ? "attachment" : "inline"}; filename*=UTF-8''${encodeURIComponent(fileName)}`,
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
...(truncated ? { "X-Content-Truncated": "1" } : {}),
|
||||
},
|
||||
});
|
||||
});
|
||||
app.get("/:benchmarkId/cases/:caseId/files", listCaseFiles(deps, "statement"));
|
||||
app.get("/:benchmarkId/cases/:caseId/files/content", readCaseFile(deps, "statement"));
|
||||
app.get("/:benchmarkId/cases/:caseId/rubric/files", listCaseFiles(deps, "rubric"));
|
||||
app.get("/:benchmarkId/cases/:caseId/rubric/files/content", readCaseFile(deps, "rubric"));
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import type {
|
||||
BenchmarkRunScore,
|
||||
BenchmarkSummary,
|
||||
BenchmarksResponse,
|
||||
CaseMaterial,
|
||||
WorkspaceFilesResponse,
|
||||
} from "../api/types.js";
|
||||
import type {
|
||||
@@ -235,7 +236,13 @@ export class BenchmarkService {
|
||||
.sort((a, b) => a.name.localeCompare(b.name))) {
|
||||
const fallback: BenchmarkCaseSummary = { id: entry.name, title: entry.name };
|
||||
try {
|
||||
const statementDir = await this.statementRoot(projectId, agentId, benchmarkId, entry.name);
|
||||
const statementDir = await this.caseMaterialRoot(
|
||||
projectId,
|
||||
agentId,
|
||||
benchmarkId,
|
||||
entry.name,
|
||||
"statement",
|
||||
);
|
||||
const realReadme = await fs.realpath(path.join(statementDir, "README.md"));
|
||||
if (!isWithin(statementDir, realReadme)) throw new Error("README escapes Statement");
|
||||
cases.push({
|
||||
@@ -255,9 +262,16 @@ export class BenchmarkService {
|
||||
benchmarkId: string,
|
||||
caseId: string,
|
||||
rel: string,
|
||||
material: CaseMaterial,
|
||||
): Promise<WorkspaceFilesResponse> {
|
||||
const statementDir = await this.statementRoot(projectId, agentId, benchmarkId, caseId);
|
||||
return this.workspaceFiles.list(statementDir, rel);
|
||||
const materialRoot = await this.caseMaterialRoot(
|
||||
projectId,
|
||||
agentId,
|
||||
benchmarkId,
|
||||
caseId,
|
||||
material,
|
||||
);
|
||||
return this.workspaceFiles.list(materialRoot, rel);
|
||||
}
|
||||
|
||||
async readCaseFile(
|
||||
@@ -266,37 +280,45 @@ export class BenchmarkService {
|
||||
benchmarkId: string,
|
||||
caseId: string,
|
||||
rel: string,
|
||||
material: CaseMaterial,
|
||||
options?: WorkspaceFileReadOptions,
|
||||
): Promise<WorkspaceFileContent> {
|
||||
const statementDir = await this.statementRoot(projectId, agentId, benchmarkId, caseId);
|
||||
return this.workspaceFiles.read(statementDir, rel, options);
|
||||
const materialRoot = await this.caseMaterialRoot(
|
||||
projectId,
|
||||
agentId,
|
||||
benchmarkId,
|
||||
caseId,
|
||||
material,
|
||||
);
|
||||
return this.workspaceFiles.read(materialRoot, rel, options);
|
||||
}
|
||||
|
||||
private async statementRoot(
|
||||
private async caseMaterialRoot(
|
||||
projectId: string,
|
||||
agentId: string,
|
||||
benchmarkId: string,
|
||||
caseId: string,
|
||||
material: CaseMaterial,
|
||||
): Promise<string> {
|
||||
const benchDir = path.join(benchmarksDir(this.root, projectId, agentId), benchmarkId);
|
||||
const caseDir = path.join(benchDir, caseId);
|
||||
const statementDir = path.join(caseDir, "statement");
|
||||
const materialRoot = path.join(caseDir, material);
|
||||
try {
|
||||
const [realBenchDir, realCaseDir, realStatementDir] = await Promise.all([
|
||||
const [realBenchDir, realCaseDir, realMaterialRoot] = await Promise.all([
|
||||
fs.realpath(benchDir),
|
||||
fs.realpath(caseDir),
|
||||
fs.realpath(statementDir),
|
||||
fs.realpath(materialRoot),
|
||||
]);
|
||||
if (
|
||||
!isWithin(realBenchDir, realCaseDir) ||
|
||||
path.dirname(realStatementDir) !== realCaseDir ||
|
||||
path.basename(realStatementDir) !== "statement"
|
||||
path.dirname(realMaterialRoot) !== realCaseDir ||
|
||||
path.basename(realMaterialRoot) !== material
|
||||
) {
|
||||
throw new Error("Statement path is not canonical");
|
||||
throw new Error("Case material is not canonical");
|
||||
}
|
||||
return realStatementDir;
|
||||
return realMaterialRoot;
|
||||
} catch {
|
||||
throw new HttpError(404, "not_found", "Public Case Statement does not exist.");
|
||||
throw new HttpError(404, "not_found", `Case ${material} does not exist.`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user