Skip to content
Open
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
58 changes: 58 additions & 0 deletions src/core/batchInsights.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { describe, expect, mock, test } from "bun:test";
import { StartBatchEvaluationCommand } from "@aws-sdk/client-bedrock-agentcore";
import { EvalClient } from "./eval";
import type { AwsClients } from "./types";

describe("EvalClient.startBatchInsights", () => {

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.

We don't need this unit test. This should be captured by handler unit tests

test("maps the semantic input to StartBatchEvaluation insights", async () => {
const send = mock(async (_command: unknown) => ({
batchEvaluationId: "bi-1",
status: "RUNNING",
}));
const clients = {
data: () => ({ send }),
control: () => {
throw new Error("unexpected control client call");
},
iam: () => {
throw new Error("unexpected IAM client call");
},
logs: () => {
throw new Error("unexpected Logs client call");
},
} as unknown as AwsClients;
const client = new EvalClient(clients);
const dataSourceConfig = {
onlineEvaluationConfigSource: {
onlineEvaluationConfigArn: "arn:aws:bedrock-agentcore:us-west-2:123:online-evaluation/oe-1",
},
};

await client.startBatchInsights(
{
name: "insights_run",
description: "description",
insightIds: ["Builtin.Insight.FailureAnalysis", "Builtin.Insight.UserIntent"],
evaluatorIds: ["Builtin.Helpfulness"],
source: { origin: "raw", dataSourceConfig },
kmsKeyArn: "arn:aws:kms:us-west-2:123:key/abc",
},
{ region: "us-west-2" },
);

expect(send).toHaveBeenCalledTimes(1);
const command = send.mock.calls[0]?.[0];
expect(command).toBeInstanceOf(StartBatchEvaluationCommand);
expect((command as StartBatchEvaluationCommand).input).toEqual({
batchEvaluationName: "insights_run",
description: "description",
insights: [
{ insightId: "Builtin.Insight.FailureAnalysis" },
{ insightId: "Builtin.Insight.UserIntent" },
],
evaluators: [{ evaluatorId: "Builtin.Helpfulness" }],
dataSourceConfig,
kmsKeyArn: "arn:aws:kms:us-west-2:123:key/abc",
});
});
});
18 changes: 18 additions & 0 deletions src/core/eval.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ import type {
SessionSourceValue,
SessionTrace,
SpanRecord,
StartBatchInsightsInput,
StartBatchEvaluationInput,
UpdateConfigurationBundleInput,
UpdateOnlineEvalInput,
Expand Down Expand Up @@ -379,6 +380,23 @@ export class EvalClient implements CoreEvalClient {
);
}

async startBatchInsights(
input: StartBatchInsightsInput,
options: CoreOptions,
): Promise<StartBatchEvaluationResponse> {
const dataSourceConfig = await this.dataSourceConfigForSource(input.source, options);
return this.clients.data(toClientConfig(options)).send(
new StartBatchEvaluationCommand({
batchEvaluationName: input.name,
description: input.description,
insights: input.insightIds.map((insightId) => ({ insightId })),
evaluators: input.evaluatorIds?.map((evaluatorId) => ({ evaluatorId })),
dataSourceConfig,
kmsKeyArn: input.kmsKeyArn,
}),
);
}

// dataSourceConfigForSource maps a resolved SessionSourceValue to the data-plane
// dataSourceConfig union. The agent arm reuses the same runtime resolution +
// log-group derivation the control-plane agentDataSource uses, then attaches the
Expand Down
132 changes: 5 additions & 127 deletions src/handlers/eval/batch-evaluation/evaluate/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,50 +4,16 @@ import { InputValidationError } from "../../../../errors";
import { JsonRendererKey } from "../../../../tui";
import { SourceResolver, type AppIO } from "../../../../io";
import type { Core } from "../../../types";
import type { SessionMetadataShape, DataSourceConfig } from "@aws-sdk/client-bedrock-agentcore";
import type { SessionMetadataShape } from "@aws-sdk/client-bedrock-agentcore";
import { coreOptsFromCtx, parseJsonFlag } from "../../../utils";
import type { SessionSourceValue, SessionWindow } from "../../types";
import { resolveSessionSource, sessionSourceFlags } from "../../sessionSource";

