nvidia-nat 1.3.0a20250910__py3-none-any.whl → 1.4.0a20251112__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 (213) hide show
  1. nat/agent/base.py +13 -8
  2. nat/agent/prompt_optimizer/prompt.py +68 -0
  3. nat/agent/prompt_optimizer/register.py +149 -0
  4. nat/agent/react_agent/agent.py +6 -5
  5. nat/agent/react_agent/register.py +49 -39
  6. nat/agent/reasoning_agent/reasoning_agent.py +17 -15
  7. nat/agent/register.py +2 -0
  8. nat/agent/responses_api_agent/__init__.py +14 -0
  9. nat/agent/responses_api_agent/register.py +126 -0
  10. nat/agent/rewoo_agent/agent.py +304 -117
  11. nat/agent/rewoo_agent/prompt.py +19 -22
  12. nat/agent/rewoo_agent/register.py +51 -38
  13. nat/agent/tool_calling_agent/agent.py +75 -17
  14. nat/agent/tool_calling_agent/register.py +46 -23
  15. nat/authentication/api_key/api_key_auth_provider.py +6 -11
  16. nat/authentication/api_key/api_key_auth_provider_config.py +8 -5
  17. nat/authentication/credential_validator/__init__.py +14 -0
  18. nat/authentication/credential_validator/bearer_token_validator.py +557 -0
  19. nat/authentication/http_basic_auth/http_basic_auth_provider.py +1 -1
  20. nat/authentication/interfaces.py +5 -2
  21. nat/authentication/oauth2/oauth2_auth_code_flow_provider.py +69 -36
  22. nat/authentication/oauth2/oauth2_auth_code_flow_provider_config.py +2 -1
  23. nat/authentication/oauth2/oauth2_resource_server_config.py +125 -0
  24. nat/builder/builder.py +55 -23
  25. nat/builder/component_utils.py +9 -5
  26. nat/builder/context.py +54 -15
  27. nat/builder/eval_builder.py +14 -9
  28. nat/builder/framework_enum.py +1 -0
  29. nat/builder/front_end.py +1 -1
  30. nat/builder/function.py +370 -0
  31. nat/builder/function_info.py +1 -1
  32. nat/builder/intermediate_step_manager.py +38 -2
  33. nat/builder/workflow.py +5 -0
  34. nat/builder/workflow_builder.py +306 -54
  35. nat/cli/cli_utils/config_override.py +1 -1
  36. nat/cli/commands/info/info.py +16 -6
  37. nat/cli/commands/mcp/__init__.py +14 -0
  38. nat/cli/commands/mcp/mcp.py +986 -0
  39. nat/cli/commands/optimize.py +90 -0
  40. nat/cli/commands/start.py +1 -1
  41. nat/cli/commands/workflow/templates/config.yml.j2 +14 -13
  42. nat/cli/commands/workflow/templates/register.py.j2 +2 -2
  43. nat/cli/commands/workflow/templates/workflow.py.j2 +35 -21
  44. nat/cli/commands/workflow/workflow_commands.py +60 -18
  45. nat/cli/entrypoint.py +15 -11
  46. nat/cli/main.py +3 -0
  47. nat/cli/register_workflow.py +38 -4
  48. nat/cli/type_registry.py +72 -1
  49. nat/control_flow/__init__.py +0 -0
  50. nat/control_flow/register.py +20 -0
  51. nat/control_flow/router_agent/__init__.py +0 -0
  52. nat/control_flow/router_agent/agent.py +329 -0
  53. nat/control_flow/router_agent/prompt.py +48 -0
  54. nat/control_flow/router_agent/register.py +91 -0
  55. nat/control_flow/sequential_executor.py +166 -0
  56. nat/data_models/agent.py +34 -0
  57. nat/data_models/api_server.py +199 -69
  58. nat/data_models/authentication.py +23 -9
  59. nat/data_models/common.py +47 -0
  60. nat/data_models/component.py +2 -0
  61. nat/data_models/component_ref.py +11 -0
  62. nat/data_models/config.py +41 -17
  63. nat/data_models/dataset_handler.py +4 -3
  64. nat/data_models/function.py +34 -0
  65. nat/data_models/function_dependencies.py +8 -0
  66. nat/data_models/intermediate_step.py +9 -1
  67. nat/data_models/llm.py +15 -1
  68. nat/data_models/openai_mcp.py +46 -0
  69. nat/data_models/optimizable.py +208 -0
  70. nat/data_models/optimizer.py +161 -0
  71. nat/data_models/span.py +41 -3
  72. nat/data_models/thinking_mixin.py +2 -2
  73. nat/embedder/azure_openai_embedder.py +2 -1
  74. nat/embedder/nim_embedder.py +3 -2
  75. nat/embedder/openai_embedder.py +3 -2
  76. nat/eval/config.py +1 -1
  77. nat/eval/dataset_handler/dataset_downloader.py +3 -2
  78. nat/eval/dataset_handler/dataset_filter.py +34 -2
  79. nat/eval/evaluate.py +10 -3
  80. nat/eval/evaluator/base_evaluator.py +1 -1
  81. nat/eval/rag_evaluator/evaluate.py +7 -4
  82. nat/eval/register.py +4 -0
  83. nat/eval/runtime_evaluator/__init__.py +14 -0
  84. nat/eval/runtime_evaluator/evaluate.py +123 -0
  85. nat/eval/runtime_evaluator/register.py +100 -0
  86. nat/eval/swe_bench_evaluator/evaluate.py +1 -1
  87. nat/eval/trajectory_evaluator/register.py +1 -1
  88. nat/eval/tunable_rag_evaluator/evaluate.py +1 -1
  89. nat/eval/usage_stats.py +2 -0
  90. nat/eval/utils/output_uploader.py +3 -2
  91. nat/eval/utils/weave_eval.py +17 -3
  92. nat/experimental/decorators/experimental_warning_decorator.py +27 -7
  93. nat/experimental/test_time_compute/functions/execute_score_select_function.py +1 -1
  94. nat/experimental/test_time_compute/functions/plan_select_execute_function.py +7 -3
  95. nat/experimental/test_time_compute/functions/ttc_tool_orchestration_function.py +1 -1
  96. nat/experimental/test_time_compute/functions/ttc_tool_wrapper_function.py +3 -3
  97. nat/experimental/test_time_compute/models/strategy_base.py +2 -2
  98. nat/experimental/test_time_compute/selection/llm_based_output_merging_selector.py +1 -1
  99. nat/front_ends/console/authentication_flow_handler.py +82 -30
  100. nat/front_ends/console/console_front_end_plugin.py +19 -7
  101. nat/front_ends/fastapi/auth_flow_handlers/http_flow_handler.py +1 -1
  102. nat/front_ends/fastapi/auth_flow_handlers/websocket_flow_handler.py +52 -17
  103. nat/front_ends/fastapi/dask_client_mixin.py +65 -0
  104. nat/front_ends/fastapi/fastapi_front_end_config.py +25 -3
  105. nat/front_ends/fastapi/fastapi_front_end_plugin.py +140 -3
  106. nat/front_ends/fastapi/fastapi_front_end_plugin_worker.py +445 -265
  107. nat/front_ends/fastapi/job_store.py +518 -99
  108. nat/front_ends/fastapi/main.py +11 -19
  109. nat/front_ends/fastapi/message_handler.py +69 -44
  110. nat/front_ends/fastapi/message_validator.py +8 -7
  111. nat/front_ends/fastapi/utils.py +57 -0
  112. nat/front_ends/mcp/introspection_token_verifier.py +73 -0
  113. nat/front_ends/mcp/mcp_front_end_config.py +71 -3
  114. nat/front_ends/mcp/mcp_front_end_plugin.py +85 -21
  115. nat/front_ends/mcp/mcp_front_end_plugin_worker.py +248 -29
  116. nat/front_ends/mcp/memory_profiler.py +320 -0
  117. nat/front_ends/mcp/tool_converter.py +78 -25
  118. nat/front_ends/simple_base/simple_front_end_plugin_base.py +3 -1
  119. nat/llm/aws_bedrock_llm.py +21 -8
  120. nat/llm/azure_openai_llm.py +14 -5
  121. nat/llm/litellm_llm.py +80 -0
  122. nat/llm/nim_llm.py +23 -9
  123. nat/llm/openai_llm.py +19 -7
  124. nat/llm/register.py +4 -0
  125. nat/llm/utils/thinking.py +1 -1
  126. nat/observability/exporter/base_exporter.py +1 -1
  127. nat/observability/exporter/processing_exporter.py +29 -55
  128. nat/observability/exporter/span_exporter.py +43 -15
  129. nat/observability/exporter_manager.py +2 -2
  130. nat/observability/mixin/redaction_config_mixin.py +5 -4
  131. nat/observability/mixin/tagging_config_mixin.py +26 -14
  132. nat/observability/mixin/type_introspection_mixin.py +420 -107
  133. nat/observability/processor/batching_processor.py +1 -1
  134. nat/observability/processor/processor.py +3 -0
  135. nat/observability/processor/redaction/__init__.py +24 -0
  136. nat/observability/processor/redaction/contextual_redaction_processor.py +125 -0
  137. nat/observability/processor/redaction/contextual_span_redaction_processor.py +66 -0
  138. nat/observability/processor/redaction/redaction_processor.py +177 -0
  139. nat/observability/processor/redaction/span_header_redaction_processor.py +92 -0
  140. nat/observability/processor/span_tagging_processor.py +21 -14
  141. nat/observability/register.py +16 -0
  142. nat/profiler/callbacks/langchain_callback_handler.py +32 -7
  143. nat/profiler/callbacks/llama_index_callback_handler.py +36 -2
  144. nat/profiler/callbacks/token_usage_base_model.py +2 -0
  145. nat/profiler/decorators/framework_wrapper.py +61 -9
  146. nat/profiler/decorators/function_tracking.py +35 -3
  147. nat/profiler/forecasting/models/linear_model.py +1 -1
  148. nat/profiler/forecasting/models/random_forest_regressor.py +1 -1
  149. nat/profiler/inference_optimization/bottleneck_analysis/nested_stack_analysis.py +1 -1
  150. nat/profiler/inference_optimization/experimental/prefix_span_analysis.py +1 -1
  151. nat/profiler/parameter_optimization/__init__.py +0 -0
  152. nat/profiler/parameter_optimization/optimizable_utils.py +93 -0
  153. nat/profiler/parameter_optimization/optimizer_runtime.py +67 -0
  154. nat/profiler/parameter_optimization/parameter_optimizer.py +189 -0
  155. nat/profiler/parameter_optimization/parameter_selection.py +107 -0
  156. nat/profiler/parameter_optimization/pareto_visualizer.py +460 -0
  157. nat/profiler/parameter_optimization/prompt_optimizer.py +384 -0
  158. nat/profiler/parameter_optimization/update_helpers.py +66 -0
  159. nat/profiler/utils.py +3 -1
  160. nat/registry_handlers/pypi/register_pypi.py +5 -3
  161. nat/registry_handlers/rest/register_rest.py +5 -3
  162. nat/retriever/milvus/retriever.py +1 -1
  163. nat/retriever/nemo_retriever/register.py +2 -1
  164. nat/runtime/loader.py +1 -1
  165. nat/runtime/runner.py +111 -6
  166. nat/runtime/session.py +49 -3
  167. nat/settings/global_settings.py +2 -2
  168. nat/tool/chat_completion.py +4 -1
  169. nat/tool/code_execution/code_sandbox.py +3 -6
  170. nat/tool/code_execution/local_sandbox/Dockerfile.sandbox +19 -32
  171. nat/tool/code_execution/local_sandbox/local_sandbox_server.py +6 -1
  172. nat/tool/code_execution/local_sandbox/sandbox.requirements.txt +2 -0
  173. nat/tool/code_execution/local_sandbox/start_local_sandbox.sh +10 -4
  174. nat/tool/datetime_tools.py +1 -1
  175. nat/tool/github_tools.py +450 -0
  176. nat/tool/memory_tools/add_memory_tool.py +3 -3
  177. nat/tool/memory_tools/delete_memory_tool.py +3 -4
  178. nat/tool/memory_tools/get_memory_tool.py +4 -4
  179. nat/tool/register.py +2 -7
  180. nat/tool/server_tools.py +15 -2
  181. nat/utils/__init__.py +76 -0
  182. nat/utils/callable_utils.py +70 -0
  183. nat/utils/data_models/schema_validator.py +1 -1
  184. nat/utils/decorators.py +210 -0
  185. nat/utils/exception_handlers/automatic_retries.py +278 -72
  186. nat/utils/io/yaml_tools.py +73 -3
  187. nat/utils/log_levels.py +25 -0
  188. nat/utils/responses_api.py +26 -0
  189. nat/utils/string_utils.py +16 -0
  190. nat/utils/type_converter.py +12 -3
  191. nat/utils/type_utils.py +6 -2
  192. nvidia_nat-1.4.0a20251112.dist-info/METADATA +197 -0
  193. {nvidia_nat-1.3.0a20250910.dist-info → nvidia_nat-1.4.0a20251112.dist-info}/RECORD +199 -165
  194. {nvidia_nat-1.3.0a20250910.dist-info → nvidia_nat-1.4.0a20251112.dist-info}/entry_points.txt +1 -0
  195. nat/cli/commands/info/list_mcp.py +0 -461
  196. nat/data_models/temperature_mixin.py +0 -43
  197. nat/data_models/top_p_mixin.py +0 -43
  198. nat/observability/processor/header_redaction_processor.py +0 -123
  199. nat/observability/processor/redaction_processor.py +0 -77
  200. nat/tool/code_execution/test_code_execution_sandbox.py +0 -414
  201. nat/tool/github_tools/create_github_commit.py +0 -133
  202. nat/tool/github_tools/create_github_issue.py +0 -87
  203. nat/tool/github_tools/create_github_pr.py +0 -106
  204. nat/tool/github_tools/get_github_file.py +0 -106
  205. nat/tool/github_tools/get_github_issue.py +0 -166
  206. nat/tool/github_tools/get_github_pr.py +0 -256
  207. nat/tool/github_tools/update_github_issue.py +0 -100
  208. nvidia_nat-1.3.0a20250910.dist-info/METADATA +0 -373
  209. /nat/{tool/github_tools → agent/prompt_optimizer}/__init__.py +0 -0
  210. {nvidia_nat-1.3.0a20250910.dist-info → nvidia_nat-1.4.0a20251112.dist-info}/WHEEL +0 -0
  211. {nvidia_nat-1.3.0a20250910.dist-info → nvidia_nat-1.4.0a20251112.dist-info}/licenses/LICENSE-3rd-party.txt +0 -0
  212. {nvidia_nat-1.3.0a20250910.dist-info → nvidia_nat-1.4.0a20251112.dist-info}/licenses/LICENSE.md +0 -0
  213. {nvidia_nat-1.3.0a20250910.dist-info → nvidia_nat-1.4.0a20251112.dist-info}/top_level.txt +0 -0
@@ -15,11 +15,13 @@
15
15
  import asyncio
16
16
  import copy
17
17
  import functools
18
+ import gc
18
19
  import inspect
19
20
  import logging
20
21
  import re
21
22
  import time
22
23
  import types
24
+ import weakref
23
25
  from collections.abc import Callable
24
26
  from collections.abc import Iterable
25
27
  from collections.abc import Sequence
@@ -31,9 +33,62 @@ Exc = tuple[type[BaseException], ...] # exception classes
31
33
  CodePattern = int | str | range # for retry_codes argument
32
34
  logger = logging.getLogger(__name__)
33
35
 
34
- # ──────────────────────────────────────────────────────────────────────────────
36
+ # ─────────────────────────────────────────────────────────────
37
+ # Memory-optimized helpers
38
+ # ─────────────────────────────────────────────────────────────
39
+
40
+
41
+ def _shallow_copy_args(args: tuple, kwargs: dict) -> tuple[tuple, dict]:
42
+ """Create shallow copies of args and kwargs to avoid deep copy overhead."""
43
+ # For most use cases, shallow copy is sufficient and much faster
44
+ return tuple(args), dict(kwargs)
45
+
46
+
47
+ def _deep_copy_args(args: tuple, kwargs: dict, skip_first: bool = False) -> tuple[tuple, dict]:
48
+ """Create deep copies of args and kwargs to prevent mutation issues.
49
+
50
+ Args:
51
+ args: Positional arguments to copy
52
+ kwargs: Keyword arguments to copy
53
+ skip_first: If True, skip copying the first arg (typically 'self')
54
+ """
55
+ if skip_first and args:
56
+ # Don't deep copy self, only the remaining arguments
57
+ return (args[0], ) + copy.deepcopy(args[1:]), copy.deepcopy(kwargs)
58
+ return copy.deepcopy(args), copy.deepcopy(kwargs)
59
+
60
+
61
+ def _clear_exception_context(exc: BaseException) -> None:
62
+ """Clear exception traceback to free memory."""
63
+ if exc is None:
64
+ return
65
+
66
+ # Clear the exception's traceback to break reference cycles
67
+ # This is the main memory optimization
68
+ try:
69
+ exc.__traceback__ = None
70
+ except AttributeError:
71
+ pass
72
+
73
+ # Also try to clear any chained exceptions
74
+ try:
75
+ if hasattr(exc, '__cause__') and exc.__cause__ is not None:
76
+ _clear_exception_context(exc.__cause__)
77
+ if hasattr(exc, '__context__') and exc.__context__ is not None:
78
+ _clear_exception_context(exc.__context__)
79
+ except AttributeError:
80
+ pass
81
+
82
+
83
+ def _run_gc_if_needed(attempt: int, gc_frequency: int = 3) -> None:
84
+ """Run garbage collection periodically to free memory."""
85
+ if attempt > 0 and attempt % gc_frequency == 0:
86
+ gc.collect()
87
+
88
+
89
+ # ─────────────────────────────────────────────────────────────
35
90
  # Helpers: status-code extraction & pattern matching
36
- # ──────────────────────────────────────────────────────────────────────────────
91
+ # ─────────────────────────────────────────────────────────────
37
92
  _CODE_ATTRS = ("code", "status", "status_code", "http_status")
38
93
 
39
94
 
@@ -55,11 +110,12 @@ def _extract_status_code(exc: BaseException) -> int | None:
55
110
 
56
111
  def _pattern_to_regex(pat: str) -> re.Pattern[str]:
57
112
  """
58
- Convert simple wildcard pattern (4xx”, 5*”, 40x) to a ^regex$.
59
- Rule: x or ‘*’ ⇒ any digit.
113
+ Convert simple wildcard pattern ("4xx", "5*", "40x") to a ^regex$.
114
+ Rule: 'x' or '*' ⇒ any digit.
60
115
  """
61
116
  escaped = re.escape(pat)
62
- return re.compile("^" + escaped.replace(r"\*", r"\d").replace("x", r"\d") + "$")
117
+ regex_pattern = escaped.replace(r"\*", r"\d").replace("x", r"\d")
118
+ return re.compile("^" + regex_pattern + "$")
63
119
 
64
120
 
65
121
  def _code_matches(code: int, pat: CodePattern) -> bool:
@@ -70,9 +126,9 @@ def _code_matches(code: int, pat: CodePattern) -> bool:
70
126
  return bool(_pattern_to_regex(pat).match(str(code)))
71
127
 
72
128
 
73
- # ──────────────────────────────────────────────────────────────────────────────
74
- # Unified retry-decision helper
75
- # ──────────────────────────────────────────────────────────────────────────────
129
+ # ─────────────────────────────────────────────────────────────
130
+ # Unified retry-decision helper (unchanged)
131
+ # ─────────────────────────────────────────────────────────────
76
132
  def _want_retry(
77
133
  exc: BaseException,
78
134
  *,
@@ -106,9 +162,9 @@ def _want_retry(
106
162
  return False
107
163
 
108
164
 
109
- # ──────────────────────────────────────────────────────────────────────────────
110
- # Core decorator factory (sync / async / (a)gen)
111
- # ──────────────────────────────────────────────────────────────────────────────
165
+ # ─────────────────────────────────────────────────────────────
166
+ # Memory-optimized decorator factory
167
+ # ─────────────────────────────────────────────────────────────
112
168
  def _retry_decorator(
113
169
  *,
114
170
  retries: int = 3,
@@ -117,9 +173,12 @@ def _retry_decorator(
117
173
  retry_on: Exc = (Exception, ),
118
174
  retry_codes: Sequence[CodePattern] | None = None,
119
175
  retry_on_messages: Sequence[str] | None = None,
120
- deepcopy: bool = False,
176
+ shallow_copy: bool = True, # Changed default to shallow copy
177
+ gc_frequency: int = 3, # Run GC every N retries
178
+ clear_tracebacks: bool = True, # Clear exception tracebacks
179
+ instance_context_aware: bool = False,
121
180
  ) -> Callable[[Callable[..., T]], Callable[..., T]]:
122
- """
181
+ """"
123
182
  Build a decorator that retries with exponential back-off *iff*:
124
183
 
125
184
  • the raised exception is an instance of one of `retry_on`
@@ -127,72 +186,213 @@ def _retry_decorator(
127
186
 
128
187
  If both `retry_codes` and `retry_on_messages` are None, all exceptions are retried.
129
188
 
130
- deepcopy:
131
- If True, each retry receives deep‑copied *args and **kwargs* to avoid
132
- mutating shared state between attempts.
189
+ instance_context_aware:
190
+ If True, the decorator will check for a retry context flag on the first
191
+ argument (assumed to be 'self'). If the flag is set, retries are skipped
192
+ to prevent retry storms in nested method calls.
133
193
  """
134
194
 
135
195
  def decorate(fn: Callable[..., T]) -> Callable[..., T]:
136
- use_deepcopy = deepcopy
196
+ use_shallow_copy = shallow_copy
197
+ use_context_aware = instance_context_aware
198
+ skip_self_in_deepcopy = instance_context_aware
199
+
200
+ class _RetryContext:
201
+ """Context manager for instance-level retry gating."""
202
+
203
+ __slots__ = ("_obj_ref", "_enabled", "_active")
204
+
205
+ def __init__(self, args: tuple[Any, ...]):
206
+ if use_context_aware and args:
207
+ try:
208
+ # Use weak reference to avoid keeping objects alive
209
+ self._obj_ref = weakref.ref(args[0])
210
+ self._enabled = True
211
+ except TypeError:
212
+ # Object doesn't support weak references
213
+ self._obj_ref = None
214
+ self._enabled = False
215
+ else:
216
+ self._obj_ref = None
217
+ self._enabled = False
218
+ self._active = False
219
+
220
+ def __enter__(self):
221
+ if not self._enabled or self._obj_ref is None:
222
+ return False
223
+
224
+ obj = self._obj_ref()
225
+ if obj is None:
226
+ return False
137
227
 
138
- async def _call_with_retry_async(*args, **kw) -> T:
139
- delay = base_delay
140
- for attempt in range(retries):
141
- call_args = copy.deepcopy(args) if use_deepcopy else args
142
- call_kwargs = copy.deepcopy(kw) if use_deepcopy else kw
143
228
  try:
144
- return await fn(*call_args, **call_kwargs)
145
- except retry_on as exc:
146
- if (not _want_retry(exc, code_patterns=retry_codes, msg_substrings=retry_on_messages)
147
- or attempt == retries - 1):
148
- raise
149
- await asyncio.sleep(delay)
150
- delay *= backoff
229
+ # If already in retry context, skip retries
230
+ if getattr(obj, "_in_retry_context", False):
231
+ return True
232
+ object.__setattr__(obj, "_in_retry_context", True)
233
+ self._active = True
234
+ return False
235
+ except Exception:
236
+ # Cannot set attribute, disable context
237
+ self._enabled = False
238
+ return False
239
+
240
+ def __exit__(self, _exc_type, _exc, _tb):
241
+ if (self._enabled and self._active and self._obj_ref is not None):
242
+ obj = self._obj_ref()
243
+ if obj is not None:
244
+ try:
245
+ object.__setattr__(obj, "_in_retry_context", False)
246
+ except Exception:
247
+ pass
248
+
249
+ async def _call_with_retry_async(*args, **kw) -> T:
250
+ with _RetryContext(args) as already_in_context:
251
+ if already_in_context:
252
+ return await fn(*args, **kw)
253
+
254
+ delay = base_delay
255
+ last_exception = None
256
+
257
+ for attempt in range(retries):
258
+ # Copy args based on configuration
259
+ if use_shallow_copy:
260
+ call_args, call_kwargs = _shallow_copy_args(args, kw)
261
+ else:
262
+ call_args, call_kwargs = _deep_copy_args(args, kw, skip_first=skip_self_in_deepcopy)
263
+
264
+ try:
265
+ return await fn(*call_args, **call_kwargs)
266
+ except retry_on as exc:
267
+ last_exception = exc
268
+
269
+ # Clear traceback to free memory
270
+ if clear_tracebacks:
271
+ _clear_exception_context(exc)
272
+
273
+ # Run GC periodically
274
+ _run_gc_if_needed(attempt, gc_frequency)
275
+
276
+ if not _want_retry(exc, code_patterns=retry_codes,
277
+ msg_substrings=retry_on_messages) or attempt == retries - 1:
278
+ raise
279
+
280
+ await asyncio.sleep(delay)
281
+ delay *= backoff
282
+
283
+ if last_exception:
284
+ raise last_exception
151
285
 
152
286
  async def _agen_with_retry(*args, **kw):
153
- delay = base_delay
154
- for attempt in range(retries):
155
- call_args = copy.deepcopy(args) if use_deepcopy else args
156
- call_kwargs = copy.deepcopy(kw) if use_deepcopy else kw
157
- try:
158
- async for item in fn(*call_args, **call_kwargs):
287
+ with _RetryContext(args) as already_in_context:
288
+ if already_in_context:
289
+ async for item in fn(*args, **kw):
159
290
  yield item
160
291
  return
161
- except retry_on as exc:
162
- if (not _want_retry(exc, code_patterns=retry_codes, msg_substrings=retry_on_messages)
163
- or attempt == retries - 1):
164
- raise
165
- await asyncio.sleep(delay)
166
- delay *= backoff
292
+
293
+ delay = base_delay
294
+ last_exception = None
295
+
296
+ for attempt in range(retries):
297
+ if use_shallow_copy:
298
+ call_args, call_kwargs = _shallow_copy_args(args, kw)
299
+ else:
300
+ call_args, call_kwargs = _deep_copy_args(args, kw, skip_first=skip_self_in_deepcopy)
301
+
302
+ try:
303
+ async for item in fn(*call_args, **call_kwargs):
304
+ yield item
305
+ return
306
+ except retry_on as exc:
307
+ last_exception = exc
308
+
309
+ # Memory cleanup
310
+ if clear_tracebacks:
311
+ _clear_exception_context(exc)
312
+
313
+ _run_gc_if_needed(attempt, gc_frequency)
314
+
315
+ if not _want_retry(exc, code_patterns=retry_codes,
316
+ msg_substrings=retry_on_messages) or attempt == retries - 1:
317
+ raise
318
+
319
+ await asyncio.sleep(delay)
320
+ delay *= backoff
321
+
322
+ if last_exception:
323
+ raise last_exception
167
324
 
168
325
  def _gen_with_retry(*args, **kw) -> Iterable[Any]:
169
- delay = base_delay
170
- for attempt in range(retries):
171
- call_args = copy.deepcopy(args) if use_deepcopy else args
172
- call_kwargs = copy.deepcopy(kw) if use_deepcopy else kw
173
- try:
174
- yield from fn(*call_args, **call_kwargs)
326
+ with _RetryContext(args) as already_in_context:
327
+ if already_in_context:
328
+ yield from fn(*args, **kw)
175
329
  return
176
- except retry_on as exc:
177
- if (not _want_retry(exc, code_patterns=retry_codes, msg_substrings=retry_on_messages)
178
- or attempt == retries - 1):
179
- raise
180
- time.sleep(delay)
181
- delay *= backoff
330
+
331
+ delay = base_delay
332
+ last_exception = None
333
+
334
+ for attempt in range(retries):
335
+ if use_shallow_copy:
336
+ call_args, call_kwargs = _shallow_copy_args(args, kw)
337
+ else:
338
+ call_args, call_kwargs = _deep_copy_args(args, kw, skip_first=skip_self_in_deepcopy)
339
+
340
+ try:
341
+ yield from fn(*call_args, **call_kwargs)
342
+ return
343
+ except retry_on as exc:
344
+ last_exception = exc
345
+
346
+ # Memory cleanup
347
+ if clear_tracebacks:
348
+ _clear_exception_context(exc)
349
+
350
+ _run_gc_if_needed(attempt, gc_frequency)
351
+
352
+ if not _want_retry(exc, code_patterns=retry_codes,
353
+ msg_substrings=retry_on_messages) or attempt == retries - 1:
354
+ raise
355
+
356
+ time.sleep(delay)
357
+ delay *= backoff
358
+
359
+ if last_exception:
360
+ raise last_exception
182
361
 
183
362
  def _sync_with_retry(*args, **kw) -> T:
184
- delay = base_delay
185
- for attempt in range(retries):
186
- call_args = copy.deepcopy(args) if use_deepcopy else args
187
- call_kwargs = copy.deepcopy(kw) if use_deepcopy else kw
188
- try:
189
- return fn(*call_args, **call_kwargs)
190
- except retry_on as exc:
191
- if (not _want_retry(exc, code_patterns=retry_codes, msg_substrings=retry_on_messages)
192
- or attempt == retries - 1):
193
- raise
194
- time.sleep(delay)
195
- delay *= backoff
363
+ with _RetryContext(args) as already_in_context:
364
+ if already_in_context:
365
+ return fn(*args, **kw)
366
+
367
+ delay = base_delay
368
+ last_exception = None
369
+
370
+ for attempt in range(retries):
371
+ if use_shallow_copy:
372
+ call_args, call_kwargs = _shallow_copy_args(args, kw)
373
+ else:
374
+ call_args, call_kwargs = _deep_copy_args(args, kw, skip_first=skip_self_in_deepcopy)
375
+
376
+ try:
377
+ return fn(*call_args, **call_kwargs)
378
+ except retry_on as exc:
379
+ last_exception = exc
380
+
381
+ # Memory cleanup
382
+ if clear_tracebacks:
383
+ _clear_exception_context(exc)
384
+
385
+ _run_gc_if_needed(attempt, gc_frequency)
386
+
387
+ if not _want_retry(exc, code_patterns=retry_codes,
388
+ msg_substrings=retry_on_messages) or attempt == retries - 1:
389
+ raise
390
+
391
+ time.sleep(delay)
392
+ delay *= backoff
393
+
394
+ if last_exception:
395
+ raise last_exception
196
396
 
197
397
  # Decide which wrapper to return
198
398
  if inspect.iscoroutinefunction(fn):
@@ -209,9 +409,6 @@ def _retry_decorator(
209
409
  return decorate
210
410
 
211
411
 
212
- # ──────────────────────────────────────────────────────────────────────────────
213
- # Public helper : patch_with_retry
214
- # ──────────────────────────────────────────────────────────────────────────────
215
412
  def patch_with_retry(
216
413
  obj: Any,
217
414
  *,
@@ -221,7 +418,9 @@ def patch_with_retry(
221
418
  retry_on: Exc = (Exception, ),
222
419
  retry_codes: Sequence[CodePattern] | None = None,
223
420
  retry_on_messages: Sequence[str] | None = None,
224
- deepcopy: bool = False,
421
+ deep_copy: bool = False,
422
+ gc_frequency: int = 3,
423
+ clear_tracebacks: bool = True,
225
424
  ) -> Any:
226
425
  """
227
426
  Patch *obj* instance-locally so **every public method** retries on failure.
@@ -237,6 +436,10 @@ def patch_with_retry(
237
436
  If True, each retry receives deep‑copied *args and **kwargs* to avoid
238
437
  mutating shared state between attempts.
239
438
  """
439
+
440
+ # Invert deep copy to keep function signature the same
441
+ shallow_copy = not deep_copy
442
+
240
443
  deco = _retry_decorator(
241
444
  retries=retries,
242
445
  base_delay=base_delay,
@@ -244,10 +447,13 @@ def patch_with_retry(
244
447
  retry_on=retry_on,
245
448
  retry_codes=retry_codes,
246
449
  retry_on_messages=retry_on_messages,
247
- deepcopy=deepcopy,
450
+ shallow_copy=shallow_copy,
451
+ gc_frequency=gc_frequency,
452
+ clear_tracebacks=clear_tracebacks,
453
+ instance_context_aware=True, # Prevent retry storms
248
454
  )
249
455
 
250
- # Choose attribute source: the *class* to avoid triggering __getattr__
456
+ # Choose attribute source: the *class* to avoid __getattr__
251
457
  cls = obj if inspect.isclass(obj) else type(obj)
252
458
  cls_name = getattr(cls, "__name__", str(cls))
253
459
 
@@ -255,7 +461,7 @@ def patch_with_retry(
255
461
  descriptor = inspect.getattr_static(cls, name)
256
462
 
257
463
  # Skip dunders, privates and all descriptors we must not wrap
258
- if (name.startswith("_") or isinstance(descriptor, (property, staticmethod, classmethod))):
464
+ if name.startswith("_") or isinstance(descriptor, property | staticmethod | classmethod):
259
465
  continue
260
466
 
261
467
  original = descriptor.__func__ if isinstance(descriptor, types.MethodType) else descriptor
@@ -16,6 +16,7 @@
16
16
  import io
17
17
  import logging
18
18
  import typing
19
+ from pathlib import Path
19
20
 
20
21
  import expandvars
21
22
  import yaml
@@ -44,23 +45,92 @@ def _interpolate_variables(value: str | int | float | bool | None) -> str | int
44
45
  return expandvars.expandvars(value)
45
46
 
46
47
 
47
- def yaml_load(config_path: StrPath) -> dict:
48
+ def deep_merge(base: dict, override: dict) -> dict:
49
+ """
50
+ Recursively merge override dictionary into base dictionary.
51
+
52
+ Args:
53
+ base (dict): The base configuration dictionary.
54
+ override (dict): The override configuration dictionary.
55
+
56
+ Returns:
57
+ dict: The merged configuration dictionary.
58
+ """
59
+ result = base.copy()
60
+ for key, value in override.items():
61
+ if key in result and isinstance(result[key], dict) and isinstance(value, dict):
62
+ result[key] = deep_merge(result[key], value)
63
+ else:
64
+ result[key] = value
65
+ return result
66
+
67
+
68
+ def yaml_load(config_path: StrPath, _visited: set[Path] | None = None) -> dict:
48
69
  """
49
70
  Load a YAML file and interpolate variables in the format
50
71
  ${VAR:-default_value}.
51
72
 
73
+ If the YAML file contains a "base" key, the file at that path will be
74
+ loaded first, and the current config will be merged on top of it. This enables
75
+ config inheritance to reduce duplication across similar configuration files.
76
+
52
77
  Args:
53
78
  config_path (StrPath): The path to the YAML file to load.
79
+ _visited (set[Path] | None): Internal parameter for circular dependency detection.
54
80
 
55
81
  Returns:
56
82
  dict: The processed configuration dictionary.
83
+
84
+ Raises:
85
+ TypeError: If the "base" key is not a string.
86
+ FileNotFoundError: If the base configuration file does not exist.
87
+ ValueError: If a circular dependency is detected in configuration inheritance.
57
88
  """
89
+ # Normalize the config path and detect circular dependencies
90
+ config_path_obj = Path(config_path).resolve()
91
+
92
+ if _visited is None:
93
+ _visited = set()
94
+
95
+ if config_path_obj in _visited:
96
+ raise ValueError(f"Circular dependency detected in configuration inheritance: {config_path_obj} "
97
+ f"is already in the inheritance chain")
98
+
99
+ _visited.add(config_path_obj)
58
100
 
59
101
  # Read YAML file
60
- with open(config_path, "r", encoding="utf-8") as stream:
102
+ with open(config_path_obj, encoding="utf-8") as stream:
61
103
  config_str = stream.read()
62
104
 
63
- return yaml_loads(config_str)
105
+ config = yaml_loads(config_str)
106
+
107
+ # Check if config specifies a base for inheritance
108
+ if "base" in config:
109
+ base_path_str = config["base"]
110
+
111
+ # Validate that base is a string
112
+ if not isinstance(base_path_str, str):
113
+ raise TypeError(f"Configuration 'base' key must be a string, got {type(base_path_str).__name__}")
114
+
115
+ # Resolve base path relative to current config
116
+ if not Path(base_path_str).is_absolute():
117
+ base_path = config_path_obj.parent / base_path_str
118
+ else:
119
+ base_path = Path(base_path_str)
120
+
121
+ # Normalize and check if base file exists
122
+ base_path = base_path.resolve()
123
+ if not base_path.exists():
124
+ raise FileNotFoundError(f"Base configuration file not found: {base_path}")
125
+
126
+ # Load base config (recursively, so bases can have bases)
127
+ base_config = yaml_load(base_path, _visited=_visited)
128
+
129
+ # Perform deep merge and remove 'base' key from result
130
+ config = deep_merge(base_config, config)
131
+ config.pop("base", None)
132
+
133
+ return config
64
134
 
65
135
 
66
136
  def yaml_loads(config: str) -> dict:
@@ -0,0 +1,25 @@
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
+
18
+ # Define log level choices
19
+ LOG_LEVELS = {
20
+ 'DEBUG': logging.DEBUG,
21
+ 'INFO': logging.INFO,
22
+ 'WARNING': logging.WARNING,
23
+ 'ERROR': logging.ERROR,
24
+ 'CRITICAL': logging.CRITICAL
25
+ }
@@ -0,0 +1,26 @@
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
+ # pylint: disable=raising-format-tuple
16
+
17
+ from nat.builder.framework_enum import LLMFrameworkEnum
18
+ from nat.data_models.llm import APITypeEnum
19
+
20
+
21
+ def validate_no_responses_api(llm_config, framework: LLMFrameworkEnum):
22
+ """Validate that the LLM config does not use the Responses API."""
23
+
24
+ if llm_config.api_type == APITypeEnum.RESPONSES:
25
+ raise ValueError(f"Responses API is not supported for config {str(type(llm_config))} in framework {framework}. "
26
+ f"Please use a different API type.")
nat/utils/string_utils.py CHANGED
@@ -36,3 +36,19 @@ def convert_to_str(value: Any) -> str:
36
36
  return str(value)
37
37
  else:
38
38
  raise ValueError(f"Unsupported type for conversion to string: {type(value)}")
39
+
40
+
41
+ def truncate_string(text: str | None, max_length: int = 100) -> str | None:
42
+ """
43
+ Truncate a string to a maximum length, adding ellipsis if truncated.
44
+
45
+ Args:
46
+ text: The text to truncate (can be None)
47
+ max_length: Maximum allowed length (default: 100)
48
+
49
+ Returns:
50
+ The truncated text with ellipsis if needed, or None if input was None
51
+ """
52
+ if not text or len(text) <= max_length:
53
+ return text
54
+ return text[:max_length - 3] + "..."
@@ -90,9 +90,17 @@ class TypeConverter:
90
90
  decomposed = DecomposedType(to_type)
91
91
 
92
92
  # 1) If data is already correct type, return it
93
- if to_type is None or decomposed.is_instance((data, to_type)):
93
+ if to_type is None or decomposed.is_instance(data):
94
94
  return data
95
95
 
96
+ # 2) If data is a union type, try to convert to each type in the union
97
+ if decomposed.is_union:
98
+ for union_type in decomposed.args:
99
+ result = self._convert(data, union_type)
100
+ if result is not None:
101
+ return result
102
+ return None
103
+
96
104
  root = decomposed.root
97
105
 
98
106
  # 2) Attempt direct in *this* converter
@@ -198,16 +206,17 @@ class TypeConverter:
198
206
  """
199
207
  visited = set()
200
208
  final = self._try_indirect_conversion(data, to_type, visited)
209
+ src_type = type(data)
201
210
  if final is not None:
202
211
  # Warn once if found a chain
203
- self._maybe_warn_indirect(type(data), to_type)
212
+ self._maybe_warn_indirect(src_type, to_type)
204
213
  return final
205
214
 
206
215
  # If no success, try parent's indirect
207
216
  if self._parent is not None:
208
217
  parent_final = self._parent._try_indirect_convert(data, to_type)
209
218
  if parent_final is not None:
210
- self._maybe_warn_indirect(type(data), to_type)
219
+ self._maybe_warn_indirect(src_type, to_type)
211
220
  return parent_final
212
221
 
213
222
  return None