Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
2 changes: 2 additions & 0 deletions k8s/helm/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<!-- SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -->
<!-- SPDX-License-Identifier: Apache-2.0 -->



# NeMo Platform Helm Chart

![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square)
Expand Down
3 changes: 3 additions & 0 deletions openapi/ga/individual/platform.openapi.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions openapi/ga/openapi.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions openapi/openapi.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions plugins/nemo-evaluator/openapi/openapi.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion web/packages/common/src/components/StatTile/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

import { Flex, Panel, Stack, Text } from '@nvidia/foundations-react-core';
import cn from 'classnames';
import type { FC } from 'react';

export type StatTileStatus = 'success' | 'warning' | 'error' | 'neutral';
Expand All @@ -13,6 +14,7 @@ export interface StatTileProps {
trailingLabelStatus?: StatTileStatus;
hint?: string;
hintStatus?: StatTileStatus;
className?: string;
}

const MUTED_CLASS_NAME = 'text-placeholder';
Expand All @@ -31,8 +33,9 @@ export const StatTile: FC<StatTileProps> = ({
trailingLabelStatus,
hint,
hintStatus,
className,
}) => (
<Panel className="max-w-sm">
<Panel className={cn('max-w-sm', className)}>
<Stack gap="density-sm">
<Text kind="body/regular/sm" className={MUTED_CLASS_NAME}>
{label}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import type { Meta, StoryObj } from '@storybook/react';
import {
AgentTraceStatistics,
type AgentTraceStatisticsProps,
} from '@studio/components/AgentTraceStatistics/index';
import type {
TraceStatisticsRange,
TraceStatisticsSample,
} from '@studio/components/AgentTraceStatistics/types';
import { type FC, useState } from 'react';

const ANCHOR = new Date('2026-07-01T00:00:00Z').getTime();
const HOUR_MS = 60 * 60 * 1000;
const DAY_MS = 24 * HOUR_MS;

/** Deterministic LCG — fixtures must not change between story renders. */
const makeRandom = (seed: number): (() => number) => {
let state = seed;
return () => {
state = (state * 1664525 + 1013904223) % 4294967296;
return state / 4294967296;
};
};

interface FixtureOptions {
buckets: number;
bucketMs: number;
tracesPerBucket?: number;
seed?: number;
}

/**
* Token counts swing on a slow sine so the chart shows the peaks and troughs of a real workload;
* latency and cost are derived from tokens with independent jitter.
*/
const makeTraces = ({
buckets,
bucketMs,
tracesPerBucket = 6,
seed = 42,
}: FixtureOptions): TraceStatisticsSample[] => {
const random = makeRandom(seed);
const samples: TraceStatisticsSample[] = [];
const start = ANCHOR - (buckets - 1) * bucketMs;

for (let bucket = 0; bucket < buckets; bucket++) {
const wave = Math.sin(bucket / 1.7) * 0.5 + Math.sin(bucket / 5.3) * 0.5;
const baseTokens = 2200 + wave * 1300;
for (let i = 0; i < tracesPerBucket; i++) {
const totalTokens = Math.max(120, Math.round(baseTokens * (0.85 + random() * 0.3)));
const msPerToken = 0.045 + random() * 0.02;
samples.push({
startedAt: new Date(start + bucket * bucketMs + random() * bucketMs),
totalTokens,
durationMs: Math.round(totalTokens * msPerToken * 1000) / 1000,
costUsd: totalTokens * 0.0000032 * (0.9 + random() * 0.2),
});
}
}
return samples;
};

const MONTH_TRACES = makeTraces({ buckets: 31, bucketMs: DAY_MS });
const WEEK_TRACES = makeTraces({ buckets: 7, bucketMs: DAY_MS, seed: 7 });
const DAY_TRACES = makeTraces({ buckets: 24, bucketMs: HOUR_MS, tracesPerBucket: 3, seed: 11 });

const TRACES_BY_RANGE: Record<TraceStatisticsRange, TraceStatisticsSample[]> = {
day: DAY_TRACES,
week: WEEK_TRACES,
month: MONTH_TRACES,
};

const meta: Meta<typeof AgentTraceStatistics> = {
component: AgentTraceStatistics,
title: 'Studio/AgentTraceStatistics',
args: {
range: 'month',
traces: MONTH_TRACES,
onRangeChange: () => {},
onViewTraces: () => {},
},
argTypes: {
range: { control: 'select', options: ['day', 'week', 'month'] },
chartHeight: { control: { type: 'range', min: 160, max: 600, step: 20 } },
},
decorators: [
(Story) => (
<div className="w-full max-w-[1200px] p-6">
<Story />
</div>
),
],
};

export default meta;

type Story = StoryObj<typeof AgentTraceStatistics>;

/** Stands in for the caller's refetch: changing the range swaps which fixture is passed in. */
const RangeAwareStatistics: FC<AgentTraceStatisticsProps> = (props) => {
const [range, setRange] = useState<TraceStatisticsRange>(props.range);
return (
<AgentTraceStatistics
{...props}
range={range}
traces={TRACES_BY_RANGE[range]}
onRangeChange={setRange}
Comment thread
steramae-nvidia marked this conversation as resolved.
/>
);
};

export const Default: Story = {
render: (args) => <RangeAwareStatistics {...args} />,
};

/** Hourly buckets — the tick formatter switches from dates to hours. */
export const DayRange: Story = {
args: { range: 'day', traces: DAY_TRACES },
};

export const Loading: Story = {
args: { isPending: true, traces: [] },
};

/**
* First run: the tiles and chart give way to instructions, since four zeros and a blank grid tell
* the user nothing about how to get data.
*/
export const Empty: Story = {
args: {
traces: [],
onRunAgent: () => {},
onLearnMore: () => {},
},
};

/** Traces missing cost or token rollups still contribute what they have. */
export const PartialRollups: Story = {
args: {
traces: MONTH_TRACES.map((trace, index) =>
index % 3 === 0 ? { ...trace, costUsd: null, totalTokens: null } : trace
),
},
};

/** No `onViewTraces` hides the action — for surfaces that are already the traces list. */
export const WithoutViewTracesAction: Story = {
args: { onViewTraces: undefined },
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { ComparisonLineChart } from '@nemo/common/src/components/ComparisonLineChart/index';
import type {
ComparisonSeries,
ComparisonXValue,
} from '@nemo/common/src/components/ComparisonLineChart/types';
import { Card, Stack, Text } from '@nvidia/foundations-react-core';
import type {
TraceStatisticsBucket,
TraceStatisticsRange,
} from '@studio/components/AgentTraceStatistics/types';
import {
bucketAdverbForRange,
formatBucketTick,
formatCostUsd,
formatLatencyMs,
formatTokens,
} from '@studio/components/AgentTraceStatistics/utils';
import { type FC, useMemo } from 'react';

interface Props {
buckets: TraceStatisticsBucket[];
range: TraceStatisticsRange;
isPending?: boolean;
height?: number;
}

interface SeriesSpec {
id: string;
label: string;
color: string;
select: (bucket: TraceStatisticsBucket) => number | null;
format: (value: number) => string;
}

const SERIES: SeriesSpec[] = [
{
id: 'cost',
label: 'Cost',
color: 'var(--text-color-accent-green)',
select: (bucket) => bucket.costUsd,
format: formatCostUsd,
},
{
id: 'tokens',
label: 'Tokens',
color: 'var(--text-color-accent-blue)',
select: (bucket) => bucket.tokens,
format: formatTokens,
},
{
id: 'latency',
label: 'Latency',
color: 'var(--text-color-accent-yellow-strong)',
select: (bucket) => bucket.latencyMs,
format: formatLatencyMs,
},
];

const DEFAULT_HEIGHT = 320;

const asDate = (value: ComparisonXValue): Date =>
value instanceof Date ? value : new Date(value as number);

export const TraceStatisticsChart: FC<Props> = ({
buckets,
range,
isPending,
height = DEFAULT_HEIGHT,
}) => {
const xAxis = useMemo(() => buckets.map((bucket) => new Date(bucket.timestamp)), [buckets]);

const series = useMemo<ComparisonSeries[]>(
() =>
SERIES.map((spec) => ({
id: spec.id,
label: spec.label,
color: spec.color,
data: buckets.map(spec.select),
valueFormatter: (value: number | null) => (value == null ? '—' : spec.format(value)),
})),
[buckets]
);

const formatXValue = (value: ComparisonXValue): string =>
formatBucketTick(asDate(value).getTime(), range);

return (
<Card>
<Stack padding="density-xl">
<ComparisonLineChart
title={<Text kind="title/sm">{`${bucketAdverbForRange(range)} averages over time`}</Text>}
series={series}
xAxis={xAxis}
height={height}
loading={isPending}
emptyMessage="No traces in this range"
formatXValue={formatXValue}
formatYValue={formatTokens}
/>
Comment thread
steramae-nvidia marked this conversation as resolved.
</Stack>
</Card>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { Button, Card, Flex, StatusMessage } from '@nvidia/foundations-react-core';
import { ChartNoAxesCombined, Play } from 'lucide-react';
import { type FC } from 'react';

interface Props {
/** Invoke the agent so it emits its first traces. */
onRunAgent?: () => void;
/** Widen the window — offered only when a longer one exists. */
onExpandRange?: () => void;
/** Docs on turning on tracing for an agent. */
onLearnMore?: () => void;
}

export const TraceStatisticsEmptyState: FC<Props> = ({
onRunAgent,
onExpandRange,
onLearnMore,
}) => (
<Card>
<Flex justify="center" padding="density-2xl">
<StatusMessage
slotMedia={<ChartNoAxesCombined className="size-12 text-placeholder" />}
slotHeading="No traces yet"
slotSubheading={`Cost, token, and latency averages are built from instrumented agent runs — nothing has reported. Send the agent's traces to Intake, then invoke it to start filling this in.`}
slotFooter={
<Flex gap="density-sm" justify="center" wrap="wrap">
{onRunAgent ? (
<Button onClick={onRunAgent}>
<Play size={16} aria-hidden />
Run the agent
</Button>
) : null}
{onLearnMore ? (
<Button kind="secondary" onClick={onLearnMore}>
Set up tracing
</Button>
) : null}
{onExpandRange ? (
<Button kind="tertiary" onClick={onExpandRange}>
Look back a month
</Button>
) : null}
</Flex>
}
/>
</Flex>
</Card>
);
Loading
Loading