Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
9149c2d
feat(profiler): NAT-366 Option C - ATIF-native profiler, remove Profi…
afourniernv Mar 5, 2026
597440b
Merge remote-tracking branch 'upstream/develop' into feature/profiler…
afourniernv Mar 16, 2026
d5d271f
style: apply yapf formatting and fix ruff PLR0124 in test_profiler_atif
afourniernv Mar 16, 2026
7b7c783
Add framework field to ATIF step extra and profiler DataFrame
afourniernv Mar 16, 2026
125c964
Add an AtifStepExtra Model for maintaining ancestry
AnuradhaKaruppiah Mar 16, 2026
5f79129
Update profiler to use the new extra step
AnuradhaKaruppiah Mar 16, 2026
cd085f3
Move extra creation to the core ATIF converter
AnuradhaKaruppiah Mar 16, 2026
e8ed030
Remove IntermediateStep from profiler making it atif-native only
AnuradhaKaruppiah Mar 16, 2026
19c6103
Add scripts to convert ATIF and IST to crisp function trees for A/B
AnuradhaKaruppiah Mar 16, 2026
cd2371c
Style fixes
AnuradhaKaruppiah Mar 16, 2026
19510eb
WORKFLOW_END timestamp could be lost when final output==lastLLM output
AnuradhaKaruppiah Mar 16, 2026
348d487
Zero-token metrics steps were misclassified as WORKFLOW_END
AnuradhaKaruppiah Mar 16, 2026
1af6d46
Address review comments
AnuradhaKaruppiah Mar 17, 2026
3e7d78c
Fixup dynamo docs links yet again
AnuradhaKaruppiah Mar 17, 2026
eb3019f
Handle bot string and numeric epoch values
AnuradhaKaruppiah Mar 17, 2026
f60a9fa
Remove unnecessary pytest.mark.asyncio
AnuradhaKaruppiah Mar 17, 2026
eff867a
CodeRabbit NITs applied, CI re-run
afourniernv Mar 17, 2026
5a61e00
Merge upstream/develop into feature/profiler-atif
afourniernv Mar 17, 2026
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: 1 addition & 1 deletion examples/dynamo_integration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ limitations under the License.
> ⚠️ **EXPERIMENTAL**: This integration between NeMo Agent Toolkit and Dynamo is experimental and under active development. APIs, configurations, and features may change without notice.

