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
48 changes: 34 additions & 14 deletions src/components/OnlineEvalPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,30 @@ import { useNavigate } from "react-router";
import type { ScreenProps } from "../handlers/types";
import { coreOptsFromCtx } from "../handlers/utils";
import { formatTimestamp } from "./formatTimestamp";
import { PaginatedTablePicker } from "./PaginatedTablePicker";
import { PaginatedTablePicker, type TokenPage } from "./PaginatedTablePicker";
import type { DataTableColumn } from "./ui/data-table";

// OnlineEvalRow is the flat, display-ready shape the table renders. It also
// satisfies DataTable's `T extends Record<string, unknown>` constraint, which the
// SDK's OnlineEvaluationConfigSummary interface does not. The list API returns
// only summary fields (name/status/executionStatus/timestamps); richer detail
// like sampling rate and evaluators comes from GetOnlineEvaluationConfig.
// only summary fields (name/status/executionStatus/timestamps/insights); richer
// detail like sampling rate and evaluators comes from GetOnlineEvaluationConfig.
interface OnlineEvalRow extends Record<string, unknown> {
configId: string;
configName: string;
status: string;
executionStatus: string;
hasInsights: boolean;
updatedAt: string;
}

export const onlineEvalColumns = [
{ key: "configName", header: "name", flex: true },
{ key: "status", header: "status", width: 12 },
{ key: "executionStatus", header: "execution", width: 11 },
// #2029: surface whether the config has insights enabled straight from the
// list summary — no per-row GetOnlineEvaluationConfig call needed.
{ key: "hasInsights", header: "insights", width: 9, render: (value) => (value ? "yes" : "-") },
{
key: "updatedAt",
header: "updated UTC",
Expand All @@ -38,6 +42,7 @@ function toRow(config: OnlineEvaluationConfigSummary): OnlineEvalRow {
configName: config.onlineEvaluationConfigName ?? id,
status: config.status ?? "-",
executionStatus: config.executionStatus ?? "-",
hasInsights: (config.insights?.length ?? 0) > 0,
updatedAt: config.updatedAt?.toISOString() ?? "-",
};
}
Expand All @@ -47,6 +52,15 @@ export interface OnlineEvalPickerProps extends ScreenProps {
description?: string;
onSelect: (configId: string) => void;
onEscape?: () => void;
// The online-insight list reuses this picker (same columns, incl. the #2029
// insights column) but reads from a different Core method and namespace, so
// the data source and the noun in the status messages are overridable.
resourceLabel?: string;
queryKey?: readonly unknown[];
loadPage?: (
token: string | undefined,
pageSize: number,
) => Promise<TokenPage<OnlineEvaluationConfigSummary>>;
}

/**
Expand All @@ -62,6 +76,9 @@ export function OnlineEvalPicker({
description,
onSelect,
onEscape,
resourceLabel = "online evaluation configs",
queryKey,
loadPage,
}: OnlineEvalPickerProps) {
const opts = coreOptsFromCtx(ctx);
const navigate = useNavigate();
Expand All @@ -71,23 +88,26 @@ export function OnlineEvalPicker({
<PaginatedTablePicker
breadcrumb={breadcrumb}
description={description}
queryKey={["online-evals", opts.region]}
loadPage={async (token, pageSize) => {
const response = await core.eval.listOnlineEvaluationConfigs(token, pageSize, opts);
return {
items: response.onlineEvaluationConfigs ?? [],
nextToken: response.nextToken,
};
}}
queryKey={queryKey ?? ["online-evals", opts.region]}
loadPage={
loadPage ??
(async (token, pageSize) => {
const response = await core.eval.listOnlineEvaluationConfigs(token, pageSize, opts);
return {
items: response.onlineEvaluationConfigs ?? [],
nextToken: response.nextToken,
};
})
}
toRow={toRow}
columns={onlineEvalColumns}
getValue={(row) => row.configId}
onSelect={onSelect}
onBack={goBack}
loadingMessage="Loading online evaluation configs…"
loadingMessage={`Loading ${resourceLabel}…`}
errorMessage={(error) => `Error: ${error.message}`}
emptyMessage="No online evaluation configs found in this Region."
emptyPageMessage="No online evaluation configs on this page."
emptyMessage={`No ${resourceLabel} found in this Region.`}
emptyPageMessage={`No ${resourceLabel} on this page.`}
/>
);
}
26 changes: 26 additions & 0 deletions src/components/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ import {
OnlineEvalGetScreen,
OnlineEvalGetJsonScreen,
} from "../handlers/eval/online-eval/get/screen.tsx";
import { OnlineInsightScreen } from "../handlers/eval/online-insight/screen.tsx";
import { OnlineInsightListScreen } from "../handlers/eval/online-insight/list/screen.tsx";
import {
OnlineInsightGetScreen,
OnlineInsightGetJsonScreen,
} from "../handlers/eval/online-insight/get/screen.tsx";
import { BatchEvaluationScreen } from "../handlers/eval/batch-evaluation/screen.tsx";
import { BatchEvaluationListScreen } from "../handlers/eval/batch-evaluation/list/screen.tsx";
import { BatchEvaluationGetJsonScreen } from "../handlers/eval/batch-evaluation/get/screen.tsx";
Expand Down Expand Up @@ -482,6 +488,26 @@ export function Root({ path, ctx, core, queryClient }: RootProps) {
path="agentcore/eval/online-eval/get/:configId/json"
element={<OnlineEvalGetJsonScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/eval/online-insight"
element={<OnlineInsightScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/eval/online-insight/list"
element={<OnlineInsightListScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/eval/online-insight/get"
element={<Navigate to="/agentcore/eval/online-insight/list" replace />}
/>
<Route
path="agentcore/eval/online-insight/get/:configId"
element={<OnlineInsightGetScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/eval/online-insight/get/:configId/json"
element={<OnlineInsightGetJsonScreen ctx={ctx} core={core} />}
/>
<Route path="agentcore/eval/dataset" element={<DatasetScreen ctx={ctx} core={core} />} />
<Route
path="agentcore/eval/dataset/list"
Expand Down
20 changes: 20 additions & 0 deletions src/handlers/eval/online-eval/online-eval.screen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,26 @@ describe("online-eval picker", () => {
expect(frame).toContain("2026-07-21 02:03");
});

test("shows the #2029 insights column: 'yes' when enabled, '-' when not", async () => {
const core = coreWithConfigs([
configSummary({
onlineEvaluationConfigName: "with_insights",
insights: [{ insightId: "ins-1" }],
}),
]);
const screen = renderScreen("/agentcore/eval/online-eval/list", { core });

await waitForText(screen.lastFrame, "with_insights");
expect(screen.lastFrame()).toContain("insights");
expect(screen.lastFrame()).toContain("yes");

const noInsights = renderScreen("/agentcore/eval/online-eval/list", {
core: coreWithConfigs([configSummary({ onlineEvaluationConfigName: "no_insights" })]),
});
await waitForText(noInsights.lastFrame, "no_insights");
expect(noInsights.lastFrame()).not.toContain("yes");
});

test("calls listOnlineEvaluationConfigs with exact Core options", async () => {
const core = coreWithConfigs([configSummary()]);
renderScreen("/agentcore/eval/online-eval/list", { core, endpointUrl: evalEndpointUrl });
Expand Down
77 changes: 77 additions & 0 deletions src/handlers/eval/online-insight/get/screen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { useQuery } from "@tanstack/react-query";
import { useNavigate, useParams } from "react-router";
import { JsonDetail } from "../../../../components/JsonDetail";
import { ResourceDetailScreen } from "../../../../components/ResourceDetailScreen";
import type { ScreenProps } from "../../../types";
import { coreOptsFromCtx } from "../../../utils";

function useOnlineInsightDetail({ ctx, core }: ScreenProps, configId: string | undefined) {
const opts = coreOptsFromCtx(ctx);
return useQuery({
queryKey: ["online-insight", opts.region, configId],
queryFn: () => core.eval.getOnlineInsight(configId!, opts),
enabled: configId !== undefined,
});
}

export function OnlineInsightGetScreen(props: ScreenProps) {
const navigate = useNavigate();
const { configId } = useParams();
const detail = useOnlineInsightDetail(props, configId);
const config = detail.data;
const samplingPercentage = config?.rule?.samplingConfig?.samplingPercentage;
const insightCount = config?.insights?.length ?? 0;
const frequencies = config?.clusteringConfig?.frequencies ?? [];

return (
<ResourceDetailScreen
breadcrumb={["agentcore", "eval", "online-insight", "get", configId ?? ""]}
isPending={detail.isPending}
error={detail.isError ? (detail.error as Error) : null}
items={{
id: config?.onlineEvaluationConfigId ?? "",
name: config?.onlineEvaluationConfigName ?? "",
status: config?.status ?? "-",
execution: config?.executionStatus ?? "-",
sampling: samplingPercentage !== undefined ? `${samplingPercentage}%` : "-",
insights: insightCount > 0 ? insightCount.toString() : "-",
clustering: frequencies.length > 0 ? frequencies.join(", ") : "-",
...(config?.failureReason ? { failureReason: config.failureReason } : {}),
role: config?.evaluationExecutionRoleArn ?? "-",
}}
actions={
configId && config
? [
{
name: "detail",
description: "show the full JSON (insights, clustering, filters, data source)",
onSelect: () =>
navigate(
`/agentcore/eval/online-insight/get/${encodeURIComponent(configId)}/json`,
),
},
]
: []
}
loadingLabel="Loading online insight config…"
onRetry={() => void detail.refetch()}
selectLabel="open detail"
/>
);
}

export function OnlineInsightGetJsonScreen(props: ScreenProps) {
const { configId } = useParams();
const detail = useOnlineInsightDetail(props, configId);

return (
<JsonDetail
breadcrumb={["agentcore", "eval", "online-insight", "get", configId ?? "", "json"]}
isPending={detail.isPending}
error={detail.isError ? (detail.error as Error) : null}
data={detail.data}
loadingLabel="Loading online insight config…"
onRetry={() => void detail.refetch()}
/>
);
}
7 changes: 7 additions & 0 deletions src/handlers/eval/online-insight/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Router } from "../../../router";
import { renderTui } from "../../../tui";
import { withTuiOnEmptyFlagsAndArgs } from "../../../middleware";
import type { AppIO } from "../../../io";
import type { Core } from "../../types";
import { createCreateOnlineInsightHandler } from "./create";
Expand All @@ -11,6 +13,9 @@ import { createDeleteOnlineInsightHandler } from "./delete";

export function createOnlineInsightHandler(core: Core, io: AppIO): Router {
return new Router("online-insight", "manage AgentCore online insight configs")
.use(withTuiOnEmptyFlagsAndArgs(core, io))
.default(renderTui(core, io))
.supportedTuiCommands("get", "list")
.handler(createCreateOnlineInsightHandler(core, io))
.handler(createGetOnlineInsightHandler(core))
.handler(createListOnlineInsightHandler(core))
Expand All @@ -19,3 +24,5 @@ export function createOnlineInsightHandler(core: Core, io: AppIO): Router {
.handler(createResumeOnlineInsightHandler(core))
.handler(createDeleteOnlineInsightHandler(core));
}

export { OnlineInsightScreen } from "./screen.tsx";
28 changes: 28 additions & 0 deletions src/handlers/eval/online-insight/list/screen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useNavigate } from "react-router";
import { OnlineEvalPicker } from "../../../../components/OnlineEvalPicker";
import type { ScreenProps } from "../../../types";
import { coreOptsFromCtx } from "../../../utils";

export function OnlineInsightListScreen(props: ScreenProps) {
const navigate = useNavigate();
const opts = coreOptsFromCtx(props.ctx);

return (
<OnlineEvalPicker
{...props}
breadcrumb={["agentcore", "eval", "online-insight", "list"]}
resourceLabel="online insight configs"
queryKey={["online-insights", opts.region]}
loadPage={async (token, pageSize) => {
const response = await props.core.eval.listOnlineInsights(token, pageSize, opts);
return {
items: response.onlineEvaluationConfigs ?? [],
nextToken: response.nextToken,
};
}}
onSelect={(configId) =>
navigate(`/agentcore/eval/online-insight/get/${encodeURIComponent(configId)}`)
}
/>
);
}
Loading
Loading