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
@@ -0,0 +1,125 @@
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 logging
17
+ from abc import abstractmethod
18
+ from collections.abc import Callable
19
+ from typing import Any
20
+ from typing import TypeVar
21
+
22
+ from nat.observability.processor.redaction.redaction_processor import RedactionContext
23
+ from nat.observability.processor.redaction.redaction_processor import RedactionContextState
24
+ from nat.observability.processor.redaction.redaction_processor import RedactionInputT
25
+ from nat.observability.processor.redaction.redaction_processor import RedactionProcessor
26
+ from nat.utils.type_utils import override
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ # Type variable for the data type extracted from context
31
+ RedactionDataT = TypeVar('RedactionDataT')
32
+
33
+
34
+ class ContextualRedactionProcessor(RedactionProcessor[RedactionInputT, RedactionDataT]):
35
+ """Generic processor with context-aware caching for any data type.
36
+
37
+ Provides a framework for redaction processors that need to:
38
+ - Extract data from the request context (headers, cookies, query params, etc.)
39
+ - Execute callbacks to determine redaction decisions
40
+ - Cache results within the request context to avoid redundant callback executions
41
+ - Handle race conditions with atomic operations
42
+
43
+ This class handles all the generic caching, context management, and callback
44
+ execution logic. Subclasses only need to implement data extraction and validation.
45
+
46
+ Args:
47
+ callback: Callable that determines if redaction should occur based on extracted data
48
+ enabled: Whether the processor is enabled
49
+ force_redact: If True, always redact regardless of data checks
50
+ redaction_value: The value to replace redacted attributes with
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ callback: Callable[..., Any],
56
+ enabled: bool,
57
+ force_redact: bool,
58
+ redaction_value: str,
59
+ ):
60
+ self.callback = callback
61
+ self.enabled = enabled
62
+ self.force_redact = force_redact
63
+ self.redaction_value = redaction_value
64
+ self._redaction_context = RedactionContext(RedactionContextState())
65
+
66
+ @abstractmethod
67
+ def extract_data_from_context(self) -> RedactionDataT | None:
68
+ """Extract the relevant data from the context for redaction decision.
69
+
70
+ This method must be implemented by subclasses to extract their specific
71
+ data type (headers, cookies, query params, etc.) from the request context
72
+
73
+ Returns:
74
+ RedactionDataT | None: The extracted data, or None if no relevant data found
75
+ """
76
+ pass
77
+
78
+ @abstractmethod
79
+ def validate_data(self, data: RedactionDataT) -> bool:
80
+ """Validate that the extracted data is suitable for callback execution.
81
+
82
+ This method allows subclasses to implement their own validation logic
83
+ (e.g., checking if headers exist, if cookies are not empty, etc.).
84
+
85
+ Args:
86
+ data (RedactionDataT): The extracted data to validate
87
+
88
+ Returns:
89
+ bool: True if the data is valid for callback execution, False otherwise
90
+ """
91
+ pass
92
+
93
+ @override
94
+ async def should_redact(self, item: RedactionInputT) -> bool:
95
+ """Determine if this span should be redacted based on extracted data.
96
+
97
+ Extracts the relevant data from the context, validates it, and passes it to the
98
+ callback function to determine if redaction should occur. Results are cached
99
+ within the request context to avoid redundant callback executions.
100
+
101
+ Args:
102
+ item (RedactionInputT): The item to check
103
+
104
+ Returns:
105
+ bool: True if the span should be redacted, False otherwise
106
+ """
107
+ # If force_redact is enabled, always redact regardless of other conditions
108
+ if self.force_redact:
109
+ return True
110
+
111
+ if not self.enabled:
112
+ return False
113
+
114
+ # Extract data using subclass implementation
115
+ data = self.extract_data_from_context()
116
+ if data is None:
117
+ return False
118
+
119
+ # Validate data using subclass implementation
120
+ if not self.validate_data(data):
121
+ return False
122
+
123
+ # Use the generic caching framework for callback execution
124
+ async with self._redaction_context.redaction_manager() as manager:
125
+ return await manager.redaction_check(self.callback, data)
@@ -0,0 +1,66 @@
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
+ from collections.abc import Callable
17
+ from typing import Any
18
+
19
+ from nat.data_models.span import Span
20
+ from nat.observability.processor.redaction.contextual_redaction_processor import ContextualRedactionProcessor
21
+ from nat.observability.processor.redaction.redaction_processor import RedactionDataT
22
+ from nat.utils.type_utils import override
23
+
24
+
25
+ class ContextualSpanRedactionProcessor(ContextualRedactionProcessor[Span, RedactionDataT]):
26
+ """Processor that redacts the Span based on the Span attributes.
27
+
28
+ Args:
29
+ attributes: List of span attribute keys to redact
30
+ callback: Callable that determines if redaction should occur
31
+ enabled: Whether the processor is enabled
32
+ force_redact: If True, always redact regardless of callback
33
+ redaction_value: The value to replace redacted attributes with
34
+ """
35
+
36
+ def __init__(self,
37
+ attributes: list[str],
38
+ callback: Callable[..., Any],
39
+ enabled: bool,
40
+ force_redact: bool,
41
+ redaction_value: str,
42
+ redaction_tag: str | None = None):
43
+ super().__init__(callback=callback, enabled=enabled, force_redact=force_redact, redaction_value=redaction_value)
44
+ self.attributes = attributes
45
+ self.redaction_tag = redaction_tag
46
+
47
+ @override
48
+ async def redact_item(self, item: Span) -> Span:
49
+ """Redact specified attributes in the span.
50
+
51
+ Replaces the values of configured attributes with the redaction value.
52
+
53
+ Args:
54
+ item (Span): The span to redact
55
+
56
+ Returns:
57
+ Span: The span with redacted attributes
58
+ """
59
+ for key in self.attributes:
60
+ if key in item.attributes:
61
+ item.set_attribute(key, self.redaction_value)
62
+
63
+ if self.redaction_tag:
64
+ item.set_attribute(self.redaction_tag, True)
65
+
66
+ return item
@@ -0,0 +1,177 @@
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 logging
17
+ from abc import abstractmethod
18
+ from collections.abc import AsyncGenerator
19
+ from collections.abc import Callable
20
+ from contextlib import asynccontextmanager
21
+ from contextvars import ContextVar
22
+ from dataclasses import dataclass
23
+ from dataclasses import field
24
+ from typing import Any
25
+ from typing import Generic
26
+ from typing import TypeVar
27
+
28
+ from nat.observability.processor.processor import Processor
29
+ from nat.utils.callable_utils import ainvoke_any
30
+ from nat.utils.type_utils import override
31
+
32
+ RedactionInputT = TypeVar('RedactionInputT')
33
+ RedactionDataT = TypeVar('RedactionDataT')
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+
38
+ class RedactionProcessor(Processor[RedactionInputT, RedactionInputT], Generic[RedactionInputT, RedactionDataT]):
39
+ """Abstract base class for redaction processors."""
40
+
41
+ @abstractmethod
42
+ async def should_redact(self, item: RedactionInputT) -> bool:
43
+ """Determine if this item should be redacted.
44
+
45
+ Args:
46
+ item (RedactionInputT): The item to check.
47
+
48
+ Returns:
49
+ bool: True if the item should be redacted, False otherwise.
50
+ """
51
+ pass
52
+
53
+ @abstractmethod
54
+ async def redact_item(self, item: RedactionInputT) -> RedactionInputT:
55
+ """Redact the item.
56
+
57
+ Args:
58
+ item (RedactionInputT): The item to redact.
59
+
60
+ Returns:
61
+ RedactionInputT: The redacted item.
62
+ """
63
+ pass
64
+
65
+ @override
66
+ async def process(self, item: RedactionInputT) -> RedactionInputT:
67
+ """Perform redaction on the item if it should be redacted.
68
+
69
+ Args:
70
+ item (RedactionInputT): The item to process.
71
+
72
+ Returns:
73
+ RedactionInputT: The processed item.
74
+ """
75
+ if await self.should_redact(item):
76
+ return await self.redact_item(item)
77
+ return item
78
+
79
+
80
+ @dataclass
81
+ class RedactionContextState:
82
+ """Generic context state for redaction results.
83
+
84
+ Stores the redaction result in a context variable to avoid redundant
85
+ callback executions within the same request context.
86
+ """
87
+
88
+ redaction_result: ContextVar[bool
89
+ | None] = field(default_factory=lambda: ContextVar("redaction_result", default=None))
90
+
91
+
92
+ class RedactionManager(Generic[RedactionDataT]):
93
+ """Generic manager for atomic redaction operations.
94
+
95
+ Handles state mutations and ensures atomic callback execution
96
+ with proper result caching within a request context.
97
+
98
+ Args:
99
+ RedactionDataT: The type of data being processed for redaction decisions.
100
+ """
101
+
102
+ def __init__(self, context_state: RedactionContextState):
103
+ self._context_state = context_state
104
+
105
+ def set_redaction_result(self, result: bool) -> None:
106
+ """Set the redaction result in the context.
107
+
108
+ Args:
109
+ result (bool): The redaction result to cache.
110
+ """
111
+ self._context_state.redaction_result.set(result)
112
+
113
+ def clear_redaction_result(self) -> None:
114
+ """Clear the cached redaction result from the context."""
115
+ self._context_state.redaction_result.set(None)
116
+
117
+ async def redaction_check(self, callback: Callable[..., Any], data: RedactionDataT) -> bool:
118
+ """Execute redaction callback with atomic result caching.
119
+
120
+ Checks for existing cached results first, then executes the callback
121
+ and caches the result atomically. Since data is static per request,
122
+ subsequent calls within the same context return the cached result.
123
+
124
+ Supports sync/async functions, generators, and async generators.
125
+
126
+ Args:
127
+ callback (Callable[..., Any]): The callback to execute (sync/async function, generator, etc.).
128
+ data (RedactionDataT): The data to pass to the callback for redaction decision.
129
+
130
+ Returns:
131
+ bool: True if the item should be redacted, False otherwise.
132
+ """
133
+ # Check if we already have a result for this context
134
+ existing_result = self._context_state.redaction_result.get()
135
+ if existing_result is not None:
136
+ return existing_result
137
+
138
+ # Execute callback and cache result
139
+ result_value = await ainvoke_any(callback, data)
140
+ result = bool(result_value)
141
+ self.set_redaction_result(result)
142
+ return result
143
+
144
+
145
+ class RedactionContext(Generic[RedactionDataT]):
146
+ """Generic context provider for redaction operations.
147
+
148
+ Provides read-only access to redaction state and manages the
149
+ RedactionManager lifecycle through async context managers.
150
+
151
+ Args:
152
+ RedactionDataT: The type of data being processed for redaction decisions.
153
+ """
154
+
155
+ def __init__(self, context: RedactionContextState):
156
+ self._context_state: RedactionContextState = context
157
+
158
+ @property
159
+ def redaction_result(self) -> bool | None:
160
+ """Get the current redaction result from context.
161
+
162
+ Returns:
163
+ bool | None: The cached redaction result, or None if not set.
164
+ """
165
+ return self._context_state.redaction_result.get()
166
+
167
+ @asynccontextmanager
168
+ async def redaction_manager(self) -> AsyncGenerator[RedactionManager[RedactionDataT], None]:
169
+ """Provide a redaction manager within an async context.
170
+
171
+ Creates and yields a RedactionManager instance for atomic
172
+ redaction operations within the current context.
173
+
174
+ Yields:
175
+ RedactionManager[RedactionDataT]: Manager instance for redaction operations.
176
+ """
177
+ yield RedactionManager(self._context_state)
@@ -0,0 +1,92 @@
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 logging
17
+ from collections.abc import Callable
18
+ from typing import Any
19
+
20
+ from starlette.datastructures import Headers
21
+
22
+ from nat.builder.context import Context
23
+ from nat.observability.processor.redaction.contextual_span_redaction_processor import ContextualSpanRedactionProcessor
24
+ from nat.utils.type_utils import override
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class SpanHeaderRedactionProcessor(ContextualSpanRedactionProcessor[dict[str, Any]]):
30
+ """Processor that redacts the Span based on multiple headers and callback logic.
31
+
32
+ Uses context-scoped atomic updates to avoid redundant callback executions within a single context.
33
+ Since headers are static per request, the callback result is cached for the entire context using
34
+ an asynccontextmanager to ensure atomic operations.
35
+
36
+ Args:
37
+ headers: List of header keys to extract and pass to the callback
38
+ attributes: List of Span attribute keys to redact
39
+ callback: Callable that determines if redaction should occur
40
+ enabled: Whether the processor is enabled (default: True)
41
+ force_redact: If True, always redact regardless of header checks (default: False)
42
+ redaction_value: The value to replace redacted attributes with (default: "[REDACTED]")
43
+ """
44
+
45
+ def __init__(self,
46
+ headers: list[str],
47
+ attributes: list[str],
48
+ callback: Callable[..., Any],
49
+ enabled: bool = True,
50
+ force_redact: bool = False,
51
+ redaction_value: str = "[REDACTED]",
52
+ redaction_tag: str | None = None):
53
+ # Initialize the base class with common parameters
54
+ super().__init__(attributes=attributes,
55
+ callback=callback,
56
+ enabled=enabled,
57
+ force_redact=force_redact,
58
+ redaction_value=redaction_value,
59
+ redaction_tag=redaction_tag)
60
+ # Store header-specific configuration
61
+ self.headers = headers
62
+
63
+ @override
64
+ def extract_data_from_context(self) -> dict[str, Any] | None:
65
+ """Extract header data from the context.
66
+
67
+ Returns:
68
+ dict[str, Any] | None: Dictionary of header names to values, or None if no headers.
69
+ """
70
+
71
+ context = Context.get()
72
+ headers: Headers | None = context.metadata.headers
73
+
74
+ if headers is None or not self.headers:
75
+ return None
76
+
77
+ header_map: dict[str, Any] = {header: headers.get(header, None) for header in self.headers}
78
+
79
+ return header_map
80
+
81
+ @override
82
+ def validate_data(self, data: dict[str, Any]) -> bool:
83
+ """Validate that the extracted headers are suitable for callback execution.
84
+
85
+ Args:
86
+ data (dict[str, Any]): The extracted header dictionary.
87
+
88
+ Returns:
89
+ bool: True if headers exist and are not all None, False otherwise.
90
+ """
91
+ # Skip callback if no headers were found (all None values)
92
+ return bool(data) and not all(value is None for value in data.values())
@@ -15,6 +15,8 @@
15
15
 
16
16
  import logging
17
17
  import os
18
+ from collections.abc import Mapping
19
+ from enum import Enum
18
20
 
19
21
  from nat.data_models.span import Span
20
22
  from nat.observability.processor.processor import Processor
@@ -24,22 +26,20 @@ logger = logging.getLogger(__name__)
24
26
 
25
27
 
26
28
  class SpanTaggingProcessor(Processor[Span, Span]):
27
- """Processor that tags spans with key-value metadata attributes.
29
+ """Processor that tags spans with multiple key-value metadata attributes.
28
30
 
29
31
  This processor adds custom tags to spans by setting attributes with a configurable prefix.
30
- Tags are only applied when both tag_key and tag_value are provided. The processor uses
32
+ Tags are applied for each key-value pair in the tags dictionary. The processor uses
31
33
  a span prefix (configurable via NAT_SPAN_PREFIX environment variable) to namespace
32
34
  the tag attributes.
33
35
 
34
- Args:
35
- tag_key: The key name for the tag to add to spans.
36
- tag_value: The value for the tag to add to spans.
37
- span_prefix: The prefix to use for tag attributes (default: from NAT_SPAN_PREFIX env var or "nat").
36
+ Args:
37
+ tags: Mapping of tag keys to their values. Values can be enums (converted to strings) or strings
38
+ span_prefix: The prefix to use for tag attributes (default: from NAT_SPAN_PREFIX env var or "nat")
38
39
  """
39
40
 
40
- def __init__(self, tag_key: str | None = None, tag_value: str | None = None, span_prefix: str | None = None):
41
- self.tag_key = tag_key
42
- self.tag_value = tag_value
41
+ def __init__(self, tags: Mapping[str, Enum | str] | None = None, span_prefix: str | None = None):
42
+ self.tags = tags or {}
43
43
 
44
44
  if span_prefix is None:
45
45
  span_prefix = os.getenv("NAT_SPAN_PREFIX", "nat").strip() or "nat"
@@ -48,14 +48,21 @@ class SpanTaggingProcessor(Processor[Span, Span]):
48
48
 
49
49
  @override
50
50
  async def process(self, item: Span) -> Span:
51
- """Tag the span with a tag if both tag_key and tag_value are provided.
51
+ """Tag the span with all configured tags.
52
52
 
53
53
  Args:
54
- item (Span): The span to tag.
54
+ item (Span): The span to tag
55
55
 
56
56
  Returns:
57
- Span: The tagged span.
57
+ Span: The tagged span with all configured tags applied
58
58
  """
59
- if self.tag_key and self.tag_value:
60
- item.set_attribute(f"{self._span_prefix}.{self.tag_key}", self.tag_value)
59
+ for tag_key, tag_value in self.tags.items():
60
+ key = str(tag_key).strip()
61
+ if not key:
62
+ continue
63
+ value_str = str(tag_value.value) if isinstance(tag_value, Enum) else str(tag_value)
64
+ if value_str == "":
65
+ continue
66
+ item.set_attribute(f"{self._span_prefix}.{key}", value_str)
67
+
61
68
  return item
@@ -51,16 +51,19 @@ def set_framework_profiler_handler(
51
51
  @asynccontextmanager
52
52
  async def wrapper(workflow_config, builder):
53
53
 
54
- if LLMFrameworkEnum.LANGCHAIN in frameworks and not _library_instrumented["langchain"]:
55
- from langchain_core.tracers.context import register_configure_hook
56
-
54
+ if LLMFrameworkEnum.LANGCHAIN in frameworks:
55
+ # Always set a fresh handler in the current context so callbacks
56
+ # route to the active run. Only register the hook once globally.
57
57
  from nat.profiler.callbacks.langchain_callback_handler import LangchainProfilerHandler
58
58
 
59
59
  handler = LangchainProfilerHandler()
60
60
  callback_handler_var.set(handler)
61
- register_configure_hook(callback_handler_var, inheritable=True)
62
- _library_instrumented["langchain"] = True
63
- logger.debug("Langchain callback handler registered")
61
+
62
+ if not _library_instrumented["langchain"]:
63
+ from langchain_core.tracers.context import register_configure_hook
64
+ register_configure_hook(callback_handler_var, inheritable=True)
65
+ _library_instrumented["langchain"] = True
66
+ logger.debug("LangChain/LangGraph callback hook registered")
64
67
 
65
68
  if LLMFrameworkEnum.LLAMA_INDEX in frameworks:
66
69
  from llama_index.core import Settings
File without changes
@@ -0,0 +1,93 @@
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 logging
17
+ from typing import get_args
18
+ from typing import get_origin
19
+
20
+ from pydantic import BaseModel
21
+
22
+ from nat.data_models.optimizable import SearchSpace
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ def walk_optimizables(obj: BaseModel, path: str = "") -> dict[str, SearchSpace]:
28
+ """
29
+ Recursively build ``{flattened.path: SearchSpace}`` for every optimizable
30
+ field inside *obj*.
31
+
32
+ * Honors ``optimizable_params`` on any model that mixes in
33
+ ``OptimizableMixin`` – only listed fields are kept.
34
+ * If a model contains optimizable fields **but** omits
35
+ ``optimizable_params``, we emit a warning and skip them.
36
+ """
37
+ spaces: dict[str, SearchSpace] = {}
38
+
39
+ allowed_params_raw = getattr(obj, "optimizable_params", None)
40
+ allowed_params = set(allowed_params_raw) if allowed_params_raw is not None else None
41
+ overrides = getattr(obj, "search_space", {}) or {}
42
+ has_optimizable_flag = False
43
+
44
+ for name, fld in obj.model_fields.items():
45
+ full = f"{path}.{name}" if path else name
46
+ extra = fld.json_schema_extra or {}
47
+
48
+ is_field_optimizable = extra.get("optimizable", False) or name in overrides
49
+ has_optimizable_flag = has_optimizable_flag or is_field_optimizable
50
+
51
+ # honour allow-list
52
+ if allowed_params is not None and name not in allowed_params:
53
+ continue
54
+
55
+ # 1. plain optimizable field or override from config
56
+ if is_field_optimizable:
57
+ space = overrides.get(name, extra.get("search_space"))
58
+ if space is None:
59
+ logger.error(
60
+ "Field %s is marked optimizable but no search space was provided.",
61
+ full,
62
+ )
63
+ raise ValueError(f"Field {full} is marked optimizable but no search space was provided")
64
+ spaces[full] = space
65
+
66
+ value = getattr(obj, name, None)
67
+
68
+ # 2. nested BaseModel
69
+ if isinstance(value, BaseModel):
70
+ spaces.update(walk_optimizables(value, full))
71
+
72
+ # 3. dict[str, BaseModel] container
73
+ elif isinstance(value, dict):
74
+ for key, subval in value.items():
75
+ if isinstance(subval, BaseModel):
76
+ spaces.update(walk_optimizables(subval, f"{full}.{key}"))
77
+
78
+ # 4. static-type fallback for class-level annotations
79
+ elif isinstance(obj, type):
80
+ ann = fld.annotation
81
+ if get_origin(ann) in (dict, dict):
82
+ _, val_t = get_args(ann) or (None, None)
83
+ if isinstance(val_t, type) and issubclass(val_t, BaseModel):
84
+ if allowed_params is None or name in allowed_params:
85
+ spaces[f"{full}.*"] = SearchSpace(low=None, high=None) # sentinel
86
+
87
+ if allowed_params is None and has_optimizable_flag:
88
+ logger.warning(
89
+ "Model %s contains optimizable fields but no `optimizable_params` "
90
+ "were defined; these fields will be ignored.",
91
+ obj.__class__.__name__,
92
+ )
93
+ return spaces