> [!WARNING]
> **This example requires a Linux system with an NVIDIA GPU.** See the [Dynamo Support Matrix](https://docs.nvidia.com/dynamo/getting-started/support-matrix) for full details.
> **This example requires a Linux system with an NVIDIA GPU.** See the [Dynamo Support Matrix](https://docs.nvidia.com/dynamo/latest/resources/support-matrix) for full details.
>
> **Supported Platforms:**
> - Ubuntu 22.04 / 24.04 (x86_64)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Currently this agent supports evaluation exclusively for the [Galileo Agent Lead
### Software Requirements

> [!WARNING]
> **This example requires a Linux system with an NVIDIA GPU.** See the [Dynamo Support Matrix](https://docs.nvidia.com/dynamo/getting-started/support-matrix) for full details.
> **This example requires a Linux system with an NVIDIA GPU.** See the [Dynamo Support Matrix](https://docs.nvidia.com/dynamo/latest/resources/support-matrix) for full details.
>
> **Supported Platforms:**
> - Ubuntu 22.04 / 24.04 (x86_64)
Expand Down
2 changes: 1 addition & 1 deletion external/dynamo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ Dynamo is NVIDIA's high-performance LLM serving platform with KV cache optimizat
### Platform Requirements

> [!WARNING]
> **This example requires a Linux system with an NVIDIA GPU.** See the [Dynamo Support Matrix](https://docs.nvidia.com/dynamo/getting-started/support-matrix) for full details.
> **This example requires a Linux system with an NVIDIA GPU.** See the [Dynamo Support Matrix](https://docs.nvidia.com/dynamo/latest/resources/support-matrix) for full details.
>
> **Supported Platforms:**
> - Ubuntu 22.04 / 24.04 (x86_64)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
"""

from nat.data_models.atif.agent import Agent
from nat.data_models.atif.atif_step_extra import AtifAncestry
from nat.data_models.atif.atif_step_extra import AtifStepExtra
from nat.data_models.atif.content import ContentPart
from nat.data_models.atif.content import ImageSource
from nat.data_models.atif.final_metrics import FinalMetrics
Expand Down Expand Up @@ -73,6 +75,8 @@
"Metrics",
"Observation",
"ObservationResult",
"AtifAncestry",
"AtifStepExtra",
"Step",
"SubagentTrajectoryRef",
"ToolCall",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Typed models for NAT metadata inside ATIF ``Step.extra``."""

from __future__ import annotations

from pydantic import BaseModel
from pydantic import ConfigDict
from pydantic import Field

from nat.data_models.invocation_node import InvocationNode


class AtifAncestry(BaseModel):
"""Validated ancestry metadata embedded in ATIF ``Step.extra``."""

model_config = ConfigDict(extra="forbid")

function_ancestry: InvocationNode = Field(
...,
description="Function ancestry for the event represented by this metadata entry.",
)
span_event_timestamp: float | None = Field(
default=None,
description=("Start timestamp of the span for an END event. For step-level ancestry this is the step span "
"start; for tool ancestry entries this is the tool span start."),
)
framework: str | None = Field(
default=None,
description="Optional LLM framework identifier (for example, `langchain`).",
)


class AtifStepExtra(BaseModel):
"""Validated structure for NAT-owned ATIF ``Step.extra`` payload."""

model_config = ConfigDict(extra="allow")

ancestry: AtifAncestry = Field(
...,
description="Required step-level ancestry metadata for ATIF processing.",
)
tool_ancestry: list[AtifAncestry] = Field(
default_factory=list,
description=("Optional per-tool ancestry metadata aligned by index with `tool_calls` when a single agent "
"step contains multiple tool calls."),
)
92 changes: 88 additions & 4 deletions packages/nvidia_nat_core/src/nat/utils/atif_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,20 @@

from __future__ import annotations

__all__ = ["ATIFStreamConverter", "IntermediateStepToATIFConverter"]

import datetime
import logging
import uuid
from typing import Any

from nat.data_models.atif import ATIFAgentConfig
from nat.data_models.atif import AtifAncestry
from nat.data_models.atif import ATIFFinalMetrics
from nat.data_models.atif import ATIFObservation
from nat.data_models.atif import ATIFObservationResult
from nat.data_models.atif import ATIFStep
from nat.data_models.atif import AtifStepExtra
from nat.data_models.atif import ATIFStepMetrics
from nat.data_models.atif import ATIFToolCall
from nat.data_models.atif import ATIFTrajectory
Expand All @@ -46,12 +50,21 @@

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _epoch_to_iso(epoch: float) -> str:
"""Convert a Unix epoch timestamp to an ISO 8601 string."""
return datetime.datetime.fromtimestamp(epoch, tz=datetime.UTC).isoformat()


def _iso_to_epoch(timestamp: str) -> float:
"""Convert an ISO 8601 timestamp to Unix epoch seconds."""
return datetime.datetime.fromisoformat(timestamp).timestamp()


def _extract_tool_definitions(step: IntermediateStep) -> list[dict[str, Any]] | None:
"""Extract OpenAI-style tool definitions from an IntermediateStep's metadata."""
if not isinstance(step.metadata, TraceMetadata):
Expand Down Expand Up @@ -116,6 +129,20 @@ def _extract_user_input(value: Any) -> str:
return str(value)


def _atif_ancestry_from_ist(ist: IntermediateStep) -> AtifAncestry:
"""Build typed ATIF ancestry metadata from an IntermediateStep."""
return AtifAncestry(
function_ancestry=ist.function_ancestry,
span_event_timestamp=ist.payload.span_event_timestamp,
framework=ist.payload.framework.value if ist.payload.framework is not None else None,
)


def _atif_step_extra_model_from_ist(ist: IntermediateStep) -> AtifStepExtra:
"""Build typed ATIF step extra model from an IntermediateStep."""
return AtifStepExtra(ancestry=_atif_ancestry_from_ist(ist))


def _parse_tool_arguments(raw_input: Any) -> dict[str, Any]:
"""Best-effort extraction of tool arguments as a dict."""
if isinstance(raw_input, dict):
Expand Down Expand Up @@ -144,6 +171,11 @@ def _parse_tool_arguments(raw_input: Any) -> dict[str, Any]:
return {}


# ---------------------------------------------------------------------------
# Internal accumulator
# ---------------------------------------------------------------------------


class _PendingAgentTurn:
"""Accumulator for an in-progress ATIF agent turn."""

Expand All @@ -154,7 +186,14 @@ def __init__(self, message: str, timestamp: float, model_name: str | None, metri
self.metrics = metrics
self.tool_calls: list[ATIFToolCall] = []
self.observations: list[ATIFObservationResult] = []
self.ancestry: AtifAncestry | None = None
self.extra: dict[str, Any] = {}
self.tool_ancestry: list[AtifAncestry] = []


# ---------------------------------------------------------------------------
# Batch converter
# ---------------------------------------------------------------------------


class IntermediateStepToATIFConverter:
Expand Down Expand Up @@ -190,6 +229,13 @@ def _flush_pending() -> None:
if pending is None:
return
observation = ATIFObservation(results=pending.observations) if pending.observations else None
if pending.ancestry is None:
raise ValueError("Pending agent turn is missing required ATIF ancestry metadata")
step_extra = AtifStepExtra(
ancestry=pending.ancestry,
tool_ancestry=pending.tool_ancestry,
**pending.extra,
)
atif_steps.append(
ATIFStep(
step_id=step_id,
Expand All @@ -200,7 +246,7 @@ def _flush_pending() -> None:
tool_calls=pending.tool_calls or None,
observation=observation,
metrics=pending.metrics,
extra=pending.extra or None,
extra=step_extra.model_dump(exclude_none=True),
))
step_id += 1
pending = None
Expand All @@ -218,12 +264,14 @@ def _flush_pending() -> None:
fn_name = ist.function_ancestry.function_name
if fn_name and fn_name != "root":
agent_config.name = fn_name
extra = _atif_step_extra_model_from_ist(ist).model_dump(exclude_none=True)
atif_steps.append(
ATIFStep(
step_id=step_id,
source="user",
message=user_input,
timestamp=_epoch_to_iso(ist.event_timestamp),
extra=extra or None,
))
step_id += 1
continue
Expand All @@ -234,17 +282,24 @@ def _flush_pending() -> None:
if ist.data and ist.data.output is not None:
final_output = _safe_str(ist.data.output)
last_agent_msg = ""
last_agent_ts: float | None = None
for s in reversed(atif_steps):
if s.source == "agent":
last_agent_msg = str(s.message)
last_agent_ts = _iso_to_epoch(s.timestamp) if s.timestamp else None
break
if final_output and final_output != last_agent_msg:
should_emit_terminal_step = bool(final_output) and (final_output != last_agent_msg or
(last_agent_ts is not None
and ist.event_timestamp > last_agent_ts))
if should_emit_terminal_step:
extra = _atif_step_extra_model_from_ist(ist).model_dump(exclude_none=True)
atif_steps.append(
ATIFStep(
step_id=step_id,
source="agent",
message=final_output,
timestamp=_epoch_to_iso(ist.event_timestamp),
extra=extra or None,
))
step_id += 1
continue
Expand Down Expand Up @@ -272,6 +327,7 @@ def _flush_pending() -> None:
model_name=ist.name,
metrics=metrics,
)
pending.ancestry = _atif_ancestry_from_ist(ist)
continue

if event_type == IntermediateStepType.TOOL_END:
Expand All @@ -287,7 +343,9 @@ def _flush_pending() -> None:
if pending is not None:
pending.tool_calls.append(tc)
pending.observations.append(obs)
pending.tool_ancestry.append(_atif_ancestry_from_ist(ist))
else:
extra = _atif_step_extra_model_from_ist(ist).model_dump(exclude_none=True)
atif_steps.append(
ATIFStep(
step_id=step_id,
Expand All @@ -296,6 +354,7 @@ def _flush_pending() -> None:
timestamp=_epoch_to_iso(ist.event_timestamp),
tool_calls=[tc],
observation=ATIFObservation(results=[obs]),
extra=extra or None,
))
step_id += 1
continue
Expand Down Expand Up @@ -339,6 +398,11 @@ def _flush_pending() -> None:
)


# ---------------------------------------------------------------------------
# Stream converter
# ---------------------------------------------------------------------------


class ATIFStreamConverter:
"""Stateful converter that emits ATIF steps incrementally."""

Expand Down Expand Up @@ -370,11 +434,13 @@ def push(self, ist: IntermediateStep) -> ATIFStep | None:
fn_name = ist.function_ancestry.function_name
if fn_name and fn_name != "root":
self._agent_config.name = fn_name
extra = _atif_step_extra_model_from_ist(ist).model_dump(exclude_none=True)
step = ATIFStep(
step_id=self._step_id,
source="user",
message=user_input,
timestamp=_epoch_to_iso(ist.event_timestamp),
extra=extra or None,
)
self._step_id += 1
self._emitted_steps.append(step)
Expand All @@ -389,16 +455,23 @@ def push(self, ist: IntermediateStep) -> ATIFStep | None:
if ist.data and ist.data.output is not None:
final_output = _safe_str(ist.data.output)
last_agent_msg = ""
last_agent_ts: float | None = None
for s in reversed(self._emitted_steps):
if s.source == "agent":
last_agent_msg = str(s.message)
last_agent_ts = _iso_to_epoch(s.timestamp) if s.timestamp else None
break
if final_output and final_output != last_agent_msg:
should_emit_terminal_step = bool(final_output) and (final_output != last_agent_msg or
(last_agent_ts is not None
and ist.event_timestamp > last_agent_ts))
if should_emit_terminal_step:
extra = _atif_step_extra_model_from_ist(ist).model_dump(exclude_none=True)
final_step = ATIFStep(
step_id=self._step_id,
source="agent",
message=final_output,
timestamp=_epoch_to_iso(ist.event_timestamp),
extra=extra or None,
)
self._step_id += 1
self._emitted_steps.append(final_step)
Expand Down Expand Up @@ -428,6 +501,7 @@ def push(self, ist: IntermediateStep) -> ATIFStep | None:
model_name=ist.name,
metrics=metrics,
)
self._pending.ancestry = _atif_ancestry_from_ist(ist)
return flushed

if event_type == IntermediateStepType.TOOL_END:
Expand All @@ -443,15 +517,18 @@ def push(self, ist: IntermediateStep) -> ATIFStep | None:
if self._pending is not None:
self._pending.tool_calls.append(tc)
self._pending.observations.append(obs)
self._pending.tool_ancestry.append(_atif_ancestry_from_ist(ist))
return None

extra = _atif_step_extra_model_from_ist(ist).model_dump(exclude_none=True)
orphan_step = ATIFStep(
step_id=self._step_id,
source="agent",
message="",
timestamp=_epoch_to_iso(ist.event_timestamp),
tool_calls=[tc],
observation=ATIFObservation(results=[obs]),
extra=extra or None,
)
self._step_id += 1
self._emitted_steps.append(orphan_step)
Expand Down Expand Up @@ -502,6 +579,13 @@ def _flush_pending(self) -> ATIFStep | None:
return None
pending = self._pending
observation = ATIFObservation(results=pending.observations) if pending.observations else None
if pending.ancestry is None:
raise ValueError("Pending agent turn is missing required ATIF ancestry metadata")
step_extra = AtifStepExtra(
ancestry=pending.ancestry,
tool_ancestry=pending.tool_ancestry,
**pending.extra,
)
step = ATIFStep(
step_id=self._step_id,
source="agent",
Expand All @@ -511,7 +595,7 @@ def _flush_pending(self) -> ATIFStep | None:
tool_calls=pending.tool_calls or None,
observation=observation,
metrics=pending.metrics,
extra=pending.extra or None,
extra=step_extra.model_dump(exclude_none=True),
)
self._step_id += 1
self._emitted_steps.append(step)
Expand Down
Loading
Loading