nvidia-nat 1.3.0a20250909__py3-none-any.whl → 1.3.0a20250917__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. nat/agent/base.py +11 -6
  2. nat/agent/dual_node.py +2 -2
  3. nat/agent/prompt_optimizer/prompt.py +68 -0
  4. nat/agent/prompt_optimizer/register.py +149 -0
  5. nat/agent/react_agent/agent.py +1 -1
  6. nat/agent/react_agent/register.py +17 -7
  7. nat/agent/reasoning_agent/reasoning_agent.py +6 -1
  8. nat/agent/register.py +2 -0
  9. nat/agent/rewoo_agent/agent.py +6 -3
  10. nat/agent/rewoo_agent/register.py +16 -10
  11. nat/agent/router_agent/__init__.py +0 -0
  12. nat/agent/router_agent/agent.py +329 -0
  13. nat/agent/router_agent/prompt.py +48 -0
  14. nat/agent/router_agent/register.py +97 -0
  15. nat/agent/tool_calling_agent/agent.py +69 -7
  16. nat/agent/tool_calling_agent/register.py +17 -9
  17. nat/builder/builder.py +27 -4
  18. nat/builder/component_utils.py +7 -3
  19. nat/builder/function.py +167 -0
  20. nat/builder/function_info.py +1 -1
  21. nat/builder/workflow.py +5 -0
  22. nat/builder/workflow_builder.py +213 -16
  23. nat/cli/commands/optimize.py +90 -0
  24. nat/cli/commands/workflow/templates/config.yml.j2 +0 -1
  25. nat/cli/commands/workflow/workflow_commands.py +5 -8
  26. nat/cli/entrypoint.py +2 -0
  27. nat/cli/register_workflow.py +38 -4
  28. nat/cli/type_registry.py +71 -0
  29. nat/data_models/api_server.py +1 -1
  30. nat/data_models/component.py +2 -0
  31. nat/data_models/component_ref.py +11 -0
  32. nat/data_models/config.py +40 -16
  33. nat/data_models/function.py +34 -0
  34. nat/data_models/function_dependencies.py +8 -0
  35. nat/data_models/optimizable.py +119 -0
  36. nat/data_models/optimizer.py +149 -0
  37. nat/data_models/temperature_mixin.py +4 -3
  38. nat/data_models/top_p_mixin.py +4 -3
  39. nat/embedder/nim_embedder.py +1 -1
  40. nat/embedder/openai_embedder.py +1 -1
  41. nat/eval/config.py +1 -1
  42. nat/eval/evaluate.py +5 -1
  43. nat/eval/register.py +4 -0
  44. nat/eval/runtime_evaluator/__init__.py +14 -0
  45. nat/eval/runtime_evaluator/evaluate.py +123 -0
  46. nat/eval/runtime_evaluator/register.py +100 -0
  47. nat/experimental/test_time_compute/functions/plan_select_execute_function.py +5 -1
  48. nat/front_ends/fastapi/dask_client_mixin.py +43 -0
  49. nat/front_ends/fastapi/fastapi_front_end_config.py +14 -3
  50. nat/front_ends/fastapi/fastapi_front_end_plugin.py +111 -3
  51. nat/front_ends/fastapi/fastapi_front_end_plugin_worker.py +243 -228
  52. nat/front_ends/fastapi/job_store.py +518 -99
  53. nat/front_ends/fastapi/main.py +11 -19
  54. nat/front_ends/fastapi/utils.py +57 -0
  55. nat/front_ends/mcp/mcp_front_end_plugin_worker.py +3 -2
  56. nat/llm/aws_bedrock_llm.py +15 -4
  57. nat/llm/nim_llm.py +14 -3
  58. nat/llm/openai_llm.py +8 -1
  59. nat/observability/exporter/processing_exporter.py +29 -55
  60. nat/observability/mixin/redaction_config_mixin.py +5 -4
  61. nat/observability/mixin/tagging_config_mixin.py +26 -14
  62. nat/observability/mixin/type_introspection_mixin.py +401 -107
  63. nat/observability/processor/processor.py +3 -0
  64. nat/observability/processor/redaction/__init__.py +24 -0
  65. nat/observability/processor/redaction/contextual_redaction_processor.py +125 -0
  66. nat/observability/processor/redaction/contextual_span_redaction_processor.py +66 -0
  67. nat/observability/processor/redaction/redaction_processor.py +177 -0
  68. nat/observability/processor/redaction/span_header_redaction_processor.py +92 -0
  69. nat/observability/processor/span_tagging_processor.py +21 -14
  70. nat/profiler/decorators/framework_wrapper.py +9 -6
  71. nat/profiler/parameter_optimization/__init__.py +0 -0
  72. nat/profiler/parameter_optimization/optimizable_utils.py +93 -0
  73. nat/profiler/parameter_optimization/optimizer_runtime.py +67 -0
  74. nat/profiler/parameter_optimization/parameter_optimizer.py +149 -0
  75. nat/profiler/parameter_optimization/parameter_selection.py +108 -0
  76. nat/profiler/parameter_optimization/pareto_visualizer.py +380 -0
  77. nat/profiler/parameter_optimization/prompt_optimizer.py +384 -0
  78. nat/profiler/parameter_optimization/update_helpers.py +66 -0
  79. nat/profiler/utils.py +3 -1
  80. nat/tool/chat_completion.py +5 -2
  81. nat/tool/document_search.py +1 -1
  82. nat/tool/github_tools.py +450 -0
  83. nat/tool/register.py +2 -7
  84. nat/utils/callable_utils.py +70 -0
  85. nat/utils/exception_handlers/automatic_retries.py +103 -48
  86. nat/utils/type_utils.py +4 -0
  87. {nvidia_nat-1.3.0a20250909.dist-info → nvidia_nat-1.3.0a20250917.dist-info}/METADATA +8 -1
  88. {nvidia_nat-1.3.0a20250909.dist-info → nvidia_nat-1.3.0a20250917.dist-info}/RECORD +94 -74
  89. nat/observability/processor/header_redaction_processor.py +0 -123
  90. nat/observability/processor/redaction_processor.py +0 -77
  91. nat/tool/github_tools/create_github_commit.py +0 -133
  92. nat/tool/github_tools/create_github_issue.py +0 -87
  93. nat/tool/github_tools/create_github_pr.py +0 -106
  94. nat/tool/github_tools/get_github_file.py +0 -106
  95. nat/tool/github_tools/get_github_issue.py +0 -166
  96. nat/tool/github_tools/get_github_pr.py +0 -256
  97. nat/tool/github_tools/update_github_issue.py +0 -100
  98. /nat/{tool/github_tools → agent/prompt_optimizer}/__init__.py +0 -0
  99. {nvidia_nat-1.3.0a20250909.dist-info → nvidia_nat-1.3.0a20250917.dist-info}/WHEEL +0 -0
  100. {nvidia_nat-1.3.0a20250909.dist-info → nvidia_nat-1.3.0a20250917.dist-info}/entry_points.txt +0 -0
  101. {nvidia_nat-1.3.0a20250909.dist-info → nvidia_nat-1.3.0a20250917.dist-info}/licenses/LICENSE-3rd-party.txt +0 -0
  102. {nvidia_nat-1.3.0a20250909.dist-info → nvidia_nat-1.3.0a20250917.dist-info}/licenses/LICENSE.md +0 -0
  103. {nvidia_nat-1.3.0a20250909.dist-info → nvidia_nat-1.3.0a20250917.dist-info}/top_level.txt +0 -0
