nvidia-nat 1.1.0a20251020__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.
- aiq/__init__.py +66 -0
- nat/agent/__init__.py +0 -0
- nat/agent/base.py +265 -0
- nat/agent/dual_node.py +72 -0
- nat/agent/prompt_optimizer/__init__.py +0 -0
- nat/agent/prompt_optimizer/prompt.py +68 -0
- nat/agent/prompt_optimizer/register.py +149 -0
- nat/agent/react_agent/__init__.py +0 -0
- nat/agent/react_agent/agent.py +394 -0
- nat/agent/react_agent/output_parser.py +104 -0
- nat/agent/react_agent/prompt.py +44 -0
- nat/agent/react_agent/register.py +168 -0
- nat/agent/reasoning_agent/__init__.py +0 -0
- nat/agent/reasoning_agent/reasoning_agent.py +227 -0
- nat/agent/register.py +23 -0
- nat/agent/rewoo_agent/__init__.py +0 -0
- nat/agent/rewoo_agent/agent.py +593 -0
- nat/agent/rewoo_agent/prompt.py +107 -0
- nat/agent/rewoo_agent/register.py +175 -0
- nat/agent/tool_calling_agent/__init__.py +0 -0
- nat/agent/tool_calling_agent/agent.py +246 -0
- nat/agent/tool_calling_agent/register.py +129 -0
- nat/authentication/__init__.py +14 -0
- nat/authentication/api_key/__init__.py +14 -0
- nat/authentication/api_key/api_key_auth_provider.py +96 -0
- nat/authentication/api_key/api_key_auth_provider_config.py +124 -0
- nat/authentication/api_key/register.py +26 -0
- nat/authentication/credential_validator/__init__.py +14 -0
- nat/authentication/credential_validator/bearer_token_validator.py +557 -0
- nat/authentication/exceptions/__init__.py +14 -0
- nat/authentication/exceptions/api_key_exceptions.py +38 -0
- nat/authentication/http_basic_auth/__init__.py +0 -0
- nat/authentication/http_basic_auth/http_basic_auth_provider.py +81 -0
- nat/authentication/http_basic_auth/register.py +30 -0
- nat/authentication/interfaces.py +96 -0
- nat/authentication/oauth2/__init__.py +14 -0
- nat/authentication/oauth2/oauth2_auth_code_flow_provider.py +140 -0
- nat/authentication/oauth2/oauth2_auth_code_flow_provider_config.py +39 -0
- nat/authentication/oauth2/oauth2_resource_server_config.py +124 -0
- nat/authentication/oauth2/register.py +25 -0
- nat/authentication/register.py +20 -0
- nat/builder/__init__.py +0 -0
- nat/builder/builder.py +317 -0
- nat/builder/component_utils.py +320 -0
- nat/builder/context.py +321 -0
- nat/builder/embedder.py +24 -0
- nat/builder/eval_builder.py +166 -0
- nat/builder/evaluator.py +29 -0
- nat/builder/framework_enum.py +25 -0
- nat/builder/front_end.py +73 -0
- nat/builder/function.py +714 -0
- nat/builder/function_base.py +380 -0
- nat/builder/function_info.py +625 -0
- nat/builder/intermediate_step_manager.py +206 -0
- nat/builder/llm.py +25 -0
- nat/builder/retriever.py +25 -0
- nat/builder/user_interaction_manager.py +78 -0
- nat/builder/workflow.py +160 -0
- nat/builder/workflow_builder.py +1365 -0
- nat/cli/__init__.py +14 -0
- nat/cli/cli_utils/__init__.py +0 -0
- nat/cli/cli_utils/config_override.py +231 -0
- nat/cli/cli_utils/validation.py +37 -0
- nat/cli/commands/__init__.py +0 -0
- nat/cli/commands/configure/__init__.py +0 -0
- nat/cli/commands/configure/channel/__init__.py +0 -0
- nat/cli/commands/configure/channel/add.py +28 -0
- nat/cli/commands/configure/channel/channel.py +34 -0
- nat/cli/commands/configure/channel/remove.py +30 -0
- nat/cli/commands/configure/channel/update.py +30 -0
- nat/cli/commands/configure/configure.py +33 -0
- nat/cli/commands/evaluate.py +139 -0
- nat/cli/commands/info/__init__.py +14 -0
- nat/cli/commands/info/info.py +47 -0
- nat/cli/commands/info/list_channels.py +32 -0
- nat/cli/commands/info/list_components.py +128 -0
- nat/cli/commands/mcp/__init__.py +14 -0
- nat/cli/commands/mcp/mcp.py +986 -0
- nat/cli/commands/object_store/__init__.py +14 -0
- nat/cli/commands/object_store/object_store.py +227 -0
- nat/cli/commands/optimize.py +90 -0
- nat/cli/commands/registry/__init__.py +14 -0
- nat/cli/commands/registry/publish.py +88 -0
- nat/cli/commands/registry/pull.py +118 -0
- nat/cli/commands/registry/registry.py +36 -0
- nat/cli/commands/registry/remove.py +108 -0
- nat/cli/commands/registry/search.py +153 -0
- nat/cli/commands/sizing/__init__.py +14 -0
- nat/cli/commands/sizing/calc.py +297 -0
- nat/cli/commands/sizing/sizing.py +27 -0
- nat/cli/commands/start.py +257 -0
- nat/cli/commands/uninstall.py +81 -0
- nat/cli/commands/validate.py +47 -0
- nat/cli/commands/workflow/__init__.py +14 -0
- nat/cli/commands/workflow/templates/__init__.py.j2 +0 -0
- nat/cli/commands/workflow/templates/config.yml.j2 +17 -0
- nat/cli/commands/workflow/templates/pyproject.toml.j2 +25 -0
- nat/cli/commands/workflow/templates/register.py.j2 +4 -0
- nat/cli/commands/workflow/templates/workflow.py.j2 +50 -0
- nat/cli/commands/workflow/workflow.py +37 -0
- nat/cli/commands/workflow/workflow_commands.py +403 -0
- nat/cli/entrypoint.py +141 -0
- nat/cli/main.py +60 -0
- nat/cli/register_workflow.py +522 -0
- nat/cli/type_registry.py +1069 -0
- 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/__init__.py +14 -0
- nat/data_models/agent.py +34 -0
- nat/data_models/api_server.py +843 -0
- nat/data_models/authentication.py +245 -0
- nat/data_models/common.py +171 -0
- nat/data_models/component.py +60 -0
- nat/data_models/component_ref.py +179 -0
- nat/data_models/config.py +434 -0
- nat/data_models/dataset_handler.py +169 -0
- nat/data_models/discovery_metadata.py +305 -0
- nat/data_models/embedder.py +27 -0
- nat/data_models/evaluate.py +130 -0
- nat/data_models/evaluator.py +26 -0
- nat/data_models/front_end.py +26 -0
- nat/data_models/function.py +64 -0
- nat/data_models/function_dependencies.py +80 -0
- nat/data_models/gated_field_mixin.py +242 -0
- nat/data_models/interactive.py +246 -0
- nat/data_models/intermediate_step.py +302 -0
- nat/data_models/invocation_node.py +38 -0
- nat/data_models/llm.py +27 -0
- nat/data_models/logging.py +26 -0
- nat/data_models/memory.py +27 -0
- nat/data_models/object_store.py +44 -0
- nat/data_models/optimizable.py +119 -0
- nat/data_models/optimizer.py +149 -0
- nat/data_models/profiler.py +54 -0
- nat/data_models/registry_handler.py +26 -0
- nat/data_models/retriever.py +30 -0
- nat/data_models/retry_mixin.py +35 -0
- nat/data_models/span.py +228 -0
- nat/data_models/step_adaptor.py +64 -0
- nat/data_models/streaming.py +33 -0
- nat/data_models/swe_bench_model.py +54 -0
- nat/data_models/telemetry_exporter.py +26 -0
- nat/data_models/temperature_mixin.py +44 -0
- nat/data_models/thinking_mixin.py +86 -0
- nat/data_models/top_p_mixin.py +44 -0
- nat/data_models/ttc_strategy.py +30 -0
- nat/embedder/__init__.py +0 -0
- nat/embedder/azure_openai_embedder.py +46 -0
- nat/embedder/nim_embedder.py +59 -0
- nat/embedder/openai_embedder.py +42 -0
- nat/embedder/register.py +22 -0
- nat/eval/__init__.py +14 -0
- nat/eval/config.py +62 -0
- nat/eval/dataset_handler/__init__.py +0 -0
- nat/eval/dataset_handler/dataset_downloader.py +106 -0
- nat/eval/dataset_handler/dataset_filter.py +52 -0
- nat/eval/dataset_handler/dataset_handler.py +431 -0
- nat/eval/evaluate.py +565 -0
- nat/eval/evaluator/__init__.py +14 -0
- nat/eval/evaluator/base_evaluator.py +77 -0
- nat/eval/evaluator/evaluator_model.py +58 -0
- nat/eval/intermediate_step_adapter.py +99 -0
- nat/eval/rag_evaluator/__init__.py +0 -0
- nat/eval/rag_evaluator/evaluate.py +178 -0
- nat/eval/rag_evaluator/register.py +143 -0
- nat/eval/register.py +26 -0
- nat/eval/remote_workflow.py +133 -0
- nat/eval/runners/__init__.py +14 -0
- nat/eval/runners/config.py +39 -0
- nat/eval/runners/multi_eval_runner.py +54 -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/runtime_event_subscriber.py +52 -0
- nat/eval/swe_bench_evaluator/__init__.py +0 -0
- nat/eval/swe_bench_evaluator/evaluate.py +215 -0
- nat/eval/swe_bench_evaluator/register.py +36 -0
- nat/eval/trajectory_evaluator/__init__.py +0 -0
- nat/eval/trajectory_evaluator/evaluate.py +75 -0
- nat/eval/trajectory_evaluator/register.py +40 -0
- nat/eval/tunable_rag_evaluator/__init__.py +0 -0
- nat/eval/tunable_rag_evaluator/evaluate.py +242 -0
- nat/eval/tunable_rag_evaluator/register.py +52 -0
- nat/eval/usage_stats.py +41 -0
- nat/eval/utils/__init__.py +0 -0
- nat/eval/utils/eval_trace_ctx.py +89 -0
- nat/eval/utils/output_uploader.py +140 -0
- nat/eval/utils/tqdm_position_registry.py +40 -0
- nat/eval/utils/weave_eval.py +193 -0
- nat/experimental/__init__.py +0 -0
- nat/experimental/decorators/__init__.py +0 -0
- nat/experimental/decorators/experimental_warning_decorator.py +154 -0
- nat/experimental/test_time_compute/__init__.py +0 -0
- nat/experimental/test_time_compute/editing/__init__.py +0 -0
- nat/experimental/test_time_compute/editing/iterative_plan_refinement_editor.py +147 -0
- nat/experimental/test_time_compute/editing/llm_as_a_judge_editor.py +204 -0
- nat/experimental/test_time_compute/editing/motivation_aware_summarization.py +107 -0
- nat/experimental/test_time_compute/functions/__init__.py +0 -0
- nat/experimental/test_time_compute/functions/execute_score_select_function.py +105 -0
- nat/experimental/test_time_compute/functions/plan_select_execute_function.py +228 -0
- nat/experimental/test_time_compute/functions/ttc_tool_orchestration_function.py +205 -0
- nat/experimental/test_time_compute/functions/ttc_tool_wrapper_function.py +146 -0
- nat/experimental/test_time_compute/models/__init__.py +0 -0
- nat/experimental/test_time_compute/models/editor_config.py +132 -0
- nat/experimental/test_time_compute/models/scoring_config.py +112 -0
- nat/experimental/test_time_compute/models/search_config.py +120 -0
- nat/experimental/test_time_compute/models/selection_config.py +154 -0
- nat/experimental/test_time_compute/models/stage_enums.py +43 -0
- nat/experimental/test_time_compute/models/strategy_base.py +67 -0
- nat/experimental/test_time_compute/models/tool_use_config.py +41 -0
- nat/experimental/test_time_compute/models/ttc_item.py +48 -0
- nat/experimental/test_time_compute/register.py +35 -0
- nat/experimental/test_time_compute/scoring/__init__.py +0 -0
- nat/experimental/test_time_compute/scoring/llm_based_agent_scorer.py +168 -0
- nat/experimental/test_time_compute/scoring/llm_based_plan_scorer.py +168 -0
- nat/experimental/test_time_compute/scoring/motivation_aware_scorer.py +111 -0
- nat/experimental/test_time_compute/search/__init__.py +0 -0
- nat/experimental/test_time_compute/search/multi_llm_planner.py +128 -0
- nat/experimental/test_time_compute/search/multi_query_retrieval_search.py +122 -0
- nat/experimental/test_time_compute/search/single_shot_multi_plan_planner.py +128 -0
- nat/experimental/test_time_compute/selection/__init__.py +0 -0
- nat/experimental/test_time_compute/selection/best_of_n_selector.py +63 -0
- nat/experimental/test_time_compute/selection/llm_based_agent_output_selector.py +131 -0
- nat/experimental/test_time_compute/selection/llm_based_output_merging_selector.py +157 -0
- nat/experimental/test_time_compute/selection/llm_based_plan_selector.py +128 -0
- nat/experimental/test_time_compute/selection/threshold_selector.py +58 -0
- nat/front_ends/__init__.py +14 -0
- nat/front_ends/console/__init__.py +14 -0
- nat/front_ends/console/authentication_flow_handler.py +285 -0
- nat/front_ends/console/console_front_end_config.py +32 -0
- nat/front_ends/console/console_front_end_plugin.py +108 -0
- nat/front_ends/console/register.py +25 -0
- nat/front_ends/cron/__init__.py +14 -0
- nat/front_ends/fastapi/__init__.py +14 -0
- nat/front_ends/fastapi/auth_flow_handlers/__init__.py +0 -0
- nat/front_ends/fastapi/auth_flow_handlers/http_flow_handler.py +27 -0
- nat/front_ends/fastapi/auth_flow_handlers/websocket_flow_handler.py +142 -0
- nat/front_ends/fastapi/dask_client_mixin.py +65 -0
- nat/front_ends/fastapi/fastapi_front_end_config.py +272 -0
- nat/front_ends/fastapi/fastapi_front_end_controller.py +68 -0
- nat/front_ends/fastapi/fastapi_front_end_plugin.py +247 -0
- nat/front_ends/fastapi/fastapi_front_end_plugin_worker.py +1257 -0
- nat/front_ends/fastapi/html_snippets/__init__.py +14 -0
- nat/front_ends/fastapi/html_snippets/auth_code_grant_success.py +35 -0
- nat/front_ends/fastapi/intermediate_steps_subscriber.py +80 -0
- nat/front_ends/fastapi/job_store.py +602 -0
- nat/front_ends/fastapi/main.py +64 -0
- nat/front_ends/fastapi/message_handler.py +344 -0
- nat/front_ends/fastapi/message_validator.py +351 -0
- nat/front_ends/fastapi/register.py +25 -0
- nat/front_ends/fastapi/response_helpers.py +195 -0
- nat/front_ends/fastapi/step_adaptor.py +319 -0
- nat/front_ends/fastapi/utils.py +57 -0
- nat/front_ends/mcp/__init__.py +14 -0
- nat/front_ends/mcp/introspection_token_verifier.py +73 -0
- nat/front_ends/mcp/mcp_front_end_config.py +90 -0
- nat/front_ends/mcp/mcp_front_end_plugin.py +113 -0
- nat/front_ends/mcp/mcp_front_end_plugin_worker.py +268 -0
- nat/front_ends/mcp/memory_profiler.py +320 -0
- nat/front_ends/mcp/register.py +27 -0
- nat/front_ends/mcp/tool_converter.py +290 -0
- nat/front_ends/register.py +21 -0
- nat/front_ends/simple_base/__init__.py +14 -0
- nat/front_ends/simple_base/simple_front_end_plugin_base.py +56 -0
- nat/llm/__init__.py +0 -0
- nat/llm/aws_bedrock_llm.py +69 -0
- nat/llm/azure_openai_llm.py +57 -0
- nat/llm/litellm_llm.py +69 -0
- nat/llm/nim_llm.py +58 -0
- nat/llm/openai_llm.py +54 -0
- nat/llm/register.py +27 -0
- nat/llm/utils/__init__.py +14 -0
- nat/llm/utils/env_config_value.py +93 -0
- nat/llm/utils/error.py +17 -0
- nat/llm/utils/thinking.py +215 -0
- nat/memory/__init__.py +20 -0
- nat/memory/interfaces.py +183 -0
- nat/memory/models.py +112 -0
- nat/meta/pypi.md +58 -0
- nat/object_store/__init__.py +20 -0
- nat/object_store/in_memory_object_store.py +76 -0
- nat/object_store/interfaces.py +84 -0
- nat/object_store/models.py +38 -0
- nat/object_store/register.py +19 -0
- nat/observability/__init__.py +14 -0
- nat/observability/exporter/__init__.py +14 -0
- nat/observability/exporter/base_exporter.py +449 -0
- nat/observability/exporter/exporter.py +78 -0
- nat/observability/exporter/file_exporter.py +33 -0
- nat/observability/exporter/processing_exporter.py +550 -0
- nat/observability/exporter/raw_exporter.py +52 -0
- nat/observability/exporter/span_exporter.py +308 -0
- nat/observability/exporter_manager.py +335 -0
- nat/observability/mixin/__init__.py +14 -0
- nat/observability/mixin/batch_config_mixin.py +26 -0
- nat/observability/mixin/collector_config_mixin.py +23 -0
- nat/observability/mixin/file_mixin.py +288 -0
- nat/observability/mixin/file_mode.py +23 -0
- nat/observability/mixin/redaction_config_mixin.py +42 -0
- nat/observability/mixin/resource_conflict_mixin.py +134 -0
- nat/observability/mixin/serialize_mixin.py +61 -0
- nat/observability/mixin/tagging_config_mixin.py +62 -0
- nat/observability/mixin/type_introspection_mixin.py +496 -0
- nat/observability/processor/__init__.py +14 -0
- nat/observability/processor/batching_processor.py +308 -0
- nat/observability/processor/callback_processor.py +42 -0
- nat/observability/processor/falsy_batch_filter_processor.py +55 -0
- nat/observability/processor/intermediate_step_serializer.py +28 -0
- nat/observability/processor/processor.py +74 -0
- nat/observability/processor/processor_factory.py +70 -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 +68 -0
- nat/observability/register.py +114 -0
- nat/observability/utils/__init__.py +14 -0
- nat/observability/utils/dict_utils.py +236 -0
- nat/observability/utils/time_utils.py +31 -0
- nat/plugins/.namespace +1 -0
- nat/profiler/__init__.py +0 -0
- nat/profiler/calc/__init__.py +14 -0
- nat/profiler/calc/calc_runner.py +626 -0
- nat/profiler/calc/calculations.py +288 -0
- nat/profiler/calc/data_models.py +188 -0
- nat/profiler/calc/plot.py +345 -0
- nat/profiler/callbacks/__init__.py +0 -0
- nat/profiler/callbacks/agno_callback_handler.py +295 -0
- nat/profiler/callbacks/base_callback_class.py +20 -0
- nat/profiler/callbacks/langchain_callback_handler.py +297 -0
- nat/profiler/callbacks/llama_index_callback_handler.py +205 -0
- nat/profiler/callbacks/semantic_kernel_callback_handler.py +238 -0
- nat/profiler/callbacks/token_usage_base_model.py +27 -0
- nat/profiler/data_frame_row.py +51 -0
- nat/profiler/data_models.py +24 -0
- nat/profiler/decorators/__init__.py +0 -0
- nat/profiler/decorators/framework_wrapper.py +180 -0
- nat/profiler/decorators/function_tracking.py +411 -0
- nat/profiler/forecasting/__init__.py +0 -0
- nat/profiler/forecasting/config.py +18 -0
- nat/profiler/forecasting/model_trainer.py +75 -0
- nat/profiler/forecasting/models/__init__.py +22 -0
- nat/profiler/forecasting/models/forecasting_base_model.py +42 -0
- nat/profiler/forecasting/models/linear_model.py +197 -0
- nat/profiler/forecasting/models/random_forest_regressor.py +269 -0
- nat/profiler/inference_metrics_model.py +28 -0
- nat/profiler/inference_optimization/__init__.py +0 -0
- nat/profiler/inference_optimization/bottleneck_analysis/__init__.py +0 -0
- nat/profiler/inference_optimization/bottleneck_analysis/nested_stack_analysis.py +460 -0
- nat/profiler/inference_optimization/bottleneck_analysis/simple_stack_analysis.py +258 -0
- nat/profiler/inference_optimization/data_models.py +386 -0
- nat/profiler/inference_optimization/experimental/__init__.py +0 -0
- nat/profiler/inference_optimization/experimental/concurrency_spike_analysis.py +468 -0
- nat/profiler/inference_optimization/experimental/prefix_span_analysis.py +404 -0
- nat/profiler/inference_optimization/llm_metrics.py +212 -0
- nat/profiler/inference_optimization/prompt_caching.py +163 -0
- nat/profiler/inference_optimization/token_uniqueness.py +107 -0
- nat/profiler/inference_optimization/workflow_runtimes.py +72 -0
- nat/profiler/intermediate_property_adapter.py +102 -0
- 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 +153 -0
- nat/profiler/parameter_optimization/parameter_selection.py +107 -0
- nat/profiler/parameter_optimization/pareto_visualizer.py +380 -0
- nat/profiler/parameter_optimization/prompt_optimizer.py +384 -0
- nat/profiler/parameter_optimization/update_helpers.py +66 -0
- nat/profiler/profile_runner.py +478 -0
- nat/profiler/utils.py +186 -0
- nat/registry_handlers/__init__.py +0 -0
- nat/registry_handlers/local/__init__.py +0 -0
- nat/registry_handlers/local/local_handler.py +176 -0
- nat/registry_handlers/local/register_local.py +37 -0
- nat/registry_handlers/metadata_factory.py +60 -0
- nat/registry_handlers/package_utils.py +570 -0
- nat/registry_handlers/pypi/__init__.py +0 -0
- nat/registry_handlers/pypi/pypi_handler.py +248 -0
- nat/registry_handlers/pypi/register_pypi.py +40 -0
- nat/registry_handlers/register.py +20 -0
- nat/registry_handlers/registry_handler_base.py +157 -0
- nat/registry_handlers/rest/__init__.py +0 -0
- nat/registry_handlers/rest/register_rest.py +56 -0
- nat/registry_handlers/rest/rest_handler.py +236 -0
- nat/registry_handlers/schemas/__init__.py +0 -0
- nat/registry_handlers/schemas/headers.py +42 -0
- nat/registry_handlers/schemas/package.py +68 -0
- nat/registry_handlers/schemas/publish.py +68 -0
- nat/registry_handlers/schemas/pull.py +82 -0
- nat/registry_handlers/schemas/remove.py +36 -0
- nat/registry_handlers/schemas/search.py +91 -0
- nat/registry_handlers/schemas/status.py +47 -0
- nat/retriever/__init__.py +0 -0
- nat/retriever/interface.py +41 -0
- nat/retriever/milvus/__init__.py +14 -0
- nat/retriever/milvus/register.py +81 -0
- nat/retriever/milvus/retriever.py +228 -0
- nat/retriever/models.py +77 -0
- nat/retriever/nemo_retriever/__init__.py +14 -0
- nat/retriever/nemo_retriever/register.py +60 -0
- nat/retriever/nemo_retriever/retriever.py +190 -0
- nat/retriever/register.py +21 -0
- nat/runtime/__init__.py +14 -0
- nat/runtime/loader.py +220 -0
- nat/runtime/runner.py +292 -0
- nat/runtime/session.py +223 -0
- nat/runtime/user_metadata.py +130 -0
- nat/settings/__init__.py +0 -0
- nat/settings/global_settings.py +329 -0
- nat/test/.namespace +1 -0
- nat/tool/__init__.py +0 -0
- nat/tool/chat_completion.py +77 -0
- nat/tool/code_execution/README.md +151 -0
- nat/tool/code_execution/__init__.py +0 -0
- nat/tool/code_execution/code_sandbox.py +267 -0
- nat/tool/code_execution/local_sandbox/.gitignore +1 -0
- nat/tool/code_execution/local_sandbox/Dockerfile.sandbox +60 -0
- nat/tool/code_execution/local_sandbox/__init__.py +13 -0
- nat/tool/code_execution/local_sandbox/local_sandbox_server.py +198 -0
- nat/tool/code_execution/local_sandbox/sandbox.requirements.txt +6 -0
- nat/tool/code_execution/local_sandbox/start_local_sandbox.sh +50 -0
- nat/tool/code_execution/register.py +74 -0
- nat/tool/code_execution/test_code_execution_sandbox.py +414 -0
- nat/tool/code_execution/utils.py +100 -0
- nat/tool/datetime_tools.py +82 -0
- nat/tool/document_search.py +141 -0
- nat/tool/github_tools.py +450 -0
- nat/tool/memory_tools/__init__.py +0 -0
- nat/tool/memory_tools/add_memory_tool.py +79 -0
- nat/tool/memory_tools/delete_memory_tool.py +66 -0
- nat/tool/memory_tools/get_memory_tool.py +72 -0
- nat/tool/nvidia_rag.py +95 -0
- nat/tool/register.py +31 -0
- nat/tool/retriever.py +95 -0
- nat/tool/server_tools.py +66 -0
- nat/utils/__init__.py +0 -0
- nat/utils/callable_utils.py +70 -0
- nat/utils/data_models/__init__.py +0 -0
- nat/utils/data_models/schema_validator.py +58 -0
- nat/utils/debugging_utils.py +43 -0
- nat/utils/decorators.py +210 -0
- nat/utils/dump_distro_mapping.py +32 -0
- nat/utils/exception_handlers/__init__.py +0 -0
- nat/utils/exception_handlers/automatic_retries.py +342 -0
- nat/utils/exception_handlers/schemas.py +114 -0
- nat/utils/io/__init__.py +0 -0
- nat/utils/io/model_processing.py +28 -0
- nat/utils/io/yaml_tools.py +119 -0
- nat/utils/log_levels.py +25 -0
- nat/utils/log_utils.py +37 -0
- nat/utils/metadata_utils.py +74 -0
- nat/utils/optional_imports.py +142 -0
- nat/utils/producer_consumer_queue.py +178 -0
- nat/utils/reactive/__init__.py +0 -0
- nat/utils/reactive/base/__init__.py +0 -0
- nat/utils/reactive/base/observable_base.py +65 -0
- nat/utils/reactive/base/observer_base.py +55 -0
- nat/utils/reactive/base/subject_base.py +79 -0
- nat/utils/reactive/observable.py +59 -0
- nat/utils/reactive/observer.py +76 -0
- nat/utils/reactive/subject.py +131 -0
- nat/utils/reactive/subscription.py +49 -0
- nat/utils/settings/__init__.py +0 -0
- nat/utils/settings/global_settings.py +195 -0
- nat/utils/string_utils.py +38 -0
- nat/utils/type_converter.py +299 -0
- nat/utils/type_utils.py +488 -0
- nat/utils/url_utils.py +27 -0
- nvidia_nat-1.1.0a20251020.dist-info/METADATA +195 -0
- nvidia_nat-1.1.0a20251020.dist-info/RECORD +480 -0
- nvidia_nat-1.1.0a20251020.dist-info/WHEEL +5 -0
- nvidia_nat-1.1.0a20251020.dist-info/entry_points.txt +22 -0
- nvidia_nat-1.1.0a20251020.dist-info/licenses/LICENSE-3rd-party.txt +5478 -0
- nvidia_nat-1.1.0a20251020.dist-info/licenses/LICENSE.md +201 -0
- nvidia_nat-1.1.0a20251020.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-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
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
import typing
|
|
20
|
+
from abc import abstractmethod
|
|
21
|
+
from typing import TypeVar
|
|
22
|
+
|
|
23
|
+
from nat.data_models.intermediate_step import IntermediateStep
|
|
24
|
+
from nat.data_models.intermediate_step import IntermediateStepState
|
|
25
|
+
from nat.data_models.intermediate_step import TraceMetadata
|
|
26
|
+
from nat.data_models.span import MimeTypes
|
|
27
|
+
from nat.data_models.span import Span
|
|
28
|
+
from nat.data_models.span import SpanAttributes
|
|
29
|
+
from nat.data_models.span import SpanContext
|
|
30
|
+
from nat.data_models.span import event_type_to_span_kind
|
|
31
|
+
from nat.observability.exporter.base_exporter import IsolatedAttribute
|
|
32
|
+
from nat.observability.exporter.processing_exporter import ProcessingExporter
|
|
33
|
+
from nat.observability.mixin.serialize_mixin import SerializeMixin
|
|
34
|
+
from nat.observability.utils.dict_utils import merge_dicts
|
|
35
|
+
from nat.observability.utils.time_utils import ns_timestamp
|
|
36
|
+
from nat.utils.type_utils import override
|
|
37
|
+
|
|
38
|
+
if typing.TYPE_CHECKING:
|
|
39
|
+
from nat.builder.context import ContextState
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
InputSpanT = TypeVar("InputSpanT")
|
|
44
|
+
OutputSpanT = TypeVar("OutputSpanT")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class SpanExporter(ProcessingExporter[InputSpanT, OutputSpanT], SerializeMixin):
|
|
48
|
+
"""Abstract base class for span exporters with processing pipeline support.
|
|
49
|
+
|
|
50
|
+
This class specializes ProcessingExporter for span-based telemetry export. It converts
|
|
51
|
+
IntermediateStep events into Span objects and supports processing pipelines for
|
|
52
|
+
span transformation before export.
|
|
53
|
+
|
|
54
|
+
The generic types work as follows:
|
|
55
|
+
- InputSpanT: The type of spans that enter the processing pipeline (typically Span)
|
|
56
|
+
- OutputSpanT: The type of spans after processing through the pipeline (e.g., OtelSpan)
|
|
57
|
+
|
|
58
|
+
Key Features:
|
|
59
|
+
- Automatic span creation from IntermediateStep events
|
|
60
|
+
- Span lifecycle management (start/end event tracking)
|
|
61
|
+
- Processing pipeline support via ProcessingExporter
|
|
62
|
+
- Metadata and attribute handling
|
|
63
|
+
- Usage information tracking
|
|
64
|
+
- Automatic isolation of mutable state for concurrent execution using descriptors
|
|
65
|
+
|
|
66
|
+
Inheritance Hierarchy:
|
|
67
|
+
- BaseExporter: Core event subscription and lifecycle management + DescriptorIsolationMixin
|
|
68
|
+
- ProcessingExporter: Adds processor pipeline functionality
|
|
69
|
+
- SpanExporter: Specializes for span creation and export
|
|
70
|
+
|
|
71
|
+
Event Processing Flow:
|
|
72
|
+
1. IntermediateStep (START) → Create Span → Add to tracking
|
|
73
|
+
2. IntermediateStep (END) → Complete Span → Process through pipeline → Export
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
context_state: `ContextState`, optional
|
|
78
|
+
The context state to use for the exporter. Defaults to None.
|
|
79
|
+
span_prefix: `str`, optional
|
|
80
|
+
The prefix name to use for span attributes. If `None` the value of the `NAT_SPAN_PREFIX` environment
|
|
81
|
+
variable is used. Defaults to `"nat"` if neither are defined.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
# Use descriptors for automatic isolation of span-specific state
|
|
85
|
+
_outstanding_spans: IsolatedAttribute[dict] = IsolatedAttribute(dict)
|
|
86
|
+
_span_stack: IsolatedAttribute[dict] = IsolatedAttribute(dict)
|
|
87
|
+
_metadata_stack: IsolatedAttribute[dict] = IsolatedAttribute(dict)
|
|
88
|
+
|
|
89
|
+
def __init__(self, context_state: "ContextState | None" = None, span_prefix: str | None = None):
|
|
90
|
+
super().__init__(context_state=context_state)
|
|
91
|
+
if span_prefix is None:
|
|
92
|
+
span_prefix = os.getenv("NAT_SPAN_PREFIX", "nat").strip() or "nat"
|
|
93
|
+
|
|
94
|
+
self._span_prefix = span_prefix
|
|
95
|
+
|
|
96
|
+
@abstractmethod
|
|
97
|
+
async def export_processed(self, item: OutputSpanT) -> None:
|
|
98
|
+
"""Export the processed span.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
item (OutputSpanT): The processed span to export.
|
|
102
|
+
"""
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
@override
|
|
106
|
+
def export(self, event: IntermediateStep) -> None:
|
|
107
|
+
"""The main logic that reacts to each IntermediateStep.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
event (IntermediateStep): The event to process.
|
|
111
|
+
"""
|
|
112
|
+
if not isinstance(event, IntermediateStep):
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
if (event.event_state == IntermediateStepState.START):
|
|
116
|
+
self._process_start_event(event)
|
|
117
|
+
elif (event.event_state == IntermediateStepState.END):
|
|
118
|
+
self._process_end_event(event)
|
|
119
|
+
|
|
120
|
+
def _process_start_event(self, event: IntermediateStep):
|
|
121
|
+
"""Process the start event of an intermediate step.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
event (IntermediateStep): The event to process.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
parent_span = None
|
|
128
|
+
span_ctx = None
|
|
129
|
+
workflow_trace_id = self._context_state.workflow_trace_id.get()
|
|
130
|
+
|
|
131
|
+
# Look up the parent span to establish hierarchy
|
|
132
|
+
# event.parent_id is the UUID of the last START step with a different UUID from current step
|
|
133
|
+
# This maintains proper parent-child relationships in the span tree
|
|
134
|
+
# Skip lookup if parent_id is "root" (indicates this is a top-level span)
|
|
135
|
+
if len(self._span_stack) > 0 and event.parent_id and event.parent_id != "root":
|
|
136
|
+
|
|
137
|
+
parent_span = self._span_stack.get(event.parent_id, None)
|
|
138
|
+
if parent_span is None:
|
|
139
|
+
logger.warning("No parent span found for step %s", event.UUID)
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
parent_span = parent_span.model_copy() if isinstance(parent_span, Span) else None
|
|
143
|
+
if parent_span and parent_span.context:
|
|
144
|
+
span_ctx = SpanContext(trace_id=parent_span.context.trace_id)
|
|
145
|
+
# No parent: adopt workflow trace id if available to keep all spans in the same trace
|
|
146
|
+
if span_ctx is None and workflow_trace_id:
|
|
147
|
+
span_ctx = SpanContext(trace_id=workflow_trace_id)
|
|
148
|
+
|
|
149
|
+
# Extract start/end times from the step
|
|
150
|
+
# By convention, `span_event_timestamp` is the time we started, `event_timestamp` is the time we ended.
|
|
151
|
+
# If span_event_timestamp is missing, we default to event_timestamp (meaning zero-length).
|
|
152
|
+
s_ts = event.payload.span_event_timestamp or event.payload.event_timestamp
|
|
153
|
+
start_ns = ns_timestamp(s_ts)
|
|
154
|
+
|
|
155
|
+
# Optional: embed the LLM/tool name if present
|
|
156
|
+
if event.payload.name:
|
|
157
|
+
sub_span_name = f"{event.payload.name}"
|
|
158
|
+
else:
|
|
159
|
+
sub_span_name = f"{event.payload.event_type}"
|
|
160
|
+
|
|
161
|
+
# Prefer parent/context trace id for attribute, else workflow trace id
|
|
162
|
+
_attr_trace_id = None
|
|
163
|
+
if span_ctx is not None:
|
|
164
|
+
_attr_trace_id = span_ctx.trace_id
|
|
165
|
+
elif parent_span and parent_span.context:
|
|
166
|
+
_attr_trace_id = parent_span.context.trace_id
|
|
167
|
+
elif workflow_trace_id:
|
|
168
|
+
_attr_trace_id = workflow_trace_id
|
|
169
|
+
|
|
170
|
+
attributes = {
|
|
171
|
+
f"{self._span_prefix}.event_type":
|
|
172
|
+
event.payload.event_type.value,
|
|
173
|
+
f"{self._span_prefix}.function.id":
|
|
174
|
+
event.function_ancestry.function_id if event.function_ancestry else "unknown",
|
|
175
|
+
f"{self._span_prefix}.function.name":
|
|
176
|
+
event.function_ancestry.function_name if event.function_ancestry else "unknown",
|
|
177
|
+
f"{self._span_prefix}.subspan.name":
|
|
178
|
+
event.payload.name or "",
|
|
179
|
+
f"{self._span_prefix}.event_timestamp":
|
|
180
|
+
event.event_timestamp,
|
|
181
|
+
f"{self._span_prefix}.framework":
|
|
182
|
+
event.payload.framework.value if event.payload.framework else "unknown",
|
|
183
|
+
f"{self._span_prefix}.conversation.id":
|
|
184
|
+
self._context_state.conversation_id.get() or "unknown",
|
|
185
|
+
f"{self._span_prefix}.workflow.run_id":
|
|
186
|
+
self._context_state.workflow_run_id.get() or "unknown",
|
|
187
|
+
f"{self._span_prefix}.workflow.trace_id": (f"{_attr_trace_id:032x}" if _attr_trace_id else "unknown"),
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
sub_span = Span(name=sub_span_name,
|
|
191
|
+
parent=parent_span,
|
|
192
|
+
context=span_ctx,
|
|
193
|
+
attributes=attributes,
|
|
194
|
+
start_time=start_ns)
|
|
195
|
+
|
|
196
|
+
span_kind = event_type_to_span_kind(event.event_type)
|
|
197
|
+
sub_span.set_attribute(f"{self._span_prefix}.span.kind", span_kind.value)
|
|
198
|
+
|
|
199
|
+
if event.payload.data and event.payload.data.input:
|
|
200
|
+
match = re.search(r"Human:\s*Question:\s*(.*)", str(event.payload.data.input))
|
|
201
|
+
if match:
|
|
202
|
+
human_question = match.group(1).strip()
|
|
203
|
+
sub_span.set_attribute(SpanAttributes.INPUT_VALUE.value, human_question)
|
|
204
|
+
else:
|
|
205
|
+
serialized_input, is_json = self._serialize_payload(event.payload.data.input)
|
|
206
|
+
sub_span.set_attribute(SpanAttributes.INPUT_VALUE.value, serialized_input)
|
|
207
|
+
sub_span.set_attribute(SpanAttributes.INPUT_MIME_TYPE.value,
|
|
208
|
+
MimeTypes.JSON.value if is_json else MimeTypes.TEXT.value)
|
|
209
|
+
|
|
210
|
+
# Add metadata to the metadata stack
|
|
211
|
+
start_metadata = event.payload.metadata or {}
|
|
212
|
+
|
|
213
|
+
if isinstance(start_metadata, dict):
|
|
214
|
+
self._metadata_stack[event.UUID] = start_metadata # type: ignore
|
|
215
|
+
elif isinstance(start_metadata, TraceMetadata):
|
|
216
|
+
self._metadata_stack[event.UUID] = start_metadata.model_dump() # type: ignore
|
|
217
|
+
else:
|
|
218
|
+
logger.warning("Invalid metadata type for step %s", event.UUID)
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
self._span_stack[event.UUID] = sub_span # type: ignore
|
|
222
|
+
self._outstanding_spans[event.UUID] = sub_span # type: ignore
|
|
223
|
+
|
|
224
|
+
logger.debug(
|
|
225
|
+
"Added span to tracking (outstanding: %d, stack: %d, event_id: %s)",
|
|
226
|
+
len(self._outstanding_spans), # type: ignore
|
|
227
|
+
len(self._span_stack), # type: ignore
|
|
228
|
+
event.UUID)
|
|
229
|
+
|
|
230
|
+
def _process_end_event(self, event: IntermediateStep):
|
|
231
|
+
"""Process the end event of an intermediate step.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
event (IntermediateStep): The event to process.
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
# Find the subspan that was created in the start event
|
|
238
|
+
sub_span: Span | None = self._outstanding_spans.pop(event.UUID, None) # type: ignore
|
|
239
|
+
|
|
240
|
+
if sub_span is None:
|
|
241
|
+
logger.warning("No subspan found for step %s", event.UUID)
|
|
242
|
+
return
|
|
243
|
+
|
|
244
|
+
self._span_stack.pop(event.UUID, None) # type: ignore
|
|
245
|
+
|
|
246
|
+
# Optionally add more attributes from usage_info or data
|
|
247
|
+
usage_info = event.payload.usage_info
|
|
248
|
+
if usage_info:
|
|
249
|
+
sub_span.set_attribute(SpanAttributes.NAT_USAGE_NUM_LLM_CALLS.value,
|
|
250
|
+
usage_info.num_llm_calls if usage_info.num_llm_calls else 0)
|
|
251
|
+
sub_span.set_attribute(SpanAttributes.NAT_USAGE_SECONDS_BETWEEN_CALLS.value,
|
|
252
|
+
usage_info.seconds_between_calls if usage_info.seconds_between_calls else 0)
|
|
253
|
+
sub_span.set_attribute(SpanAttributes.LLM_TOKEN_COUNT_PROMPT.value,
|
|
254
|
+
usage_info.token_usage.prompt_tokens if usage_info.token_usage else 0)
|
|
255
|
+
sub_span.set_attribute(SpanAttributes.LLM_TOKEN_COUNT_COMPLETION.value,
|
|
256
|
+
usage_info.token_usage.completion_tokens if usage_info.token_usage else 0)
|
|
257
|
+
sub_span.set_attribute(SpanAttributes.LLM_TOKEN_COUNT_TOTAL.value,
|
|
258
|
+
usage_info.token_usage.total_tokens if usage_info.token_usage else 0)
|
|
259
|
+
|
|
260
|
+
if event.payload.data and event.payload.data.output is not None:
|
|
261
|
+
serialized_output, is_json = self._serialize_payload(event.payload.data.output)
|
|
262
|
+
sub_span.set_attribute(SpanAttributes.OUTPUT_VALUE.value, serialized_output)
|
|
263
|
+
sub_span.set_attribute(SpanAttributes.OUTPUT_MIME_TYPE.value,
|
|
264
|
+
MimeTypes.JSON.value if is_json else MimeTypes.TEXT.value)
|
|
265
|
+
|
|
266
|
+
# Merge metadata from start event with end event metadata
|
|
267
|
+
start_metadata = self._metadata_stack.pop(event.UUID) # type: ignore
|
|
268
|
+
|
|
269
|
+
if start_metadata is None:
|
|
270
|
+
logger.warning("No metadata found for step %s", event.UUID)
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
end_metadata = event.payload.metadata or {}
|
|
274
|
+
|
|
275
|
+
if not isinstance(end_metadata, dict | TraceMetadata):
|
|
276
|
+
logger.warning("Invalid metadata type for step %s", event.UUID)
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
if isinstance(end_metadata, TraceMetadata):
|
|
280
|
+
end_metadata = end_metadata.model_dump()
|
|
281
|
+
|
|
282
|
+
merged_metadata = merge_dicts(start_metadata, end_metadata)
|
|
283
|
+
serialized_metadata, is_json = self._serialize_payload(merged_metadata)
|
|
284
|
+
sub_span.set_attribute(f"{self._span_prefix}.metadata", serialized_metadata)
|
|
285
|
+
sub_span.set_attribute(f"{self._span_prefix}.metadata.mime_type",
|
|
286
|
+
MimeTypes.JSON.value if is_json else MimeTypes.TEXT.value)
|
|
287
|
+
|
|
288
|
+
end_ns = ns_timestamp(event.payload.event_timestamp)
|
|
289
|
+
|
|
290
|
+
# End the subspan
|
|
291
|
+
sub_span.end(end_time=end_ns)
|
|
292
|
+
|
|
293
|
+
# Export the span with processing pipeline
|
|
294
|
+
self._create_export_task(self._export_with_processing(sub_span)) # type: ignore
|
|
295
|
+
|
|
296
|
+
@override
|
|
297
|
+
async def _cleanup(self):
|
|
298
|
+
"""Clean up any remaining spans."""
|
|
299
|
+
if self._outstanding_spans: # type: ignore
|
|
300
|
+
logger.warning("Not all spans were closed. Remaining: %s", self._outstanding_spans) # type: ignore
|
|
301
|
+
|
|
302
|
+
for span_info in self._outstanding_spans.values(): # type: ignore
|
|
303
|
+
span_info.end()
|
|
304
|
+
|
|
305
|
+
self._outstanding_spans.clear() # type: ignore
|
|
306
|
+
self._span_stack.clear() # type: ignore
|
|
307
|
+
self._metadata_stack.clear() # type: ignore
|
|
308
|
+
await super()._cleanup()
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-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 asyncio
|
|
17
|
+
import logging
|
|
18
|
+
from contextlib import asynccontextmanager
|
|
19
|
+
|
|
20
|
+
from nat.builder.context import ContextState
|
|
21
|
+
from nat.observability.exporter.base_exporter import BaseExporter
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ExporterManager:
|
|
27
|
+
"""
|
|
28
|
+
Manages the lifecycle of asynchronous exporters.
|
|
29
|
+
|
|
30
|
+
ExporterManager maintains a registry of exporters, allowing for dynamic addition and removal. It provides
|
|
31
|
+
methods to start and stop all registered exporters concurrently, ensuring proper synchronization and
|
|
32
|
+
lifecycle management. The manager is designed to prevent race conditions during exporter operations and to
|
|
33
|
+
handle exporter tasks in an asyncio event loop.
|
|
34
|
+
|
|
35
|
+
Each workflow execution gets its own ExporterManager instance to manage the lifecycle of exporters
|
|
36
|
+
during that workflow's execution.
|
|
37
|
+
|
|
38
|
+
Exporters added after `start()` is called will not be started automatically. They will only be
|
|
39
|
+
started on the next lifecycle (i.e., after a stop and subsequent start).
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
shutdown_timeout (int, optional): Maximum time in seconds to wait for exporters to shut down gracefully.
|
|
43
|
+
Defaults to 120 seconds.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, shutdown_timeout: int = 120):
|
|
47
|
+
"""Initialize the ExporterManager."""
|
|
48
|
+
self._tasks: dict[str, asyncio.Task] = {}
|
|
49
|
+
self._running: bool = False
|
|
50
|
+
self._exporter_registry: dict[str, BaseExporter] = {}
|
|
51
|
+
self._is_registry_shared: bool = False
|
|
52
|
+
self._lock: asyncio.Lock = asyncio.Lock()
|
|
53
|
+
self._shutdown_event: asyncio.Event = asyncio.Event()
|
|
54
|
+
self._shutdown_timeout: int = shutdown_timeout
|
|
55
|
+
# Track isolated exporters for proper cleanup
|
|
56
|
+
self._active_isolated_exporters: dict[str, BaseExporter] = {}
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def _create_with_shared_registry(cls, shutdown_timeout: int,
|
|
60
|
+
shared_registry: dict[str, BaseExporter]) -> "ExporterManager":
|
|
61
|
+
"""Internal factory method for creating instances with shared registry."""
|
|
62
|
+
instance = cls.__new__(cls)
|
|
63
|
+
instance._tasks = {}
|
|
64
|
+
instance._running = False
|
|
65
|
+
instance._exporter_registry = shared_registry
|
|
66
|
+
instance._is_registry_shared = True
|
|
67
|
+
instance._lock = asyncio.Lock()
|
|
68
|
+
instance._shutdown_event = asyncio.Event()
|
|
69
|
+
instance._shutdown_timeout = shutdown_timeout
|
|
70
|
+
instance._active_isolated_exporters = {}
|
|
71
|
+
return instance
|
|
72
|
+
|
|
73
|
+
def _ensure_registry_owned(self):
|
|
74
|
+
"""Ensure we own the registry (copy-on-write)."""
|
|
75
|
+
if self._is_registry_shared:
|
|
76
|
+
self._exporter_registry = self._exporter_registry.copy()
|
|
77
|
+
self._is_registry_shared = False
|
|
78
|
+
|
|
79
|
+
def add_exporter(self, name: str, exporter: BaseExporter) -> None:
|
|
80
|
+
"""
|
|
81
|
+
Add an exporter to the manager.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
name (str): The unique name for the exporter.
|
|
85
|
+
exporter (BaseExporter): The exporter instance to add.
|
|
86
|
+
"""
|
|
87
|
+
self._ensure_registry_owned()
|
|
88
|
+
|
|
89
|
+
if name in self._exporter_registry:
|
|
90
|
+
logger.warning("Exporter '%s' already registered. Overwriting.", name)
|
|
91
|
+
|
|
92
|
+
self._exporter_registry[name] = exporter
|
|
93
|
+
|
|
94
|
+
def remove_exporter(self, name: str) -> None:
|
|
95
|
+
"""
|
|
96
|
+
Remove an exporter from the manager.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
name (str): The name of the exporter to remove.
|
|
100
|
+
"""
|
|
101
|
+
self._ensure_registry_owned()
|
|
102
|
+
if name in self._exporter_registry:
|
|
103
|
+
del self._exporter_registry[name]
|
|
104
|
+
else:
|
|
105
|
+
raise ValueError(f"Cannot remove exporter '{name}' because it is not registered.")
|
|
106
|
+
|
|
107
|
+
def get_exporter(self, name: str) -> BaseExporter:
|
|
108
|
+
"""
|
|
109
|
+
Get an exporter instance by name.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
name (str): The name of the exporter to retrieve.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
BaseExporter: The exporter instance if found, otherwise raises a ValueError.
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
ValueError: If the exporter is not found.
|
|
119
|
+
"""
|
|
120
|
+
exporter = self._exporter_registry.get(name, None)
|
|
121
|
+
|
|
122
|
+
if exporter is not None:
|
|
123
|
+
return exporter
|
|
124
|
+
|
|
125
|
+
raise ValueError(f"Cannot get exporter '{name}' because it is not registered.")
|
|
126
|
+
|
|
127
|
+
async def get_all_exporters(self) -> dict[str, BaseExporter]:
|
|
128
|
+
"""
|
|
129
|
+
Get all registered exporters instances.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
dict[str, BaseExporter]: A dictionary mapping exporter names to exporter instances.
|
|
133
|
+
"""
|
|
134
|
+
return self._exporter_registry
|
|
135
|
+
|
|
136
|
+
def create_isolated_exporters(self, context_state: ContextState | None = None) -> dict[str, BaseExporter]:
|
|
137
|
+
"""
|
|
138
|
+
Create isolated copies of all exporters for concurrent execution.
|
|
139
|
+
|
|
140
|
+
This uses copy-on-write to efficiently create isolated instances that share
|
|
141
|
+
expensive resources but have separate mutable state.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
context_state (ContextState | None, optional): The isolated context state for the new exporter instances.
|
|
145
|
+
If not provided, a new context state will be created.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
dict[str, BaseExporter]: Dictionary of isolated exporter instances
|
|
149
|
+
"""
|
|
150
|
+
# Provide default context state if None
|
|
151
|
+
if context_state is None:
|
|
152
|
+
context_state = ContextState.get()
|
|
153
|
+
|
|
154
|
+
isolated_exporters = {}
|
|
155
|
+
for name, exporter in self._exporter_registry.items():
|
|
156
|
+
if hasattr(exporter, 'create_isolated_instance'):
|
|
157
|
+
isolated_exporters[name] = exporter.create_isolated_instance(context_state)
|
|
158
|
+
else:
|
|
159
|
+
# Fallback for exporters that don't support isolation
|
|
160
|
+
logger.warning("Exporter '%s' doesn't support isolation, using shared instance", name)
|
|
161
|
+
isolated_exporters[name] = exporter
|
|
162
|
+
return isolated_exporters
|
|
163
|
+
|
|
164
|
+
async def _cleanup_isolated_exporters(self):
|
|
165
|
+
"""Explicitly clean up isolated exporter instances."""
|
|
166
|
+
if not self._active_isolated_exporters:
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
logger.debug("Cleaning up %d isolated exporters", len(self._active_isolated_exporters))
|
|
170
|
+
|
|
171
|
+
cleanup_tasks = []
|
|
172
|
+
for name, exporter in self._active_isolated_exporters.items():
|
|
173
|
+
try:
|
|
174
|
+
# Only clean up isolated instances that have a stop method
|
|
175
|
+
if hasattr(exporter, 'stop') and exporter.is_isolated_instance:
|
|
176
|
+
cleanup_tasks.append(self._cleanup_single_exporter(name, exporter))
|
|
177
|
+
else:
|
|
178
|
+
logger.debug("Skipping cleanup for non-isolated exporter '%s'", name)
|
|
179
|
+
except Exception as e:
|
|
180
|
+
logger.exception("Error preparing cleanup for isolated exporter '%s': %s", name, e)
|
|
181
|
+
|
|
182
|
+
if cleanup_tasks:
|
|
183
|
+
# Run cleanup tasks concurrently with timeout
|
|
184
|
+
try:
|
|
185
|
+
await asyncio.wait_for(asyncio.gather(*cleanup_tasks, return_exceptions=True),
|
|
186
|
+
timeout=self._shutdown_timeout)
|
|
187
|
+
except TimeoutError:
|
|
188
|
+
logger.warning("Some isolated exporters did not clean up within timeout")
|
|
189
|
+
|
|
190
|
+
self._active_isolated_exporters.clear()
|
|
191
|
+
|
|
192
|
+
async def _cleanup_single_exporter(self, name: str, exporter: BaseExporter):
|
|
193
|
+
"""Clean up a single isolated exporter."""
|
|
194
|
+
try:
|
|
195
|
+
logger.debug("Stopping isolated exporter '%s'", name)
|
|
196
|
+
await exporter.stop()
|
|
197
|
+
except Exception as e:
|
|
198
|
+
logger.exception("Error stopping isolated exporter '%s': %s", name, e)
|
|
199
|
+
|
|
200
|
+
@asynccontextmanager
|
|
201
|
+
async def start(self, context_state: ContextState | None = None):
|
|
202
|
+
"""
|
|
203
|
+
Start all registered exporters concurrently.
|
|
204
|
+
|
|
205
|
+
This method acquires a lock to ensure only one start/stop cycle is active at a time. It starts all
|
|
206
|
+
currently registered exporters in their own asyncio tasks. Exporters added after this call will not be
|
|
207
|
+
started until the next lifecycle.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
context_state: Optional context state for creating isolated exporters
|
|
211
|
+
|
|
212
|
+
Yields:
|
|
213
|
+
ExporterManager: The manager instance for use within the context.
|
|
214
|
+
|
|
215
|
+
Raises:
|
|
216
|
+
RuntimeError: If the manager is already running.
|
|
217
|
+
"""
|
|
218
|
+
async with self._lock:
|
|
219
|
+
if self._running:
|
|
220
|
+
raise RuntimeError("Exporter manager is already running")
|
|
221
|
+
self._shutdown_event.clear()
|
|
222
|
+
self._running = True
|
|
223
|
+
|
|
224
|
+
# Create isolated exporters if context_state provided, otherwise use originals
|
|
225
|
+
if context_state:
|
|
226
|
+
exporters_to_start = self.create_isolated_exporters(context_state)
|
|
227
|
+
# Store isolated exporters for cleanup
|
|
228
|
+
self._active_isolated_exporters = exporters_to_start
|
|
229
|
+
logger.debug("Created %d isolated exporters", len(exporters_to_start))
|
|
230
|
+
else:
|
|
231
|
+
exporters_to_start = self._exporter_registry
|
|
232
|
+
# Clear isolated exporters since we're using originals
|
|
233
|
+
self._active_isolated_exporters = {}
|
|
234
|
+
|
|
235
|
+
# Start all exporters concurrently
|
|
236
|
+
exporters = []
|
|
237
|
+
tasks = []
|
|
238
|
+
for name, exporter in exporters_to_start.items():
|
|
239
|
+
task = asyncio.create_task(self._run_exporter(name, exporter))
|
|
240
|
+
exporters.append(exporter)
|
|
241
|
+
self._tasks[name] = task
|
|
242
|
+
tasks.append(task)
|
|
243
|
+
|
|
244
|
+
# Wait for all exporters to be ready
|
|
245
|
+
await asyncio.gather(*[exporter.wait_ready() for exporter in exporters])
|
|
246
|
+
|
|
247
|
+
try:
|
|
248
|
+
yield self
|
|
249
|
+
finally:
|
|
250
|
+
# Clean up isolated exporters BEFORE stopping tasks
|
|
251
|
+
try:
|
|
252
|
+
await self._cleanup_isolated_exporters()
|
|
253
|
+
except Exception as e:
|
|
254
|
+
logger.exception("Error during isolated exporter cleanup: %s", e)
|
|
255
|
+
|
|
256
|
+
# Then stop the manager tasks
|
|
257
|
+
await self.stop()
|
|
258
|
+
|
|
259
|
+
async def _run_exporter(self, name: str, exporter: BaseExporter):
|
|
260
|
+
"""
|
|
261
|
+
Run an exporter in its own task.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
name (str): The name of the exporter.
|
|
265
|
+
exporter (BaseExporter): The exporter instance to run.
|
|
266
|
+
"""
|
|
267
|
+
try:
|
|
268
|
+
async with exporter.start():
|
|
269
|
+
logger.info("Started exporter '%s'", name)
|
|
270
|
+
# The context manager will keep the task alive until shutdown is signaled
|
|
271
|
+
await self._shutdown_event.wait()
|
|
272
|
+
logger.info("Stopped exporter '%s'", name)
|
|
273
|
+
except asyncio.CancelledError:
|
|
274
|
+
logger.debug("Exporter '%s' task cancelled", name)
|
|
275
|
+
logger.info("Stopped exporter '%s'", name)
|
|
276
|
+
raise
|
|
277
|
+
except Exception as e:
|
|
278
|
+
logger.error("Failed to run exporter '%s': %s", name, str(e))
|
|
279
|
+
# Re-raise the exception to ensure it's properly handled
|
|
280
|
+
raise
|
|
281
|
+
|
|
282
|
+
async def stop(self) -> None:
|
|
283
|
+
"""
|
|
284
|
+
Stop all registered exporters.
|
|
285
|
+
|
|
286
|
+
This method signals all running exporter tasks to shut down and waits for their completion, up to the
|
|
287
|
+
configured shutdown timeout. If any tasks do not complete in time, a warning is logged.
|
|
288
|
+
"""
|
|
289
|
+
async with self._lock:
|
|
290
|
+
if not self._running:
|
|
291
|
+
return
|
|
292
|
+
self._running = False
|
|
293
|
+
self._shutdown_event.set()
|
|
294
|
+
|
|
295
|
+
# Create a copy of tasks to prevent modification during iteration
|
|
296
|
+
tasks_to_cancel = dict(self._tasks)
|
|
297
|
+
self._tasks.clear()
|
|
298
|
+
stuck_tasks = []
|
|
299
|
+
# Cancel all running tasks and await their completion
|
|
300
|
+
for name, task in tasks_to_cancel.items():
|
|
301
|
+
try:
|
|
302
|
+
task.cancel()
|
|
303
|
+
await asyncio.wait_for(task, timeout=self._shutdown_timeout)
|
|
304
|
+
except TimeoutError:
|
|
305
|
+
logger.warning("Exporter '%s' task did not shut down in time and may be stuck.", name)
|
|
306
|
+
stuck_tasks.append(name)
|
|
307
|
+
except asyncio.CancelledError:
|
|
308
|
+
logger.debug("Exporter '%s' task cancelled", name)
|
|
309
|
+
except Exception as e:
|
|
310
|
+
logger.exception("Failed to stop exporter '%s': %s", name, str(e))
|
|
311
|
+
|
|
312
|
+
if stuck_tasks:
|
|
313
|
+
logger.warning("Exporters did not shut down in time: %s", ", ".join(stuck_tasks))
|
|
314
|
+
|
|
315
|
+
@staticmethod
|
|
316
|
+
def from_exporters(exporters: dict[str, BaseExporter], shutdown_timeout: int = 120) -> "ExporterManager":
|
|
317
|
+
"""
|
|
318
|
+
Create an ExporterManager from a dictionary of exporters.
|
|
319
|
+
"""
|
|
320
|
+
exporter_manager = ExporterManager(shutdown_timeout=shutdown_timeout)
|
|
321
|
+
for name, exporter in exporters.items():
|
|
322
|
+
exporter_manager.add_exporter(name, exporter)
|
|
323
|
+
|
|
324
|
+
return exporter_manager
|
|
325
|
+
|
|
326
|
+
def get(self) -> "ExporterManager":
|
|
327
|
+
"""
|
|
328
|
+
Create a copy of this ExporterManager with the same configuration using copy-on-write.
|
|
329
|
+
|
|
330
|
+
This is the most efficient approach - shares the registry until modifications are needed.
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
ExporterManager: A new ExporterManager instance with shared exporters (copy-on-write).
|
|
334
|
+
"""
|
|
335
|
+
return self._create_with_shared_registry(self._shutdown_timeout, self._exporter_registry)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-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.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-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 pydantic import BaseModel
|
|
17
|
+
from pydantic import Field
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BatchConfigMixin(BaseModel):
|
|
21
|
+
"""Mixin for telemetry exporters that require batching."""
|
|
22
|
+
batch_size: int = Field(default=100, description="The batch size for the telemetry exporter.")
|
|
23
|
+
flush_interval: float = Field(default=5.0, description="The flush interval for the telemetry exporter.")
|
|
24
|
+
max_queue_size: int = Field(default=1000, description="The maximum queue size for the telemetry exporter.")
|
|
25
|
+
drop_on_overflow: bool = Field(default=False, description="Whether to drop on overflow for the telemetry exporter.")
|
|
26
|
+
shutdown_timeout: float = Field(default=10.0, description="The shutdown timeout for the telemetry exporter.")
|