From a0795c01a31a29a1d89cbeb390321bf3067bc9c7 Mon Sep 17 00:00:00 2001 From: Yuchen Zhang Date: Thu, 21 May 2026 15:43:21 -0700 Subject: [PATCH 1/2] fix missing attributes in sub-span Signed-off-by: Yuchen Zhang --- .../src/nat/data_models/span.py | 2 ++ .../observability/exporter/span_exporter.py | 6 ++++ .../exporter/test_span_exporter.py | 36 +++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/packages/nvidia_nat_core/src/nat/data_models/span.py b/packages/nvidia_nat_core/src/nat/data_models/span.py index 4fece16d2f..591f92f626 100644 --- a/packages/nvidia_nat_core/src/nat/data_models/span.py +++ b/packages/nvidia_nat_core/src/nat/data_models/span.py @@ -93,6 +93,8 @@ class SpanAttributes(Enum): NAT_SPAN_KIND = f"{_SPAN_PREFIX}.span.kind" INPUT_VALUE = "input.value" INPUT_MIME_TYPE = "input.mime_type" + LLM_MODEL_NAME = "llm.model_name" + LLM_PROVIDER = "llm.provider" LLM_TOKEN_COUNT_PROMPT = "llm.token_count.prompt" LLM_TOKEN_COUNT_COMPLETION = "llm.token_count.completion" LLM_TOKEN_COUNT_TOTAL = "llm.token_count.total" diff --git a/packages/nvidia_nat_core/src/nat/observability/exporter/span_exporter.py b/packages/nvidia_nat_core/src/nat/observability/exporter/span_exporter.py index 01e4425c31..9bdcf1c398 100644 --- a/packages/nvidia_nat_core/src/nat/observability/exporter/span_exporter.py +++ b/packages/nvidia_nat_core/src/nat/observability/exporter/span_exporter.py @@ -28,6 +28,7 @@ from nat.data_models.span import Span from nat.data_models.span import SpanAttributes from nat.data_models.span import SpanContext +from nat.data_models.span import SpanKind from nat.data_models.span import event_type_to_span_kind from nat.observability.exporter.base_exporter import IsolatedAttribute from nat.observability.exporter.processing_exporter import ProcessingExporter @@ -210,6 +211,11 @@ def _process_start_event(self, event: IntermediateStep): span_kind = event_type_to_span_kind(event.event_type) sub_span.set_attribute(f"{self._span_prefix}.span.kind", span_kind.value) + if span_kind == SpanKind.LLM: + if event.payload.name: + sub_span.set_attribute(SpanAttributes.LLM_MODEL_NAME.value, event.payload.name) + if event.payload.framework: + sub_span.set_attribute(SpanAttributes.LLM_PROVIDER.value, event.payload.framework.value) # Enable session grouping by setting session.id from conversation_id try: diff --git a/packages/nvidia_nat_core/tests/nat/observability/exporter/test_span_exporter.py b/packages/nvidia_nat_core/tests/nat/observability/exporter/test_span_exporter.py index 1fff3bf017..c58d49fce8 100644 --- a/packages/nvidia_nat_core/tests/nat/observability/exporter/test_span_exporter.py +++ b/packages/nvidia_nat_core/tests/nat/observability/exporter/test_span_exporter.py @@ -268,6 +268,42 @@ async def test_process_end_event(self, span_exporter, sample_start_event, sample assert exported_span.attributes[SpanAttributes.OUTPUT_VALUE.value] == "Test output" assert "nat.metadata" in exported_span.attributes + async def test_llm_span_exports_cost_lookup_attributes(self, span_exporter): + """Minimal reproducer for Phoenix cost lookup attributes on LLM spans.""" + event_id = str(uuid.uuid4()) + + start_event = create_intermediate_step(UUID=event_id, + event_type=IntermediateStepType.LLM_START, + framework=LLMFrameworkEnum.LANGCHAIN, + name="gemini-2.5-flash", + event_timestamp=datetime.now().timestamp(), + data=StreamEventData(input="What is the capital of France?"), + metadata={}) + + end_event = create_intermediate_step(UUID=event_id, + event_type=IntermediateStepType.LLM_END, + framework=LLMFrameworkEnum.LANGCHAIN, + name="gemini-2.5-flash", + event_timestamp=datetime.now().timestamp(), + span_event_timestamp=datetime.now().timestamp(), + data=StreamEventData(output="Paris"), + metadata={}, + usage_info=UsageInfo(num_llm_calls=1, + seconds_between_calls=0, + token_usage=TokenUsageBaseModel(prompt_tokens=7, + completion_tokens=1, + total_tokens=8))) + + async with span_exporter.start(): + span_exporter.export(start_event) + span_exporter.export(end_event) + await span_exporter.wait_for_tasks() + + exported_span = span_exporter.exported_spans[0] + assert exported_span.attributes[SpanAttributes.LLM_MODEL_NAME.value] == "gemini-2.5-flash" + assert exported_span.attributes[SpanAttributes.LLM_PROVIDER.value] == LLMFrameworkEnum.LANGCHAIN.value + assert exported_span.attributes[SpanAttributes.LLM_TOKEN_COUNT_TOTAL.value] == 8 + def test_process_end_event_missing_span(self, span_exporter, sample_end_event): """Test processing END event with missing span.""" with patch('nat.observability.exporter.span_exporter.logger') as mock_logger: From 3030a195d36cec12a7713f65e8df736ba244153f Mon Sep 17 00:00:00 2001 From: Yuchen Zhang Date: Thu, 21 May 2026 15:52:27 -0700 Subject: [PATCH 2/2] remove dead links Signed-off-by: Yuchen Zhang --- docs/source/get-started/tutorials/create-a-new-workflow.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/get-started/tutorials/create-a-new-workflow.md b/docs/source/get-started/tutorials/create-a-new-workflow.md index 3d3227e1e0..b81b9dd8b4 100644 --- a/docs/source/get-started/tutorials/create-a-new-workflow.md +++ b/docs/source/get-started/tutorials/create-a-new-workflow.md @@ -108,7 +108,7 @@ async def text_file_ingest_function(config: TextFileIngestFunctionConfig, builde ``` -Examining the `webquery_tool` function (`examples/getting_started/simple_web_query/src/nat_simple_web_query/register.py`), you can observe that at the heart of the tool is the [`langchain_community.document_loaders.WebBaseLoader`](https://python.langchain.com/docs/integrations/document_loaders/web_base) class. +Examining the `webquery_tool` function (`examples/getting_started/simple_web_query/src/nat_simple_web_query/register.py`), you can observe that at the heart of the tool is the `langchain_community.document_loaders.WebBaseLoader` class. ```python loader = WebBaseLoader(config.webpage_url) @@ -174,7 +174,7 @@ async def text_file_ingest_function(config: TextFileIngestFunctionConfig, builde ## Creating the Workflow Configuration -Starting from the `custom_config.yml` file you created in the previous section, replace the two `webpage_query` tools with the new `text_file_ingest` tool. For the data source, you can use a collection of text files located in the `examples/documentation_guides/workflows/text_file_ingest/data` directory that describes [DOCA GPUNetIO](https://docs.nvidia.com/doca/sdk/DOCA-GPUNetIO/index.html). +Starting from the `custom_config.yml` file you created in the previous section, replace the two `webpage_query` tools with the new `text_file_ingest` tool. For the data source, you can use a collection of text files located in the `examples/documentation_guides/workflows/text_file_ingest/data` directory that describes `DOCA GPUNetIO`. :::{note}