@@ -13,19 +13,24 @@
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
15
 
16
- import importlib
17
16
  import logging
18
17
  import os
18
+ import typing
19
19
 
20
20
  from nat.front_ends.fastapi.fastapi_front_end_plugin_worker import FastApiFrontEndPluginWorkerBase
21
+ from nat.front_ends.fastapi.utils import get_config_file_path
22
+ from nat.front_ends.fastapi.utils import import_class_from_string
21
23
  from nat.runtime.loader import load_config
22
24
 
25
+ if typing.TYPE_CHECKING:
26
+ from fastapi import FastAPI
27
+
23
28
  logger = logging.getLogger(__name__)
24
29
 
25
30
 
26
- def get_app():
31
+ def get_app() -> "FastAPI":
27
32
 
28
- config_file_path = os.getenv("NAT_CONFIG_FILE")
33
+ config_file_path = get_config_file_path()
29
34
  front_end_worker_full_name = os.getenv("NAT_FRONT_END_WORKER")
30
35
 
31
36
  if (not config_file_path):
@@ -36,28 +41,15 @@ def get_app():
36
41
 
37
42
  # Try to import the front end worker class
38
43
  try:
39
- # Split the package from the class
40
- front_end_worker_parts = front_end_worker_full_name.split(".")
41
-
42
- front_end_worker_module_name = ".".join(front_end_worker_parts[:-1])
43
- front_end_worker_class_name = front_end_worker_parts[-1]
44
-
45
- front_end_worker_module = importlib.import_module(front_end_worker_module_name)
46
-
47
- if not hasattr(front_end_worker_module, front_end_worker_class_name):
48
- raise ValueError(f"Front end worker {front_end_worker_full_name} not found.")
49
-
50
- front_end_worker_class: type[FastApiFrontEndPluginWorkerBase] = getattr(front_end_worker_module,
51
- front_end_worker_class_name)
44
+ front_end_worker_class: type[FastApiFrontEndPluginWorkerBase] = import_class_from_string(
45
+ front_end_worker_full_name)
52
46
 