export const createEvaluateBatchEvaluationHandler = (core: Core, io: AppIO) =>
createHandler({
name: "evaluate",
description: "evaluate existing sessions service-side (async; returns a job id)",
flags: [
flag(
"agent",
"source: harness id or runtime id whose sessions to evaluate",
z.string().optional(),
),
flag(
"endpoint",
"runtime endpoint qualifier (default DEFAULT; only with --agent)",
z.string().optional(),
),
flag(
"online-eval",

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.

Can you use online-eval as a data source for batch insight?

"source: evaluate sessions an online-eval config already sampled",
z.string().optional(),
),
flag(
"data-source-config",
"source: raw DataSourceConfig JSON (inline, file://<path>, or -); escape hatch",
z.string().optional(),
),
flag(
"start-time",
"time filter: window start (ISO-8601, with --end-time)",
z.string().optional(),
),
flag(
"end-time",
"time filter: window end (ISO-8601, with --start-time)",
z.string().optional(),
),
flag(
"session-ids",
"filter: specific session ids (only with --agent)",
z.array(z.string()).optional(),
),
...sessionSourceFlags,
flag("evaluator", "evaluator id(s) to apply", z.array(z.string()).optional()),
flag(
"ground-truth",
Expand All @@ -68,13 +34,9 @@ export const createEvaluateBatchEvaluationHandler = (core: Core, io: AppIO) =>
);
}

const resolver = new SourceResolver({ stdin: io.stdin });
const rawDataSourceConfig = parseJsonFlag<DataSourceConfig>(
"data-source-config",
await resolver.resolveText("data-source-config", flags["data-source-config"]),
);
const source = resolveDataSource(flags, rawDataSourceConfig);
const source = await resolveSessionSource(flags, io);

const resolver = new SourceResolver({ stdin: io.stdin });
const groundTruth = parseJsonFlag<SessionMetadataShape[]>(
"ground-truth",
await resolver.resolveText("ground-truth", flags["ground-truth"]),
Expand All @@ -94,87 +56,3 @@ export const createEvaluateBatchEvaluationHandler = (core: Core, io: AppIO) =>
ctx.require(JsonRendererKey).renderJson(response);
},
});

// DataSourceFlags is hand-listed and can drift from the flag declarations above.
// When insights lands as a second consumer, promote this type + resolveDataSource
// into a static SessionSource class instead of keeping them as loose siblings.
type DataSourceFlags = {
agent?: string;
endpoint?: string;
"online-eval"?: string;
"start-time"?: string;
"end-time"?: string;
"session-ids"?: string[];
};

function resolveDataSource(
flags: DataSourceFlags,
rawDataSourceConfig: DataSourceConfig | undefined,
): SessionSourceValue {
const hasAgent = flags["agent"] !== undefined;
const hasOnlineEval = flags["online-eval"] !== undefined;
const hasRaw = rawDataSourceConfig !== undefined;

const armCount = [hasAgent, hasOnlineEval, hasRaw].filter(Boolean).length;
if (armCount !== 1) {
throw new InputValidationError(
"specify exactly one source: '--agent', '--online-eval', or '--data-source-config'",
);
}

const hasIds = !!flags["session-ids"]?.length;

if (hasRaw) {
// The raw config is self-contained; the ergonomic filter flags don't apply.
if (
flags["start-time"] !== undefined ||
flags["end-time"] !== undefined ||
hasIds ||
flags["endpoint"] !== undefined
) {
throw new InputValidationError(
"filter flags cannot be combined with '--data-source-config' (put them in the JSON)",
);
}
return { origin: "raw", dataSourceConfig: rawDataSourceConfig! };
}

const window = resolveWindow(flags);

if (hasOnlineEval) {
// The online-eval arm has no sessionIds filter and no endpoint.
if (hasIds)
throw new InputValidationError("'--session-ids' cannot be used with '--online-eval'");
if (flags["endpoint"])
throw new InputValidationError("'--endpoint' can only be used with '--agent'");
return { origin: "online-eval", onlineEvaluationConfigId: flags["online-eval"]!, window };
}

return {
origin: "agent",
agent: flags["agent"]!,
endpoint: flags["endpoint"],
window,
sessionIds: hasIds ? flags["session-ids"] : undefined,
};
}

