Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 0 additions & 123 deletions src/core/batchInsights.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
ListBatchEvaluationsCommand,
type BatchEvaluationSummary,
} from "@aws-sdk/client-bedrock-agentcore";
import { ResultTruncationError } from "../errors";
import type { AwsClients } from "./types";
import { EvalClient } from "./eval";

Expand Down Expand Up @@ -72,16 +71,6 @@ describe("EvalClient.getBatchInsights", () => {
});

describe("EvalClient.listBatchInsights", () => {
test("rejects an invalid logical page size before calling the service", async () => {
const send = mock(async () => ({ batchEvaluations: [] }));
const client = evalClient(send);

await expect(client.listBatchInsights(undefined, 0, options)).rejects.toThrow(
"maxResults must be a positive integer",
);
expect(send).not.toHaveBeenCalled();
});

test("filters the final Batch Evaluation page", async () => {
const insightsJob = insight("insights-1");
const client = evalClient(async (command) => {
Expand All @@ -95,116 +84,4 @@ describe("EvalClient.listBatchInsights", () => {
nextToken: undefined,
});
});

test("scans sparse service pages to fill one logical Insights page", async () => {
const insights = [insight("insights-1"), insight("insights-2")];
const requests: unknown[] = [];
const client = evalClient(async (command) => {
requests.push(command.input);
switch (command.input.nextToken) {
case undefined:
return {
batchEvaluations: [evaluation("evaluation-1")],
nextToken: "page-2",
};
case "page-2":
return {
batchEvaluations: [insights[0], evaluation("evaluation-2")],
nextToken: "page-3",
};
default:
return {
batchEvaluations: [evaluation("evaluation-3"), insights[1]],
};
}
});

await expect(client.listBatchInsights(undefined, 2, options)).resolves.toEqual({
batchEvaluations: insights,
nextToken: undefined,
});
expect(requests).toEqual([
{ nextToken: undefined, maxResults: undefined },
{ nextToken: "page-2", maxResults: undefined },
{ nextToken: "page-3", maxResults: undefined },
]);
});

test("replays the exact Batch Evaluation prefix without skipping the next Insight", async () => {
const firstInsight = insight("insights-1");
const secondInsight = insight("insights-2");
const requests: unknown[] = [];
const client = evalClient(async (command) => {
requests.push(command.input);

if (command.input.nextToken === "after-insights-1") {
return {
batchEvaluations: [evaluation("evaluation-2"), secondInsight],
};
}
if (command.input.maxResults === 2) {
return {
batchEvaluations: [evaluation("evaluation-1"), firstInsight],
nextToken: "after-insights-1",
};
}
return {
batchEvaluations: [
evaluation("evaluation-1"),
firstInsight,
evaluation("evaluation-2"),
secondInsight,
],
};
});

const first = await client.listBatchInsights(undefined, 1, options);
const second = await client.listBatchInsights(first.nextToken, 1, options);

expect(first).toEqual({
batchEvaluations: [firstInsight],
nextToken: "after-insights-1",
});
expect(second).toEqual({
batchEvaluations: [secondInsight],
nextToken: undefined,
});
expect(requests).toEqual([
{ nextToken: undefined, maxResults: undefined },
{ nextToken: undefined, maxResults: 2 },
{ nextToken: "after-insights-1", maxResults: undefined },
]);
});

test("returns a token only when it leads to another Insights job", async () => {
const firstInsight = insight("insights-1");
const secondInsight = insight("insights-2");
const client = evalClient(async (command) => {
if (command.input.nextToken === "page-2") {
return { batchEvaluations: [evaluation("evaluation-2"), secondInsight] };
}
return {
batchEvaluations: [firstInsight, evaluation("evaluation-1")],
nextToken: "page-2",
};
});

await expect(client.listBatchInsights(undefined, 1, options)).resolves.toEqual({
batchEvaluations: [firstInsight],
nextToken: "page-2",
});
});

test("throws when Insights discovery exceeds the Batch Evaluation scan cap", async () => {
let calls = 0;
const client = evalClient(async () => {
calls += 1;
return { batchEvaluations: [], nextToken: `page-${calls}` };
});

await expect(client.listBatchInsights(undefined, 1, options)).rejects.toThrow(
ResultTruncationError,
);
expect(calls).toBe(101);
});
});
104 changes: 25 additions & 79 deletions src/core/eval.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ import {
type EvaluationReferenceInput,
type EvaluationResultContent,
type EvaluationTarget,
type BatchEvaluationSummary,
type GetABTestResponse,
type ListABTestsResponse,
type ABTestExecutionStatus,
Expand Down Expand Up @@ -102,7 +101,6 @@ import {
InputValidationError,
NetworkingError,
ResourceNotFoundError,
ResultTruncationError,
} from "../errors";
import type {
BatchEvaluationDetail,
Expand Down Expand Up @@ -147,6 +145,7 @@ import { applyExampleIds, diffExamples, indexRemoteById, parseJsonl } from "./da
import type { Addition } from "./datasetDiff";
import type { AwsClients, CoreFetch, CoreOptions } from "./types";
import type { Logger } from "../logging";
import { FilteredPaginator } from "./filteredPaginator";
import { toClientConfig } from "./utils";
import {
accountIdFromRoleArn,
Expand Down Expand Up @@ -184,7 +183,6 @@ const EVALUATE_TARGET_BATCH = 10;
const INSIGHTS_MAX_ROWS = 100_000;

const DEFAULT_BATCH_INSIGHTS_PAGE_SIZE = 50;
const MAX_BATCH_INSIGHTS_SCAN_REQUESTS = 101;

// noopLogger is the default for the optional logger arg so callers that don't
// need batch-evaluation result-log diagnostics (e.g. dataset-only tests) can
Expand All @@ -198,8 +196,6 @@ const noopLogger: Logger = {
};

const DEFAULT_ONLINE_INSIGHT_PAGE_SIZE = 100;
const MAX_ONLINE_INSIGHT_PAGES = 101;
type InsightSummary = NonNullable<ListOnlineInsightsResponse["onlineEvaluationConfigs"]>[number];

export class EvalClient implements CoreEvalClient {
constructor(
Expand Down Expand Up @@ -440,49 +436,18 @@ export class EvalClient implements CoreEvalClient {
maxResults: number | undefined,
options: CoreOptions,
): Promise<ListBatchEvaluationsResponse> {
const insightsPageSize = maxResults ?? DEFAULT_BATCH_INSIGHTS_PAGE_SIZE;
if (!Number.isInteger(insightsPageSize) || insightsPageSize < 1) {
throw new InputValidationError("maxResults must be a positive integer");
}
const batchEvaluations: BatchEvaluationSummary[] = [];
let batchEvaluationToken = nextToken;

for (let request = 0; request < MAX_BATCH_INSIGHTS_SCAN_REQUESTS; request++) {
const requestToken = batchEvaluationToken;
const response = await this.listBatchEvaluations(requestToken, undefined, options);
const serviceItems = response.batchEvaluations ?? [];
const insights = serviceItems.filter(EvalClient.isBatchInsights);

if (batchEvaluations.length < insightsPageSize) {
const remaining = insightsPageSize - batchEvaluations.length;
if (insights.length > remaining) {
const boundaryInsight = insights[remaining - 1]!;
const boundarySize = serviceItems.indexOf(boundaryInsight) + 1;
// Re-read through the last returned Insights job so the service token
// cannot skip later matches from this Batch Evaluation page.
const boundaryResponse = await this.listBatchEvaluations(
requestToken,
boundarySize,
options,
);

batchEvaluations.push(...insights.slice(0, remaining));
return { ...boundaryResponse, batchEvaluations };
}
batchEvaluations.push(...insights);
} else if (insights.length > 0) {
return { ...response, batchEvaluations, nextToken: requestToken };
}

if (response.nextToken === undefined) {
return { ...response, batchEvaluations, nextToken: undefined };
}
batchEvaluationToken = response.nextToken;
}

throw new ResultTruncationError(
`Batch Insights discovery exceeded ${MAX_BATCH_INSIGHTS_SCAN_REQUESTS} Batch Evaluation scan requests; results are incomplete`,
);
const page = await FilteredPaginator.paginate({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like the design of this

fetchPage: async (token, size) => {
const r = await this.listBatchEvaluations(token, size, options);
return { items: r.batchEvaluations ?? [], nextToken: r.nextToken };
},
predicate: EvalClient.isBatchInsights,
nextToken,
maxResults,
defaultPageSize: DEFAULT_BATCH_INSIGHTS_PAGE_SIZE,
resourceLabel: "Batch Insights",
});
return { batchEvaluations: page.items, nextToken: page.nextToken };
}

async startBatchEvaluation(
Expand Down Expand Up @@ -924,37 +889,18 @@ export class EvalClient implements CoreEvalClient {
maxResults: number | undefined,
options: CoreOptions,
): Promise<ListOnlineInsightsResponse> {
// Fill the requested page across underlying pages, since the shared List API
// returns eval configs too (mirrors listGatewayConnectors over Targets).
const pageSize = maxResults ?? DEFAULT_ONLINE_INSIGHT_PAGE_SIZE;
const items: InsightSummary[] = [];
let token = nextToken;
let filling = true;

for (let page = 0; page < MAX_ONLINE_INSIGHT_PAGES; page++) {
const requestToken = token;
const requestSize = filling ? pageSize - items.length : DEFAULT_ONLINE_INSIGHT_PAGE_SIZE;
const response = await this.listOnlineEvaluationConfigs(token, requestSize, options);
const insights = (response.onlineEvaluationConfigs ?? []).filter(
(c) => (c.insights?.length ?? 0) > 0,
);

if (filling) {
items.push(...insights);
filling = items.length < pageSize;
} else if (insights.length > 0) {
return { ...response, onlineEvaluationConfigs: items, nextToken: requestToken };
}

if (response.nextToken === undefined) {
return { ...response, onlineEvaluationConfigs: items, nextToken: undefined };
}
token = response.nextToken;
}

throw new ResultTruncationError(
`Online insight discovery exceeded ${MAX_ONLINE_INSIGHT_PAGES} config pages; results are incomplete`,
);
const page = await FilteredPaginator.paginate({
fetchPage: async (token, size) => {
const r = await this.listOnlineEvaluationConfigs(token, size, options);
return { items: r.onlineEvaluationConfigs ?? [], nextToken: r.nextToken };
},
predicate: (c) => (c.insights?.length ?? 0) > 0,
nextToken,
maxResults,
defaultPageSize: DEFAULT_ONLINE_INSIGHT_PAGE_SIZE,
resourceLabel: "Online insight",
});
return { onlineEvaluationConfigs: page.items, nextToken: page.nextToken };
}

async setOnlineInsightExecutionStatus(
Expand Down
Loading
Loading