53
47
  if (not issubclass(front_end_worker_class, FastApiFrontEndPluginWorkerBase)):
54
48
  raise ValueError(
55
49
  f"Front end worker {front_end_worker_full_name} is not a subclass of FastApiFrontEndPluginWorker.")
56
50
 
57
51
  # Load the config
58
- abs_config_file_path = os.path.abspath(config_file_path)
59
-
60
- config = load_config(abs_config_file_path)
52
+ config = load_config(config_file_path)
61
53
 
62
54
  # Create an instance of the front end worker class
63
55
  front_end_worker = front_end_worker_class(config)
@@ -0,0 +1,57 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ import importlib
17
+ import os
18
+
19
+
20
+ def get_config_file_path() -> str:
21
+ """
22
+ Get the path to the NAT configuration file from the environment variable NAT_CONFIG_FILE.
23
+ Raises ValueError if the environment variable is not set.
24
+ """
25
+ config_file_path = os.getenv("NAT_CONFIG_FILE")
26
+ if (not config_file_path):
27
+ raise ValueError("Config file not found in environment variable NAT_CONFIG_FILE.")
28
+
29
+ return os.path.abspath(config_file_path)
30
+
31
+
32
+ def import_class_from_string(class_full_name: str) -> type:
33
+ """
34
+ Import a class from a string in the format 'module.submodule.ClassName'.
35
+ Raises ImportError if the class cannot be imported.
36
+ """
37
+ try:
38
+ class_name_parts = class_full_name.split(".")
39
+
40
+ module_name = ".".join(class_name_parts[:-1])
41
+ class_name = class_name_parts[-1]
42
+
43
+ module = importlib.import_module(module_name)
44
+
45
+ if not hasattr(module, class_name):
46
+ raise ValueError(f"Class '{class_full_name}' not found.")
47
+
48
+ return getattr(module, class_name)
49
+ except (ImportError, AttributeError) as e:
50
+ raise ImportError(f"Could not import {class_full_name}.") from e
51
+
52
+
53
+ def get_class_name(cls: type) -> str:
54
+ """
55
+ Get the full class name including the module.
56
+ """
57
+ return f"{cls.__module__}.{cls.__qualname__}"
@@ -94,8 +94,9 @@ class MCPFrontEndPluginWorkerBase(ABC):
94
94
  functions: dict[str, Function] = {}
95
95
 
96
96
  # Extract all functions from the workflow
97
- for function_name, function in workflow.functions.items():
98
- functions[function_name] = function
97
+ functions.update(workflow.functions)
98
+ for function_group in workflow.function_groups.values():
99
+ functions.update(function_group.get_accessible_functions())
99
100
 
100
101
  functions[workflow.config.workflow.type] = workflow
101
102
 
@@ -21,27 +21,38 @@ from nat.builder.builder import Builder
21
21
  from nat.builder.llm import LLMProviderInfo
22
22
  from nat.cli.register_workflow import register_llm_provider
23
23
  from nat.data_models.llm import LLMBaseConfig
24
+ from nat.data_models.optimizable import OptimizableField
25
+ from nat.data_models.optimizable import OptimizableMixin
26
+ from nat.data_models.optimizable import SearchSpace
24
27
  from nat.data_models.retry_mixin import RetryMixin
