mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-11 03:38:38 +00:00
feat(lens): page job reviews by index cursor
Adds api.reviews for GET /lens/{id}/runs/{job}/reviews?after=N, with a demo implementation. appendPage adds pages in arrival order and keeps the latest 200. liveJob now keys off reviewed, since the list no longer carries reviews.
This commit is contained in:
parent
73ad24f7de
commit
db0e4df4f5
4 changed files with 59 additions and 3 deletions
|
|
@ -2,7 +2,7 @@ import { z } from "zod";
|
|||
import type { RunWindow } from "../investigations/detail/RunNowDialog";
|
||||
import type { ApiClient } from "@/lib/http/client";
|
||||
import type { components } from "@/lib/http/schema";
|
||||
import type { ActivitySelection, Job, Lens, LensList, Sample, Settings, WorkerCreated } from "../model/types";
|
||||
import type { ActivitySelection, Job, Lens, LensList, Review, Sample, Settings, WorkerCreated } from "../model/types";
|
||||
import type { AnalysisModelInfo } from "../setup/fields/analysisModels";
|
||||
|
||||
export type ExecutionContent = components["schemas"]["ExecutionContent"];
|
||||
|
|
@ -25,6 +25,11 @@ export type Key = z.infer<typeof keySchema>;
|
|||
export type KeyPage = z.infer<typeof keyPageSchema>;
|
||||
export type KeyInfo = z.infer<typeof keyInfoSchema>["info"];
|
||||
|
||||
export interface ReviewPage {
|
||||
readonly reviews: readonly Review[];
|
||||
readonly reviewed: number;
|
||||
}
|
||||
|
||||
export interface AnalysisKeyRequest {
|
||||
readonly model: string;
|
||||
readonly budget: number;
|
||||
|
|
@ -37,6 +42,7 @@ export interface LensApi {
|
|||
activity(): Promise<{ traces: boolean; requests: boolean }>;
|
||||
runs(lensId: string, offset: number): Promise<Job[]>;
|
||||
run(lensId: string, jobId: string): Promise<Job>;
|
||||
reviews(lensId: string, jobId: string, after: number): Promise<ReviewPage>;
|
||||
execution(lensId: string, executionId: string, offset: number): Promise<ExecutionContent>;
|
||||
sample(selection: ActivitySelection, offset: number, asOf: string): Promise<Sample>;
|
||||
agents(): Promise<string[]>;
|
||||
|
|
@ -64,6 +70,8 @@ export function liveLensApi(apiClient: ApiClient, accessToken: string): LensApi
|
|||
activity: () => apiClient.get("/lens/activity/available", { accessToken }),
|
||||
runs: (lensId, offset) => apiClient.get<Job[]>(`/lens/${lensId}/runs`, { accessToken, query: { offset } }),
|
||||
run: (lensId, jobId) => apiClient.get<Job>(`/lens/${lensId}/runs/${jobId}`, { accessToken }),
|
||||
reviews: (lensId, jobId, after) =>
|
||||
apiClient.get<ReviewPage>(`/lens/${lensId}/runs/${jobId}/reviews`, { accessToken, query: { after } }),
|
||||
execution: (lensId, executionId, offset) =>
|
||||
apiClient.get<ExecutionContent>(`/lens/${lensId}/executions/${encode(executionId)}`, {
|
||||
accessToken,
|
||||
|
|
|
|||
|
|
@ -354,6 +354,10 @@ function demoLensApi(data: LensDemoData): LensApi {
|
|||
activity: async () => ({ traces: true, requests: false }),
|
||||
runs: (lensId, offset) => found(jobs(lensId)?.slice(offset)),
|
||||
run: (lensId, jobId) => found(jobs(lensId)?.find((job) => job.id === jobId)),
|
||||
reviews: async (lensId, jobId, after) => {
|
||||
const job = await found(jobs(lensId)?.find((item) => item.id === jobId));
|
||||
return { reviews: job.reviews.slice(Math.max(0, after - (job.reviewed - job.reviews.length))), reviewed: job.reviewed };
|
||||
},
|
||||
execution: notInDemo,
|
||||
sample: notInDemo,
|
||||
agents: notInDemo,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import {
|
|||
inGroup,
|
||||
share,
|
||||
issueCount,
|
||||
appendPage,
|
||||
EMPTY_FEED,
|
||||
shortVerdict,
|
||||
stripState,
|
||||
tickerLine,
|
||||
|
|
@ -262,7 +264,8 @@ describe("playback queue", () => {
|
|||
});
|
||||
|
||||
describe("which job the live run shows", () => {
|
||||
const job = (id: string, status: Job["status"], reviews: Review[]) => ({ id, status, reviews }) as unknown as Job;
|
||||
const job = (id: string, status: Job["status"], reviews: Review[]) =>
|
||||
({ id, status, reviews: [], reviewed: reviews.length }) as unknown as Job;
|
||||
|
||||
it("shows the active job once it has reviews", () => {
|
||||
expect(liveJob([job("new", "running", [review("a")]), job("old", "completed", [review("b")])])?.id).toBe("new");
|
||||
|
|
@ -359,6 +362,32 @@ describe("drawer focus", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("incremental reviews", () => {
|
||||
const at = (id: string, minute: number) => review(id, { at: `2026-10-03T16:${String(minute).padStart(2, "0")}:00Z` });
|
||||
|
||||
it("appends pages in arrival order, not by time, and advances the cursor to the reviewed count", () => {
|
||||
const first = appendPage(EMPTY_FEED, { reviews: [at("a", 5), at("b", 2)], reviewed: 2 });
|
||||
expect(first).toEqual({ reviews: [at("a", 5), at("b", 2)], cursor: 2 });
|
||||
const second = appendPage(first, { reviews: [at("c", 1)], reviewed: 3 });
|
||||
expect(second.reviews.map((r) => r.execution_id)).toEqual(["a", "b", "c"]);
|
||||
expect(second.cursor).toBe(3);
|
||||
});
|
||||
|
||||
it("keeps the same feed for an empty page and never duplicates a re-sent review", () => {
|
||||
const feed = appendPage(EMPTY_FEED, { reviews: [at("a", 1)], reviewed: 1 });
|
||||
expect(appendPage(feed, { reviews: [], reviewed: 1 })).toBe(feed);
|
||||
expect(appendPage(feed, { reviews: [at("a", 1)], reviewed: 1 }).reviews).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("caps how many reviews are kept, dropping the oldest", () => {
|
||||
const many = Array.from({ length: 250 }, (_, n) => review(`r${n}`));
|
||||
const feed = appendPage(EMPTY_FEED, { reviews: many, reviewed: 250 });
|
||||
expect(feed.reviews).toHaveLength(200);
|
||||
expect(feed.reviews[0].execution_id).toBe("r50");
|
||||
expect(feed.cursor).toBe(250);
|
||||
});
|
||||
});
|
||||
|
||||
describe("time left reading", () => {
|
||||
const started = {
|
||||
created_at: "2026-10-03T16:00:00Z",
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export function liveJob(jobs: readonly Job[]): Job | undefined {
|
|||
const active = jobs.find((job) => job.status === "queued" || job.status === "running");
|
||||
if (active) return active;
|
||||
const latest = jobs[0];
|
||||
return latest?.reviews.length ? latest : undefined;
|
||||
return latest && latest.reviewed > 0 ? latest : undefined;
|
||||
}
|
||||
|
||||
export function reviewKey(review: Pick<Review, "execution_id" | "at">): string {
|
||||
|
|
@ -116,6 +116,21 @@ export function verdictLine(review: Pick<Review, "cannot_assess" | "verdicts">):
|
|||
return review.verdicts[0]?.summary ?? "No issue observed";
|
||||
}
|
||||
|
||||
const KEPT_REVIEWS = 200;
|
||||
|
||||
export interface ReviewFeed {
|
||||
reviews: readonly Review[];
|
||||
cursor: number;
|
||||
}
|
||||
|
||||
export const EMPTY_FEED: ReviewFeed = { reviews: [], cursor: 0 };
|
||||
|
||||
export function appendPage(feed: ReviewFeed, page: { reviews: readonly Review[]; reviewed: number }): ReviewFeed {
|
||||
if (page.reviewed === feed.cursor && !page.reviews.length) return feed;
|
||||
const added = unseen(page.reviews, new Set(feed.reviews.map(reviewKey)));
|
||||
return { reviews: [...feed.reviews, ...added].slice(-KEPT_REVIEWS), cursor: page.reviewed };
|
||||
}
|
||||
|
||||
export function unseen(reviews: readonly Review[], seen: ReadonlySet<string>): Review[] {
|
||||
return reviews.filter((review) => !seen.has(reviewKey(review)));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue