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.
- nat/agent/base.py +13 -8
- nat/agent/prompt_optimizer/prompt.py +68 -0
- nat/agent/prompt_optimizer/register.py +149 -0
- nat/agent/react_agent/agent.py +6 -5
- nat/agent/react_agent/register.py +49 -39
- nat/agent/reasoning_agent/reasoning_agent.py +17 -15
- nat/agent/register.py +2 -0
- nat/agent/responses_api_agent/__init__.py +14 -0
- nat/agent/responses_api_agent/register.py +126 -0
- nat/agent/rewoo_agent/agent.py +304 -117
- nat/agent/rewoo_agent/prompt.py +19 -22
- nat/agent/rewoo_agent/register.py +51 -38
- nat/agent/tool_calling_agent/agent.py +75 -17
- nat/agent/tool_calling_agent/register.py +46 -23
- nat/authentication/api_key/api_key_auth_provider.py +6 -11
- nat/authentication/api_key/api_key_auth_provider_config.py +8 -5
- nat/authentication/credential_validator/__init__.py +14 -0
- nat/authentication/credential_validator/bearer_token_validator.py +557 -0
- nat/authentication/http_basic_auth/http_basic_auth_provider.py +1 -1
- nat/authentication/interfaces.py +5 -2
- nat/authentication/oauth2/oauth2_auth_code_flow_provider.py +69 -36
- nat/authentication/oauth2/oauth2_auth_code_flow_provider_config.py +2 -1
- nat/authentication/oauth2/oauth2_resource_server_config.py +125 -0
- nat/builder/builder.py +55 -23
- nat/builder/component_utils.py +9 -5
- nat/builder/context.py +54 -15
- nat/builder/eval_builder.py +14 -9
- nat/builder/framework_enum.py +1 -0
- nat/builder/front_end.py +1 -1
- nat/builder/function.py +370 -0
- nat/builder/function_info.py +1 -1
- nat/builder/intermediate_step_manager.py +38 -2
- nat/builder/workflow.py +5 -0
- nat/builder/workflow_builder.py +306 -54
- nat/cli/cli_utils/config_override.py +1 -1
- nat/cli/commands/info/info.py +16 -6
- nat/cli/commands/mcp/__init__.py +14 -0
- nat/cli/commands/mcp/mcp.py +986 -0
- nat/cli/commands/optimize.py +90 -0
- nat/cli/commands/start.py +1 -1
- nat/cli/commands/workflow/templates/config.yml.j2 +14 -13
- nat/cli/commands/workflow/templates/register.py.j2 +2 -2
- nat/cli/commands/workflow/templates/workflow.py.j2 +35 -21
- nat/cli/commands/workflow/workflow_commands.py +60 -18
- nat/cli/entrypoint.py +15 -11
- nat/cli/main.py +3 -0
- nat/cli/register_workflow.py +38 -4
- nat/cli/type_registry.py +72 -1
- nat/control_flow/__init__.py +0 -0
- nat/control_flow/register.py +20 -0
- nat/control_flow/router_agent/__init__.py +0 -0
- nat/control_flow/router_agent/agent.py +329 -0
- nat/control_flow/router_agent/prompt.py +48 -0
- nat/control_flow/router_agent/register.py +91 -0
- nat/control_flow/sequential_executor.py +166 -0
- nat/data_models/agent.py +34 -0
- nat/data_models/api_server.py +199 -69
- nat/data_models/authentication.py +23 -9
- nat/data_models/common.py +47 -0
- nat/data_models/component.py +2 -0
- nat/data_models/component_ref.py +11 -0
- nat/data_models/config.py +41 -17
- nat/data_models/dataset_handler.py +4 -3
- nat/data_models/function.py +34 -0
- nat/data_models/function_dependencies.py +8 -0
- nat/data_models/intermediate_step.py +9 -1
- nat/data_models/llm.py +15 -1
- nat/data_models/openai_mcp.py +46 -0
- nat/data_models/optimizable.py +208 -0
- nat/data_models/optimizer.py +161 -0
- nat/data_models/span.py +41 -3
- nat/data_models/thinking_mixin.py +2 -2
- nat/embedder/azure_openai_embedder.py +2 -1
- nat/embedder/nim_embedder.py +3 -2
- nat/embedder/openai_embedder.py +3 -2
- nat/eval/config.py +1 -1
- nat/eval/dataset_handler/dataset_downloader.py +3 -2
- nat/eval/dataset_handler/dataset_filter.py +34 -2
- nat/eval/evaluate.py +10 -3
- nat/eval/evaluator/base_evaluator.py +1 -1
- nat/eval/rag_evaluator/evaluate.py +7 -4
- nat/eval/register.py +4 -0
- nat/eval/runtime_evaluator/__init__.py +14 -0
- nat/eval/runtime_evaluator/evaluate.py +123 -0
- nat/eval/runtime_evaluator/register.py +100 -0
- nat/eval/swe_bench_evaluator/evaluate.py +1 -1
- nat/eval/trajectory_evaluator/register.py +1 -1
- nat/eval/tunable_rag_evaluator/evaluate.py +1 -1
- nat/eval/usage_stats.py +2 -0
- nat/eval/utils/output_uploader.py +3 -2
- nat/eval/utils/weave_eval.py +17 -3
- nat/experimental/decorators/experimental_warning_decorator.py +27 -7
- nat/experimental/test_time_compute/functions/execute_score_select_function.py +1 -1
- nat/experimental/test_time_compute/functions/plan_select_execute_function.py +7 -3
- nat/experimental/test_time_compute/functions/ttc_tool_orchestration_function.py +1 -1
- nat/experimental/test_time_compute/functions/ttc_tool_wrapper_function.py +3 -3
- nat/experimental/test_time_compute/models/strategy_base.py +2 -2
- nat/experimental/test_time_compute/selection/llm_based_output_merging_selector.py +1 -1
- nat/front_ends/console/authentication_flow_handler.py +82 -30
- nat/front_ends/console/console_front_end_plugin.py +19 -7
- nat/front_ends/fastapi/auth_flow_handlers/http_flow_handler.py +1 -1
- nat/front_ends/fastapi/auth_flow_handlers/websocket_flow_handler.py +52 -17
- nat/front_ends/fastapi/dask_client_mixin.py +65 -0
- nat/front_ends/fastapi/fastapi_front_end_config.py +25 -3
- nat/front_ends/fastapi/fastapi_front_end_plugin.py +140 -3
- nat/front_ends/fastapi/fastapi_front_end_plugin_worker.py +445 -265
- nat/front_ends/fastapi/job_store.py +518 -99
- nat/front_ends/fastapi/main.py +11 -19
- nat/front_ends/fastapi/message_handler.py +69 -44
- nat/front_ends/fastapi/message_validator.py +8 -7
- nat/front_ends/fastapi/utils.py +57 -0
- nat/front_ends/mcp/introspection_token_verifier.py +73 -0
- nat/front_ends/mcp/mcp_front_end_config.py +71 -3
- nat/front_ends/mcp/mcp_front_end_plugin.py +85 -21
- nat/front_ends/mcp/mcp_front_end_plugin_worker.py +248 -29
- nat/front_ends/mcp/memory_profiler.py +320 -0
- nat/front_ends/mcp/tool_converter.py +78 -25
- nat/front_ends/simple_base/simple_front_end_plugin_base.py +3 -1
- nat/llm/aws_bedrock_llm.py +21 -8
- nat/llm/azure_openai_llm.py +14 -5
- nat/llm/litellm_llm.py +80 -0
- nat/llm/nim_llm.py +23 -9
- nat/llm/openai_llm.py +19 -7
- nat/llm/register.py +4 -0
- nat/llm/utils/thinking.py +1 -1
- nat/observability/exporter/base_exporter.py +1 -1
- nat/observability/exporter/processing_exporter.py +29 -55
- nat/observability/exporter/span_exporter.py +43 -15
- nat/observability/exporter_manager.py +2 -2
- nat/observability/mixin/redaction_config_mixin.py +5 -4
- nat/observability/mixin/tagging_config_mixin.py +26 -14
- nat/observability/mixin/type_introspection_mixin.py +420 -107
- nat/observability/processor/batching_processor.py +1 -1
- nat/observability/processor/processor.py +3 -0
- nat/observability/processor/redaction/__init__.py +24 -0
- nat/observability/processor/redaction/contextual_redaction_processor.py +125 -0
- nat/observability/processor/redaction/contextual_span_redaction_processor.py +66 -0
- nat/observability/processor/redaction/redaction_processor.py +177 -0
- nat/observability/processor/redaction/span_header_redaction_processor.py +92 -0
- nat/observability/processor/span_tagging_processor.py +21 -14
- nat/observability/register.py +16 -0
- nat/profiler/callbacks/langchain_callback_handler.py +32 -7
- nat/profiler/callbacks/llama_index_callback_handler.py +36 -2
- nat/profiler/callbacks/token_usage_base_model.py +2 -0
- nat/profiler/decorators/framework_wrapper.py +61 -9
- nat/profiler/decorators/function_tracking.py +35 -3
- nat/profiler/forecasting/models/linear_model.py +1 -1
- nat/profiler/forecasting/models/random_forest_regressor.py +1 -1
- nat/profiler/inference_optimization/bottleneck_analysis/nested_stack_analysis.py +1 -1
- nat/profiler/inference_optimization/experimental/prefix_span_analysis.py +1 -1
- nat/profiler/parameter_optimization/__init__.py +0 -0
- nat/profiler/parameter_optimization/optimizable_utils.py +93 -0
- nat/profiler/parameter_optimization/optimizer_runtime.py +67 -0
- nat/profiler/parameter_optimization/parameter_optimizer.py +189 -0
- nat/profiler/parameter_optimization/parameter_selection.py +107 -0
- nat/profiler/parameter_optimization/pareto_visualizer.py +460 -0
- nat/profiler/parameter_optimization/prompt_optimizer.py +384 -0
- nat/profiler/parameter_optimization/update_helpers.py +66 -0
- nat/profiler/utils.py +3 -1
- nat/registry_handlers/pypi/register_pypi.py +5 -3
- nat/registry_handlers/rest/register_rest.py +5 -3
- nat/retriever/milvus/retriever.py +1 -1
- nat/retriever/nemo_retriever/register.py +2 -1
- nat/runtime/loader.py +1 -1
- nat/runtime/runner.py +111 -6
- nat/runtime/session.py +49 -3
- nat/settings/global_settings.py +2 -2
- nat/tool/chat_completion.py +4 -1
- nat/tool/code_execution/code_sandbox.py +3 -6
- nat/tool/code_execution/local_sandbox/Dockerfile.sandbox +19 -32
- nat/tool/code_execution/local_sandbox/local_sandbox_server.py +6 -1
- nat/tool/code_execution/local_sandbox/sandbox.requirements.txt +2 -0
- nat/tool/code_execution/local_sandbox/start_local_sandbox.sh +10 -4
- nat/tool/datetime_tools.py +1 -1
- nat/tool/github_tools.py +450 -0
- nat/tool/memory_tools/add_memory_tool.py +3 -3
- nat/tool/memory_tools/delete_memory_tool.py +3 -4
- nat/tool/memory_tools/get_memory_tool.py +4 -4
- nat/tool/register.py +2 -7
- nat/tool/server_tools.py +15 -2
- nat/utils/__init__.py +76 -0
- nat/utils/callable_utils.py +70 -0
- nat/utils/data_models/schema_validator.py +1 -1
- nat/utils/decorators.py +210 -0
- nat/utils/exception_handlers/automatic_retries.py +278 -72
- nat/utils/io/yaml_tools.py +73 -3
- nat/utils/log_levels.py +25 -0
- nat/utils/responses_api.py +26 -0
- nat/utils/string_utils.py +16 -0
- nat/utils/type_converter.py +12 -3
- nat/utils/type_utils.py +6 -2
- nvidia_nat-1.4.0a20251112.dist-info/METADATA +197 -0
- {nvidia_nat-1.3.0a20250910.dist-info → nvidia_nat-1.4.0a20251112.dist-info}/RECORD +199 -165
- {nvidia_nat-1.3.0a20250910.dist-info → nvidia_nat-1.4.0a20251112.dist-info}/entry_points.txt +1 -0
- nat/cli/commands/info/list_mcp.py +0 -461
- nat/data_models/temperature_mixin.py +0 -43
- nat/data_models/top_p_mixin.py +0 -43
- nat/observability/processor/header_redaction_processor.py +0 -123
- nat/observability/processor/redaction_processor.py +0 -77
- nat/tool/code_execution/test_code_execution_sandbox.py +0 -414
- nat/tool/github_tools/create_github_commit.py +0 -133
- nat/tool/github_tools/create_github_issue.py +0 -87
- nat/tool/github_tools/create_github_pr.py +0 -106
- nat/tool/github_tools/get_github_file.py +0 -106
- nat/tool/github_tools/get_github_issue.py +0 -166
- nat/tool/github_tools/get_github_pr.py +0 -256
- nat/tool/github_tools/update_github_issue.py +0 -100
- nvidia_nat-1.3.0a20250910.dist-info/METADATA +0 -373
- /nat/{tool/github_tools → agent/prompt_optimizer}/__init__.py +0 -0
- {nvidia_nat-1.3.0a20250910.dist-info → nvidia_nat-1.4.0a20251112.dist-info}/WHEEL +0 -0
- {nvidia_nat-1.3.0a20250910.dist-info → nvidia_nat-1.4.0a20251112.dist-info}/licenses/LICENSE-3rd-party.txt +0 -0
- {nvidia_nat-1.3.0a20250910.dist-info → nvidia_nat-1.4.0a20251112.dist-info}/licenses/LICENSE.md +0 -0
- {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 (
|
|
59
|
-
Rule:
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
131
|
-
If True,
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
nat/utils/io/yaml_tools.py
CHANGED
|
@@ -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
|
|
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(
|
|
102
|
+
with open(config_path_obj, encoding="utf-8") as stream:
|
|
61
103
|
config_str = stream.read()
|
|
62
104
|
|
|
63
|
-
|
|
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:
|
nat/utils/log_levels.py
ADDED
|
@@ -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] + "..."
|
nat/utils/type_converter.py
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
219
|
+
self._maybe_warn_indirect(src_type, to_type)
|
|
211
220
|
return parent_final
|
|
212
221
|
|
|
213
222
|
return None
|