25
28
  from nat.data_models.temperature_mixin import TemperatureMixin
26
29
  from nat.data_models.thinking_mixin import ThinkingMixin
27
30
  from nat.data_models.top_p_mixin import TopPMixin
28
31
 
29
32
 
30
- class AWSBedrockModelConfig(LLMBaseConfig, RetryMixin, TemperatureMixin, TopPMixin, ThinkingMixin, name="aws_bedrock"):
33
+ class AWSBedrockModelConfig(LLMBaseConfig,
34
+ RetryMixin,
35
+ OptimizableMixin,
36
+ TemperatureMixin,
37
+ TopPMixin,
38
+ ThinkingMixin,
39
+ name="aws_bedrock"):
31
40
  """An AWS Bedrock llm provider to be used with an LLM client."""
32
41
 
33
- model_config = ConfigDict(protected_namespaces=())
42
+ model_config = ConfigDict(protected_namespaces=(), extra="allow")
34
43
 
35
44
  # Completion parameters
36
45
  model_name: str = Field(validation_alias=AliasChoices("model_name", "model"),
37
46
  serialization_alias="model",
38
47
  description="The model name for the hosted AWS Bedrock.")
39
- max_tokens: int | None = Field(default=1024, gt=0, description="Maximum number of tokens to generate.")
48
+ max_tokens: int = OptimizableField(default=300,
49
+ description="Maximum number of tokens to generate.",
50
+ space=SearchSpace(high=2176, low=128, step=512))
40
51
  context_size: int | None = Field(
41
52
  default=1024,
42
53
  gt=0,
43
54
  description="The maximum number of tokens available for input. This is only required for LlamaIndex. "
44
- "This field is ignored for Langchain.",
55
+ "This field is ignored for LangChain/LangGraph.",
45
56
  )
46
57
 
47
58
  # Client parameters
nat/llm/nim_llm.py CHANGED
@@ -22,23 +22,34 @@ from nat.builder.builder import Builder
22
22
  from nat.builder.llm import LLMProviderInfo
23
23
  from nat.cli.register_workflow import register_llm_provider
24
24
  from nat.data_models.llm import LLMBaseConfig
25
+ from nat.data_models.optimizable import OptimizableField
26
+ from nat.data_models.optimizable import OptimizableMixin
27
+ from nat.data_models.optimizable import SearchSpace
25
28
  from nat.data_models.retry_mixin import RetryMixin
26
29
  from nat.data_models.temperature_mixin import TemperatureMixin
27
30
  from nat.data_models.thinking_mixin import ThinkingMixin
28
31
  from nat.data_models.top_p_mixin import TopPMixin
29
32
 
30
33
 
31
- class NIMModelConfig(LLMBaseConfig, RetryMixin, TemperatureMixin, TopPMixin, ThinkingMixin, name="nim"):
34
+ class NIMModelConfig(LLMBaseConfig,
35
+ RetryMixin,
36
+ OptimizableMixin,
37
+ TemperatureMixin,
38
+ TopPMixin,
39
+ ThinkingMixin,
40
+ name="nim"):
32
41
  """An NVIDIA Inference Microservice (NIM) llm provider to be used with an LLM client."""
33
42
 
34
- model_config = ConfigDict(protected_namespaces=())
43
+ model_config = ConfigDict(protected_namespaces=(), extra="allow")
35
44
 
36
45
  api_key: str | None = Field(default=None, description="NVIDIA API key to interact with hosted NIM.")
37
46
  base_url: str | None = Field(default=None, description="Base url to the hosted NIM.")
38
47
  model_name: str = Field(validation_alias=AliasChoices("model_name", "model"),
39
48
  serialization_alias="model",
40
49
  description="The model name for the hosted NIM.")
41
- max_tokens: PositiveInt = Field(default=300, description="Maximum number of tokens to generate.")
50
+ max_tokens: PositiveInt = OptimizableField(default=300,
51
+ description="Maximum number of tokens to generate.",
52
+ space=SearchSpace(high=2176, low=128, step=512))
42
53
 
43
54
 
44
55
  @register_llm_provider(config_type=NIMModelConfig)