// resolveWindow validates the explicit time window: both halves must come
// together and start must precede end.
function resolveWindow(flags: DataSourceFlags): SessionWindow | undefined {
const hasStart = flags["start-time"] !== undefined;
const hasEnd = flags["end-time"] !== undefined;
if (!hasStart && !hasEnd) return undefined; // no time filter — all available sessions
if (!hasStart || !hasEnd) {
throw new InputValidationError("--start-time and --end-time must be provided together");
}
const startTime = new Date(flags["start-time"]!);
const endTime = new Date(flags["end-time"]!);
if (Number.isNaN(+startTime) || Number.isNaN(+endTime)) {
throw new InputValidationError("--start-time and --end-time must be ISO-8601 timestamps");
}
if (+startTime >= +endTime) {
throw new InputValidationError("--start-time must be before --end-time");
}
return { startTime, endTime };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"agentRuntimeArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:runtime/asdf_MyAgent-3s5axvBC6Q",
"agentRuntimeName": "asdf_MyAgent",
"agentRuntimeId": "asdf_MyAgent-3s5axvBC6Q",
"agentRuntimeVersion": "1",
"createdAt": {
"$date": "2026-04-23T21:17:21.895Z"
},
"lastUpdatedAt": {
"$date": "2026-04-23T21:17:35.159Z"
},
"roleArn": "arn:aws:iam::685197708687:role/AgentCore-asdf-default-ApplicationAgentMyAgentRunti-KdyUbgImzDRK",
"networkConfiguration": {
"networkMode": "PUBLIC"
},
"status": "READY",
"lifecycleConfiguration": {
"idleRuntimeSessionTimeout": 900,
"maxLifetime": 28800
},
"description": "AgentCore Runtime: asdf_MyAgent",
"workloadIdentityDetails": {
"workloadIdentityArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:workload-identity-directory/default/workload-identity/asdf_MyAgent-3s5axvBC6Q"
},
"agentRuntimeArtifact": {
"codeConfiguration": {
"code": {
"s3": {
"bucket": "cdk-hnb659fds-assets-685197708687-us-west-2",
"prefix": "a07977786dda1e2e5be304cb7485237a19ed24d5e05b02e73ca91a43fd2e7280.zip"
}
},
"runtime": "PYTHON_3_13",
"entryPoint": [
"opentelemetry-instrument",
"main.py"
]
}
},
"environmentVariables": {
"AGENTCORE_GATEWAY_BUGBASHGW1776978672_AUTH_TYPE": "NONE",
"AGENTCORE_GATEWAY_BUGBASHGW1776978672_URL": "https://bugbashgw1776978672-zsy8cbqwts.gateway.bedrock-agentcore.us-west-2.amazonaws.com/mcp"
},
"metadataConfiguration": {
"requireMMDSV2": true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"batchEvaluationId": "golden_batch_insights_fixture-cd634815b4",
"batchEvaluationArn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:batch-evaluate/golden_batch_insights_fixture-cd634815b4",
"batchEvaluationName": "golden_batch_insights_fixture",
"status": "COMPLETED",
"createdAt": {
"$date": "2026-08-21T19:27:51.136Z"
},
"insights": [
{
"insightId": "Builtin.Insight.FailureAnalysis"
}
],
"dataSourceConfig": {
"cloudWatchLogs": {
"serviceNames": [
"asdf_MyAgent.DEFAULT"
],
"logGroupNames": [
"/aws/bedrock-agentcore/runtimes/asdf_MyAgent-3s5axvBC6Q-DEFAULT"
]
}
},
"evaluationResults": {
"numberOfSessionsCompleted": 2,
"numberOfSessionsInProgress": 0,
"numberOfSessionsFailed": 0,
"totalNumberOfSessions": 2,
"numberOfSessionsIgnored": 0,
"evaluatorSummaries": []
},
"description": "Golden batch insights fixture",
"updatedAt": {
"$date": "2026-08-21T19:28:56.756Z"
}
}
Loading
Loading