nat/llm/openai_llm.py CHANGED
@@ -21,13 +21,20 @@ from nat.builder.builder import Builder
21
21
  from nat.builder.llm import LLMProviderInfo
22
22
  from nat.cli.register_workflow import register_llm_provider
23
23
  from nat.data_models.llm import LLMBaseConfig
24
+ from nat.data_models.optimizable import OptimizableMixin
24
25
  from nat.data_models.retry_mixin import RetryMixin
25
26
  from nat.data_models.temperature_mixin import TemperatureMixin
26
27
  from nat.data_models.thinking_mixin import ThinkingMixin
27
28
  from nat.data_models.top_p_mixin import TopPMixin
28
29
 
29
30
 
30
- class OpenAIModelConfig(LLMBaseConfig, RetryMixin, TemperatureMixin, TopPMixin, ThinkingMixin, name="openai"):
31
+ class OpenAIModelConfig(LLMBaseConfig,
32
+ RetryMixin,
33
+ OptimizableMixin,
34
+ TemperatureMixin,
35
+ TopPMixin,
36
+ ThinkingMixin,
37
+ name="openai"):
31
38
  """An OpenAI LLM provider to be used with an LLM client."""
32
39
 
33
40
  model_config = ConfigDict(protected_namespaces=(), extra="allow")
@@ -53,6 +53,8 @@ class ProcessingExporter(Generic[PipelineInputT, PipelineOutputT], BaseExporter,
53
53
  - Configurable None filtering: processors returning None can drop items from pipeline
54
54
  - Automatic type validation before export
55
55
  """
56
+ # All ProcessingExporter instances automatically use this for signature checking
57
+ _signature_method = '_process_pipeline'
56
58
 
57
59
  def __init__(self, context_state: ContextState | None = None, drop_nones: bool = True):
58
60
  """Initialize the processing exporter.
@@ -294,8 +296,6 @@ class ProcessingExporter(Generic[PipelineInputT, PipelineOutputT], BaseExporter,
294
296
  self._check_processor_compatibility(predecessor,
295
297
  processor,
296
298
  "predecessor",
297
- predecessor.output_class,
298
- processor.input_class,
299
299
  str(predecessor.output_type),
300
300
  str(processor.input_type))
301
301
 
@@ -304,8 +304,6 @@ class ProcessingExporter(Generic[PipelineInputT, PipelineOutputT], BaseExporter,
304
304
  self._check_processor_compatibility(processor,
305
305
  successor,
306
306
  "successor",
307
- processor.output_class,
308
- successor.input_class,
309
307
  str(processor.output_type),
310
308
  str(successor.input_type))
311
309
 
@@ -313,34 +311,22 @@ class ProcessingExporter(Generic[PipelineInputT, PipelineOutputT], BaseExporter,
313
311
  source_processor: Processor,
314
312
  target_processor: Processor,
315
313
  relationship: str,
316
- source_class: type,
317
- target_class: type,
318
314
  source_type: str,
319
315
  target_type: str) -> None:
320
- """Check type compatibility between two processors.
316
+ """Check type compatibility between two processors using Pydantic validation.
321
317
 
322
318
  Args:
323
319
  source_processor (Processor): The processor providing output
324
320
  target_processor (Processor): The processor receiving input
325
321
  relationship (str): Description of relationship ("predecessor" or "successor")
326
- source_class (type): The output class of source processor
327
- target_class (type): The input class of target processor
328
322
  source_type (str): String representation of source type
329
323
  target_type (str): String representation of target type
330
324
  """
331
- try:
332
- if not issubclass(source_class, target_class):
333
- raise ValueError(f"Processor {target_processor.__class__.__name__} input type {target_type} "
334
- f"is not compatible with {relationship} {source_processor.__class__.__name__} "
335
- f"output type {source_type}")
336
- except TypeError:
337
- logger.warning(
338
- "Cannot use issubclass() for type compatibility check between "
339
- "%s (%s) and %s (%s). Skipping compatibility check.",
340
- source_processor.__class__.__name__,
341
- source_type,
342
- target_processor.__class__.__name__,
343
- target_type)
325
+ # Use Pydantic-based type compatibility checking
326
+ if not source_processor.is_output_compatible_with(target_processor.input_type):
327
+ raise ValueError(f"Processor {target_processor.__class__.__name__} input type {target_type} "
328
+ f"is not compatible with {relationship} {source_processor.__class__.__name__} "
329
+ f"output type {source_type}")
344
330
 
345
331
  async def _pre_start(self) -> None:
346
332
 
@@ -350,36 +336,21 @@ class ProcessingExporter(Generic[PipelineInputT, PipelineOutputT], BaseExporter,
350
336
  last_processor = self._processors[-1]
351
337
 
352
338
  # validate that the first processor's input type is compatible with the exporter's input type
353
- try:
354
- if not issubclass(self.input_class, first_processor.input_class):
355
- raise ValueError(f"Processor {first_processor.__class__.__name__} input type "
356
- f"{first_processor.input_type} is not compatible with the "
357
- f"{self.input_type} input type")
358
- except TypeError as e:
359
- # Handle cases where classes are generic types that can't be used with issubclass
360
- logger.warning(
361
- "Cannot validate type compatibility between %s (%s) "
362
- "and exporter (%s): %s. Skipping validation.",
363
- first_processor.__class__.__name__,
364
- first_processor.input_type,
365
- self.input_type,
366
- e)
367
-
339
+ if not first_processor.is_compatible_with_input(self.input_type):
340
+ logger.error("First processor %s input=%s incompatible with exporter input=%s",
341
+ first_processor.__class__.__name__,
342
+ first_processor.input_type,
343
+ self.input_type)
344
+ raise ValueError("First processor incompatible with exporter input")
368
345
  # Validate that the last processor's output type is compatible with the exporter's output type
369
- try:
370
- if not DecomposedType.is_type_compatible(last_processor.output_type, self.output_type):
371
- raise ValueError(f"Processor {last_processor.__class__.__name__} output type "
372
- f"{last_processor.output_type} is not compatible with the "
373
- f"{self.output_type} output type")
374
- except TypeError as e:
375
- # Handle cases where classes are generic types that can't be used with issubclass
376
- logger.warning(
377
- "Cannot validate type compatibility between %s (%s) "
378
- "and exporter (%s): %s. Skipping validation.",
379
- last_processor.__class__.__name__,
380
- last_processor.output_type,
381
- self.output_type,
382
- e)
346
+ # Use DecomposedType.is_type_compatible for the final export stage to allow batch compatibility
347
+ # This enables BatchingProcessor[T] -> Exporter[T] patterns where the exporter handles both T and list[T]
348
+ if not DecomposedType.is_type_compatible(last_processor.output_type, self.output_type):
349
+ logger.error("Last processor %s output=%s incompatible with exporter output=%s",
350
+ last_processor.__class__.__name__,
351
+ last_processor.output_type,
352
+ self.output_type)
353
+ raise ValueError("Last processor incompatible with exporter output")
383
354
 
384
355
  # Lock the pipeline to prevent further modifications
385
356
  self._pipeline_locked = True
@@ -432,12 +403,15 @@ class ProcessingExporter(Generic[PipelineInputT, PipelineOutputT], BaseExporter,
432
403
  await self.export_processed(processed_item)
433
404
  else:
434
405
  logger.debug("Skipping export of empty batch")
435
- elif isinstance(processed_item, self.output_class):
406
+ elif self.validate_output_type(processed_item):
436
407
  await self.export_processed(processed_item)
437
408
  else:
438
409
  if raise_on_invalid:
439
- raise ValueError(f"Processed item {processed_item} is not a valid output type. "
440
- f"Expected {self.output_class} or list[{self.output_class}]")
410
+ logger.error("Invalid processed item type for export: %s (expected %s or list[%s])",
411
+ type(processed_item),
412
+ self.output_type,
413
+ self.output_type)
414
+ raise ValueError("Invalid processed item type for export")
441
415
  logger.warning("Processed item %s is not a valid output type for export", processed_item)
442
416
 
443
417
  async def _continue_pipeline_after(self, source_processor: Processor, item: Any) -> None:
@@ -512,7 +486,7 @@ class ProcessingExporter(Generic[PipelineInputT, PipelineOutputT], BaseExporter,
512
486
  event (IntermediateStep): The event to be exported.
513
487
  """
514
488
  # Convert IntermediateStep to PipelineInputT and create export task
515
- if isinstance(event, self.input_class):
489
+ if self.validate_input_type(event):
516
490
  input_item: PipelineInputT = event # type: ignore
517
491
  coro = self._export_with_processing(input_item)
518
492
  self._create_export_task(coro)
@@ -1,4 +1,4 @@
1
- # SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -25,9 +25,10 @@ class RedactionConfigMixin(BaseModel):
25
25
  """
26
26
  redaction_enabled: bool = Field(default=False, description="Whether to enable redaction processing.")
27
27
  redaction_value: str = Field(default="[REDACTED]", description="Value to replace redacted attributes with.")
28
- redaction_attributes: list[str] = Field(default_factory=lambda: ["input.value", "output.value", "metadata"],
29
- description="Span attributes to redact when redaction is triggered.")
28
+ redaction_attributes: list[str] = Field(default_factory=lambda: ["input.value", "output.value", "nat.metadata"],
29
+ description="Attributes to redact when redaction is triggered.")
30
30
  force_redaction: bool = Field(default=False, description="Always redact regardless of other conditions.")
31
+ redaction_tag: str | None = Field(default=None, description="Tag to add to spans when redaction is triggered.")
31
32
 
32
33
 
33
34
  class HeaderRedactionConfigMixin(RedactionConfigMixin):
@@ -38,4 +39,4 @@ class HeaderRedactionConfigMixin(RedactionConfigMixin):
38
39
 
39
40
  Note: The callback function must be provided directly to the processor at runtime.
40
41
  """
41
- redaction_header: str = Field(default="x-redaction-key", description="Header to check for redaction decisions.")
42
+ redaction_headers: list[str] = Field(default_factory=list, description="Headers to check for redaction decisions.")
@@ -1,4 +1,4 @@
1
- # SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,6 +13,8 @@
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
15
 
16
+ import sys
17
+ from collections.abc import Mapping
16
18
  from enum import Enum
17
19
  from typing import Generic
18
20
  from typing import TypeVar
@@ -20,7 +22,17 @@ from typing import TypeVar
20
22
  from pydantic import BaseModel
21
23
  from pydantic import Field
22
24
 
23
- TagValueT = TypeVar("TagValueT")
25
+ if sys.version_info >= (3, 12):
26
+ from typing import TypedDict
27
+ else:
28
+ from typing_extensions import TypedDict
29
+
30
+ TagMappingT = TypeVar("TagMappingT", bound=Mapping)
31
+
32
+
33
+ class BaseTaggingConfigMixin(BaseModel, Generic[TagMappingT]):
34
+ """Base mixin for tagging spans."""
35
+ tags: TagMappingT | None = Field(default=None, description="Tags to add to the span.")
24
36
 
25
37
 
26
38
  class PrivacyLevel(str, Enum):
@@ -31,20 +43,20 @@ class PrivacyLevel(str, Enum):
31
43
  HIGH = "high"
32
44
 
33
45
 
34
- class TaggingConfigMixin(BaseModel, Generic[TagValueT]):
35
- """Generic mixin for tagging spans with typed values.
46
+ PrivacyTagSchema = TypedDict(
47
+ "PrivacyTagSchema",
48
+ {
49
+ "privacy.level": PrivacyLevel,
50
+ },
51
+ total=True,
52
+ )
36
53
 
37
- This mixin provides a flexible tagging system where both the tag key
38
- and value type can be customized for different use cases.
39
- """
40
- tag_key: str | None = Field(default=None, description="Key to use when tagging traces.")
41
- tag_value: TagValueT | None = Field(default=None, description="Value to tag the traces with.")
42
54
 
55
+ class PrivacyTaggingConfigMixin(BaseTaggingConfigMixin[PrivacyTagSchema]):
56
+ """Mixin for privacy level tagging on spans."""
57
+ pass
43
58
 
44
- class PrivacyTaggingConfigMixin(TaggingConfigMixin[PrivacyLevel]):
45
- """Mixin for privacy level tagging on spans.
46
59
 
47
- Specializes TaggingConfigMixin to work with PrivacyLevel enum values,
48
- providing a typed interface for privacy-related span tagging.
49
- """
60
+ class CustomTaggingConfigMixin(BaseTaggingConfigMixin[dict[str, str]]):
61
+ """Mixin for string key-value tagging on spans."""
50
62
  pass