nvidia-nat 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (435) hide show
  1. aiq/__init__.py +66 -0
  2. nat/agent/__init__.py +0 -0
  3. nat/agent/base.py +256 -0
  4. nat/agent/dual_node.py +67 -0
  5. nat/agent/react_agent/__init__.py +0 -0
  6. nat/agent/react_agent/agent.py +363 -0
  7. nat/agent/react_agent/output_parser.py +104 -0
  8. nat/agent/react_agent/prompt.py +44 -0
  9. nat/agent/react_agent/register.py +149 -0
  10. nat/agent/reasoning_agent/__init__.py +0 -0
  11. nat/agent/reasoning_agent/reasoning_agent.py +225 -0
  12. nat/agent/register.py +23 -0
  13. nat/agent/rewoo_agent/__init__.py +0 -0
  14. nat/agent/rewoo_agent/agent.py +415 -0
  15. nat/agent/rewoo_agent/prompt.py +110 -0
  16. nat/agent/rewoo_agent/register.py +157 -0
  17. nat/agent/tool_calling_agent/__init__.py +0 -0
  18. nat/agent/tool_calling_agent/agent.py +119 -0
  19. nat/agent/tool_calling_agent/register.py +106 -0
  20. nat/authentication/__init__.py +14 -0
  21. nat/authentication/api_key/__init__.py +14 -0
  22. nat/authentication/api_key/api_key_auth_provider.py +96 -0
  23. nat/authentication/api_key/api_key_auth_provider_config.py +124 -0
  24. nat/authentication/api_key/register.py +26 -0
  25. nat/authentication/exceptions/__init__.py +14 -0
  26. nat/authentication/exceptions/api_key_exceptions.py +38 -0
  27. nat/authentication/http_basic_auth/__init__.py +0 -0
  28. nat/authentication/http_basic_auth/http_basic_auth_provider.py +81 -0
  29. nat/authentication/http_basic_auth/register.py +30 -0
  30. nat/authentication/interfaces.py +93 -0
  31. nat/authentication/oauth2/__init__.py +14 -0
  32. nat/authentication/oauth2/oauth2_auth_code_flow_provider.py +107 -0
  33. nat/authentication/oauth2/oauth2_auth_code_flow_provider_config.py +39 -0
  34. nat/authentication/oauth2/register.py +25 -0
  35. nat/authentication/register.py +21 -0
  36. nat/builder/__init__.py +0 -0
  37. nat/builder/builder.py +285 -0
  38. nat/builder/component_utils.py +316 -0
  39. nat/builder/context.py +270 -0
  40. nat/builder/embedder.py +24 -0
  41. nat/builder/eval_builder.py +161 -0
  42. nat/builder/evaluator.py +29 -0
  43. nat/builder/framework_enum.py +24 -0
  44. nat/builder/front_end.py +73 -0
  45. nat/builder/function.py +344 -0
  46. nat/builder/function_base.py +380 -0
  47. nat/builder/function_info.py +627 -0
  48. nat/builder/intermediate_step_manager.py +174 -0
  49. nat/builder/llm.py +25 -0
  50. nat/builder/retriever.py +25 -0
  51. nat/builder/user_interaction_manager.py +78 -0
  52. nat/builder/workflow.py +148 -0
  53. nat/builder/workflow_builder.py +1117 -0
  54. nat/cli/__init__.py +14 -0
  55. nat/cli/cli_utils/__init__.py +0 -0
  56. nat/cli/cli_utils/config_override.py +231 -0
  57. nat/cli/cli_utils/validation.py +37 -0
  58. nat/cli/commands/__init__.py +0 -0
  59. nat/cli/commands/configure/__init__.py +0 -0
  60. nat/cli/commands/configure/channel/__init__.py +0 -0
  61. nat/cli/commands/configure/channel/add.py +28 -0
  62. nat/cli/commands/configure/channel/channel.py +34 -0
  63. nat/cli/commands/configure/channel/remove.py +30 -0
  64. nat/cli/commands/configure/channel/update.py +30 -0
  65. nat/cli/commands/configure/configure.py +33 -0
  66. nat/cli/commands/evaluate.py +139 -0
  67. nat/cli/commands/info/__init__.py +14 -0
  68. nat/cli/commands/info/info.py +37 -0
  69. nat/cli/commands/info/list_channels.py +32 -0
  70. nat/cli/commands/info/list_components.py +129 -0
  71. nat/cli/commands/info/list_mcp.py +304 -0
  72. nat/cli/commands/registry/__init__.py +14 -0
  73. nat/cli/commands/registry/publish.py +88 -0
  74. nat/cli/commands/registry/pull.py +118 -0
  75. nat/cli/commands/registry/registry.py +36 -0
  76. nat/cli/commands/registry/remove.py +108 -0
  77. nat/cli/commands/registry/search.py +155 -0
  78. nat/cli/commands/sizing/__init__.py +14 -0
  79. nat/cli/commands/sizing/calc.py +297 -0
  80. nat/cli/commands/sizing/sizing.py +27 -0
  81. nat/cli/commands/start.py +246 -0
  82. nat/cli/commands/uninstall.py +81 -0
  83. nat/cli/commands/validate.py +47 -0
  84. nat/cli/commands/workflow/__init__.py +14 -0
  85. nat/cli/commands/workflow/templates/__init__.py.j2 +0 -0
  86. nat/cli/commands/workflow/templates/config.yml.j2 +16 -0
  87. nat/cli/commands/workflow/templates/pyproject.toml.j2 +22 -0
  88. nat/cli/commands/workflow/templates/register.py.j2 +5 -0
  89. nat/cli/commands/workflow/templates/workflow.py.j2 +36 -0
  90. nat/cli/commands/workflow/workflow.py +37 -0
  91. nat/cli/commands/workflow/workflow_commands.py +317 -0
  92. nat/cli/entrypoint.py +135 -0
  93. nat/cli/main.py +57 -0
  94. nat/cli/register_workflow.py +488 -0
  95. nat/cli/type_registry.py +1000 -0
  96. nat/data_models/__init__.py +14 -0
  97. nat/data_models/api_server.py +716 -0
  98. nat/data_models/authentication.py +231 -0
  99. nat/data_models/common.py +171 -0
  100. nat/data_models/component.py +58 -0
  101. nat/data_models/component_ref.py +168 -0
  102. nat/data_models/config.py +410 -0
  103. nat/data_models/dataset_handler.py +169 -0
  104. nat/data_models/discovery_metadata.py +305 -0
  105. nat/data_models/embedder.py +27 -0
  106. nat/data_models/evaluate.py +127 -0
  107. nat/data_models/evaluator.py +26 -0
  108. nat/data_models/front_end.py +26 -0
  109. nat/data_models/function.py +30 -0
  110. nat/data_models/function_dependencies.py +72 -0
  111. nat/data_models/interactive.py +246 -0
  112. nat/data_models/intermediate_step.py +302 -0
  113. nat/data_models/invocation_node.py +38 -0
  114. nat/data_models/llm.py +27 -0
  115. nat/data_models/logging.py +26 -0
  116. nat/data_models/memory.py +27 -0
  117. nat/data_models/object_store.py +44 -0
  118. nat/data_models/profiler.py +54 -0
  119. nat/data_models/registry_handler.py +26 -0
  120. nat/data_models/retriever.py +30 -0
  121. nat/data_models/retry_mixin.py +35 -0
  122. nat/data_models/span.py +190 -0
  123. nat/data_models/step_adaptor.py +64 -0
  124. nat/data_models/streaming.py +33 -0
  125. nat/data_models/swe_bench_model.py +54 -0
  126. nat/data_models/telemetry_exporter.py +26 -0
  127. nat/data_models/ttc_strategy.py +30 -0
  128. nat/embedder/__init__.py +0 -0
  129. nat/embedder/nim_embedder.py +59 -0
  130. nat/embedder/openai_embedder.py +43 -0
  131. nat/embedder/register.py +22 -0
  132. nat/eval/__init__.py +14 -0
  133. nat/eval/config.py +60 -0
  134. nat/eval/dataset_handler/__init__.py +0 -0
  135. nat/eval/dataset_handler/dataset_downloader.py +106 -0
  136. nat/eval/dataset_handler/dataset_filter.py +52 -0
  137. nat/eval/dataset_handler/dataset_handler.py +367 -0
  138. nat/eval/evaluate.py +510 -0
  139. nat/eval/evaluator/__init__.py +14 -0
  140. nat/eval/evaluator/base_evaluator.py +77 -0
  141. nat/eval/evaluator/evaluator_model.py +45 -0
  142. nat/eval/intermediate_step_adapter.py +99 -0
  143. nat/eval/rag_evaluator/__init__.py +0 -0
  144. nat/eval/rag_evaluator/evaluate.py +178 -0
  145. nat/eval/rag_evaluator/register.py +143 -0
  146. nat/eval/register.py +23 -0
  147. nat/eval/remote_workflow.py +133 -0
  148. nat/eval/runners/__init__.py +14 -0
  149. nat/eval/runners/config.py +39 -0
  150. nat/eval/runners/multi_eval_runner.py +54 -0
  151. nat/eval/runtime_event_subscriber.py +52 -0
  152. nat/eval/swe_bench_evaluator/__init__.py +0 -0
  153. nat/eval/swe_bench_evaluator/evaluate.py +215 -0
  154. nat/eval/swe_bench_evaluator/register.py +36 -0
  155. nat/eval/trajectory_evaluator/__init__.py +0 -0
  156. nat/eval/trajectory_evaluator/evaluate.py +75 -0
  157. nat/eval/trajectory_evaluator/register.py +40 -0
  158. nat/eval/tunable_rag_evaluator/__init__.py +0 -0
  159. nat/eval/tunable_rag_evaluator/evaluate.py +245 -0
  160. nat/eval/tunable_rag_evaluator/register.py +52 -0
  161. nat/eval/usage_stats.py +41 -0
  162. nat/eval/utils/__init__.py +0 -0
  163. nat/eval/utils/output_uploader.py +140 -0
  164. nat/eval/utils/tqdm_position_registry.py +40 -0
  165. nat/eval/utils/weave_eval.py +184 -0
  166. nat/experimental/__init__.py +0 -0
  167. nat/experimental/decorators/__init__.py +0 -0
  168. nat/experimental/decorators/experimental_warning_decorator.py +134 -0
  169. nat/experimental/test_time_compute/__init__.py +0 -0
  170. nat/experimental/test_time_compute/editing/__init__.py +0 -0
  171. nat/experimental/test_time_compute/editing/iterative_plan_refinement_editor.py +147 -0
  172. nat/experimental/test_time_compute/editing/llm_as_a_judge_editor.py +204 -0
  173. nat/experimental/test_time_compute/editing/motivation_aware_summarization.py +107 -0
  174. nat/experimental/test_time_compute/functions/__init__.py +0 -0
  175. nat/experimental/test_time_compute/functions/execute_score_select_function.py +105 -0
  176. nat/experimental/test_time_compute/functions/plan_select_execute_function.py +224 -0
  177. nat/experimental/test_time_compute/functions/ttc_tool_orchestration_function.py +205 -0
  178. nat/experimental/test_time_compute/functions/ttc_tool_wrapper_function.py +146 -0
  179. nat/experimental/test_time_compute/models/__init__.py +0 -0
  180. nat/experimental/test_time_compute/models/editor_config.py +132 -0
  181. nat/experimental/test_time_compute/models/scoring_config.py +112 -0
  182. nat/experimental/test_time_compute/models/search_config.py +120 -0
  183. nat/experimental/test_time_compute/models/selection_config.py +154 -0
  184. nat/experimental/test_time_compute/models/stage_enums.py +43 -0
  185. nat/experimental/test_time_compute/models/strategy_base.py +66 -0
  186. nat/experimental/test_time_compute/models/tool_use_config.py +41 -0
  187. nat/experimental/test_time_compute/models/ttc_item.py +48 -0
  188. nat/experimental/test_time_compute/register.py +36 -0
  189. nat/experimental/test_time_compute/scoring/__init__.py +0 -0
  190. nat/experimental/test_time_compute/scoring/llm_based_agent_scorer.py +168 -0
  191. nat/experimental/test_time_compute/scoring/llm_based_plan_scorer.py +168 -0
  192. nat/experimental/test_time_compute/scoring/motivation_aware_scorer.py +111 -0
  193. nat/experimental/test_time_compute/search/__init__.py +0 -0
  194. nat/experimental/test_time_compute/search/multi_llm_planner.py +128 -0
  195. nat/experimental/test_time_compute/search/multi_query_retrieval_search.py +122 -0
  196. nat/experimental/test_time_compute/search/single_shot_multi_plan_planner.py +128 -0
  197. nat/experimental/test_time_compute/selection/__init__.py +0 -0
  198. nat/experimental/test_time_compute/selection/best_of_n_selector.py +63 -0
  199. nat/experimental/test_time_compute/selection/llm_based_agent_output_selector.py +131 -0
  200. nat/experimental/test_time_compute/selection/llm_based_output_merging_selector.py +159 -0
  201. nat/experimental/test_time_compute/selection/llm_based_plan_selector.py +128 -0
  202. nat/experimental/test_time_compute/selection/threshold_selector.py +58 -0
  203. nat/front_ends/__init__.py +14 -0
  204. nat/front_ends/console/__init__.py +14 -0
  205. nat/front_ends/console/authentication_flow_handler.py +233 -0
  206. nat/front_ends/console/console_front_end_config.py +32 -0
  207. nat/front_ends/console/console_front_end_plugin.py +96 -0
  208. nat/front_ends/console/register.py +25 -0
  209. nat/front_ends/cron/__init__.py +14 -0
  210. nat/front_ends/fastapi/__init__.py +14 -0
  211. nat/front_ends/fastapi/auth_flow_handlers/__init__.py +0 -0
  212. nat/front_ends/fastapi/auth_flow_handlers/http_flow_handler.py +27 -0
  213. nat/front_ends/fastapi/auth_flow_handlers/websocket_flow_handler.py +107 -0
  214. nat/front_ends/fastapi/fastapi_front_end_config.py +241 -0
  215. nat/front_ends/fastapi/fastapi_front_end_controller.py +68 -0
  216. nat/front_ends/fastapi/fastapi_front_end_plugin.py +116 -0
  217. nat/front_ends/fastapi/fastapi_front_end_plugin_worker.py +1087 -0
  218. nat/front_ends/fastapi/html_snippets/__init__.py +14 -0
  219. nat/front_ends/fastapi/html_snippets/auth_code_grant_success.py +35 -0
  220. nat/front_ends/fastapi/intermediate_steps_subscriber.py +80 -0
  221. nat/front_ends/fastapi/job_store.py +183 -0
  222. nat/front_ends/fastapi/main.py +72 -0
  223. nat/front_ends/fastapi/message_handler.py +320 -0
  224. nat/front_ends/fastapi/message_validator.py +352 -0
  225. nat/front_ends/fastapi/register.py +25 -0
  226. nat/front_ends/fastapi/response_helpers.py +195 -0
  227. nat/front_ends/fastapi/step_adaptor.py +319 -0
  228. nat/front_ends/mcp/__init__.py +14 -0
  229. nat/front_ends/mcp/mcp_front_end_config.py +36 -0
  230. nat/front_ends/mcp/mcp_front_end_plugin.py +81 -0
  231. nat/front_ends/mcp/mcp_front_end_plugin_worker.py +143 -0
  232. nat/front_ends/mcp/register.py +27 -0
  233. nat/front_ends/mcp/tool_converter.py +241 -0
  234. nat/front_ends/register.py +22 -0
  235. nat/front_ends/simple_base/__init__.py +14 -0
  236. nat/front_ends/simple_base/simple_front_end_plugin_base.py +54 -0
  237. nat/llm/__init__.py +0 -0
  238. nat/llm/aws_bedrock_llm.py +57 -0
  239. nat/llm/nim_llm.py +46 -0
  240. nat/llm/openai_llm.py +46 -0
  241. nat/llm/register.py +23 -0
  242. nat/llm/utils/__init__.py +14 -0
  243. nat/llm/utils/env_config_value.py +94 -0
  244. nat/llm/utils/error.py +17 -0
  245. nat/memory/__init__.py +20 -0
  246. nat/memory/interfaces.py +183 -0
  247. nat/memory/models.py +112 -0
  248. nat/meta/pypi.md +58 -0
  249. nat/object_store/__init__.py +20 -0
  250. nat/object_store/in_memory_object_store.py +76 -0
  251. nat/object_store/interfaces.py +84 -0
  252. nat/object_store/models.py +38 -0
  253. nat/object_store/register.py +20 -0
  254. nat/observability/__init__.py +14 -0
  255. nat/observability/exporter/__init__.py +14 -0
  256. nat/observability/exporter/base_exporter.py +449 -0
  257. nat/observability/exporter/exporter.py +78 -0
  258. nat/observability/exporter/file_exporter.py +33 -0
  259. nat/observability/exporter/processing_exporter.py +322 -0
  260. nat/observability/exporter/raw_exporter.py +52 -0
  261. nat/observability/exporter/span_exporter.py +288 -0
  262. nat/observability/exporter_manager.py +335 -0
  263. nat/observability/mixin/__init__.py +14 -0
  264. nat/observability/mixin/batch_config_mixin.py +26 -0
  265. nat/observability/mixin/collector_config_mixin.py +23 -0
  266. nat/observability/mixin/file_mixin.py +288 -0
  267. nat/observability/mixin/file_mode.py +23 -0
  268. nat/observability/mixin/resource_conflict_mixin.py +134 -0
  269. nat/observability/mixin/serialize_mixin.py +61 -0
  270. nat/observability/mixin/type_introspection_mixin.py +183 -0
  271. nat/observability/processor/__init__.py +14 -0
  272. nat/observability/processor/batching_processor.py +310 -0
  273. nat/observability/processor/callback_processor.py +42 -0
  274. nat/observability/processor/intermediate_step_serializer.py +28 -0
  275. nat/observability/processor/processor.py +71 -0
  276. nat/observability/register.py +96 -0
  277. nat/observability/utils/__init__.py +14 -0
  278. nat/observability/utils/dict_utils.py +236 -0
  279. nat/observability/utils/time_utils.py +31 -0
  280. nat/plugins/.namespace +1 -0
  281. nat/profiler/__init__.py +0 -0
  282. nat/profiler/calc/__init__.py +14 -0
  283. nat/profiler/calc/calc_runner.py +627 -0
  284. nat/profiler/calc/calculations.py +288 -0
  285. nat/profiler/calc/data_models.py +188 -0
  286. nat/profiler/calc/plot.py +345 -0
  287. nat/profiler/callbacks/__init__.py +0 -0
  288. nat/profiler/callbacks/agno_callback_handler.py +295 -0
  289. nat/profiler/callbacks/base_callback_class.py +20 -0
  290. nat/profiler/callbacks/langchain_callback_handler.py +290 -0
  291. nat/profiler/callbacks/llama_index_callback_handler.py +205 -0
  292. nat/profiler/callbacks/semantic_kernel_callback_handler.py +238 -0
  293. nat/profiler/callbacks/token_usage_base_model.py +27 -0
  294. nat/profiler/data_frame_row.py +51 -0
  295. nat/profiler/data_models.py +24 -0
  296. nat/profiler/decorators/__init__.py +0 -0
  297. nat/profiler/decorators/framework_wrapper.py +131 -0
  298. nat/profiler/decorators/function_tracking.py +254 -0
  299. nat/profiler/forecasting/__init__.py +0 -0
  300. nat/profiler/forecasting/config.py +18 -0
  301. nat/profiler/forecasting/model_trainer.py +75 -0
  302. nat/profiler/forecasting/models/__init__.py +22 -0
  303. nat/profiler/forecasting/models/forecasting_base_model.py +40 -0
  304. nat/profiler/forecasting/models/linear_model.py +197 -0
  305. nat/profiler/forecasting/models/random_forest_regressor.py +269 -0
  306. nat/profiler/inference_metrics_model.py +28 -0
  307. nat/profiler/inference_optimization/__init__.py +0 -0
  308. nat/profiler/inference_optimization/bottleneck_analysis/__init__.py +0 -0
  309. nat/profiler/inference_optimization/bottleneck_analysis/nested_stack_analysis.py +460 -0
  310. nat/profiler/inference_optimization/bottleneck_analysis/simple_stack_analysis.py +258 -0
  311. nat/profiler/inference_optimization/data_models.py +386 -0
  312. nat/profiler/inference_optimization/experimental/__init__.py +0 -0
  313. nat/profiler/inference_optimization/experimental/concurrency_spike_analysis.py +468 -0
  314. nat/profiler/inference_optimization/experimental/prefix_span_analysis.py +405 -0
  315. nat/profiler/inference_optimization/llm_metrics.py +212 -0
  316. nat/profiler/inference_optimization/prompt_caching.py +163 -0
  317. nat/profiler/inference_optimization/token_uniqueness.py +107 -0
  318. nat/profiler/inference_optimization/workflow_runtimes.py +72 -0
  319. nat/profiler/intermediate_property_adapter.py +102 -0
  320. nat/profiler/profile_runner.py +473 -0
  321. nat/profiler/utils.py +184 -0
  322. nat/registry_handlers/__init__.py +0 -0
  323. nat/registry_handlers/local/__init__.py +0 -0
  324. nat/registry_handlers/local/local_handler.py +176 -0
  325. nat/registry_handlers/local/register_local.py +37 -0
  326. nat/registry_handlers/metadata_factory.py +60 -0
  327. nat/registry_handlers/package_utils.py +571 -0
  328. nat/registry_handlers/pypi/__init__.py +0 -0
  329. nat/registry_handlers/pypi/pypi_handler.py +251 -0
  330. nat/registry_handlers/pypi/register_pypi.py +40 -0
  331. nat/registry_handlers/register.py +21 -0
  332. nat/registry_handlers/registry_handler_base.py +157 -0
  333. nat/registry_handlers/rest/__init__.py +0 -0
  334. nat/registry_handlers/rest/register_rest.py +56 -0
  335. nat/registry_handlers/rest/rest_handler.py +237 -0
  336. nat/registry_handlers/schemas/__init__.py +0 -0
  337. nat/registry_handlers/schemas/headers.py +42 -0
  338. nat/registry_handlers/schemas/package.py +68 -0
  339. nat/registry_handlers/schemas/publish.py +68 -0
  340. nat/registry_handlers/schemas/pull.py +82 -0
  341. nat/registry_handlers/schemas/remove.py +36 -0
  342. nat/registry_handlers/schemas/search.py +91 -0
  343. nat/registry_handlers/schemas/status.py +47 -0
  344. nat/retriever/__init__.py +0 -0
  345. nat/retriever/interface.py +41 -0
  346. nat/retriever/milvus/__init__.py +14 -0
  347. nat/retriever/milvus/register.py +81 -0
  348. nat/retriever/milvus/retriever.py +228 -0
  349. nat/retriever/models.py +77 -0
  350. nat/retriever/nemo_retriever/__init__.py +14 -0
  351. nat/retriever/nemo_retriever/register.py +60 -0
  352. nat/retriever/nemo_retriever/retriever.py +190 -0
  353. nat/retriever/register.py +22 -0
  354. nat/runtime/__init__.py +14 -0
  355. nat/runtime/loader.py +220 -0
  356. nat/runtime/runner.py +195 -0
  357. nat/runtime/session.py +162 -0
  358. nat/runtime/user_metadata.py +130 -0
  359. nat/settings/__init__.py +0 -0
  360. nat/settings/global_settings.py +318 -0
  361. nat/test/.namespace +1 -0
  362. nat/tool/__init__.py +0 -0
  363. nat/tool/chat_completion.py +74 -0
  364. nat/tool/code_execution/README.md +151 -0
  365. nat/tool/code_execution/__init__.py +0 -0
  366. nat/tool/code_execution/code_sandbox.py +267 -0
  367. nat/tool/code_execution/local_sandbox/.gitignore +1 -0
  368. nat/tool/code_execution/local_sandbox/Dockerfile.sandbox +60 -0
  369. nat/tool/code_execution/local_sandbox/__init__.py +13 -0
  370. nat/tool/code_execution/local_sandbox/local_sandbox_server.py +198 -0
  371. nat/tool/code_execution/local_sandbox/sandbox.requirements.txt +6 -0
  372. nat/tool/code_execution/local_sandbox/start_local_sandbox.sh +50 -0
  373. nat/tool/code_execution/register.py +74 -0
  374. nat/tool/code_execution/test_code_execution_sandbox.py +414 -0
  375. nat/tool/code_execution/utils.py +100 -0
  376. nat/tool/datetime_tools.py +42 -0
  377. nat/tool/document_search.py +141 -0
  378. nat/tool/github_tools/__init__.py +0 -0
  379. nat/tool/github_tools/create_github_commit.py +133 -0
  380. nat/tool/github_tools/create_github_issue.py +87 -0
  381. nat/tool/github_tools/create_github_pr.py +106 -0
  382. nat/tool/github_tools/get_github_file.py +106 -0
  383. nat/tool/github_tools/get_github_issue.py +166 -0
  384. nat/tool/github_tools/get_github_pr.py +256 -0
  385. nat/tool/github_tools/update_github_issue.py +100 -0
  386. nat/tool/mcp/__init__.py +14 -0
  387. nat/tool/mcp/exceptions.py +142 -0
  388. nat/tool/mcp/mcp_client.py +255 -0
  389. nat/tool/mcp/mcp_tool.py +96 -0
  390. nat/tool/memory_tools/__init__.py +0 -0
  391. nat/tool/memory_tools/add_memory_tool.py +79 -0
  392. nat/tool/memory_tools/delete_memory_tool.py +67 -0
  393. nat/tool/memory_tools/get_memory_tool.py +72 -0
  394. nat/tool/nvidia_rag.py +95 -0
  395. nat/tool/register.py +38 -0
  396. nat/tool/retriever.py +94 -0
  397. nat/tool/server_tools.py +66 -0
  398. nat/utils/__init__.py +0 -0
  399. nat/utils/data_models/__init__.py +0 -0
  400. nat/utils/data_models/schema_validator.py +58 -0
  401. nat/utils/debugging_utils.py +43 -0
  402. nat/utils/dump_distro_mapping.py +32 -0
  403. nat/utils/exception_handlers/__init__.py +0 -0
  404. nat/utils/exception_handlers/automatic_retries.py +289 -0
  405. nat/utils/exception_handlers/mcp.py +211 -0
  406. nat/utils/exception_handlers/schemas.py +114 -0
  407. nat/utils/io/__init__.py +0 -0
  408. nat/utils/io/model_processing.py +28 -0
  409. nat/utils/io/yaml_tools.py +119 -0
  410. nat/utils/log_utils.py +37 -0
  411. nat/utils/metadata_utils.py +74 -0
  412. nat/utils/optional_imports.py +142 -0
  413. nat/utils/producer_consumer_queue.py +178 -0
  414. nat/utils/reactive/__init__.py +0 -0
  415. nat/utils/reactive/base/__init__.py +0 -0
  416. nat/utils/reactive/base/observable_base.py +65 -0
  417. nat/utils/reactive/base/observer_base.py +55 -0
  418. nat/utils/reactive/base/subject_base.py +79 -0
  419. nat/utils/reactive/observable.py +59 -0
  420. nat/utils/reactive/observer.py +76 -0
  421. nat/utils/reactive/subject.py +131 -0
  422. nat/utils/reactive/subscription.py +49 -0
  423. nat/utils/settings/__init__.py +0 -0
  424. nat/utils/settings/global_settings.py +197 -0
  425. nat/utils/string_utils.py +38 -0
  426. nat/utils/type_converter.py +290 -0
  427. nat/utils/type_utils.py +484 -0
  428. nat/utils/url_utils.py +27 -0
  429. nvidia_nat-1.2.0.dist-info/METADATA +365 -0
  430. nvidia_nat-1.2.0.dist-info/RECORD +435 -0
  431. nvidia_nat-1.2.0.dist-info/WHEEL +5 -0
  432. nvidia_nat-1.2.0.dist-info/entry_points.txt +21 -0
  433. nvidia_nat-1.2.0.dist-info/licenses/LICENSE-3rd-party.txt +5478 -0
  434. nvidia_nat-1.2.0.dist-info/licenses/LICENSE.md +201 -0
  435. nvidia_nat-1.2.0.dist-info/top_level.txt +2 -0
@@ -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.error("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 asyncio.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.error("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.error("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), exc_info=True)
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 asyncio.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.error("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.")
@@ -0,0 +1,23 @@
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 CollectorConfigMixin(BaseModel):
21
+ """Mixin for telemetry exporters that require a project name and endpoint when exporting to a collector service."""
22
+ project: str = Field(description="The project name to associate the telemetry traces.")
23
+ endpoint: str = Field(description="The endpoint of the telemetry collector service.")
@@ -0,0 +1,288 @@
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 datetime import datetime
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ from nat.observability.mixin.file_mode import FileMode
23
+ from nat.observability.mixin.resource_conflict_mixin import ResourceConflictMixin
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class FileExportMixin(ResourceConflictMixin):
29
+ """Mixin for file-based exporters.
30
+
31
+ This mixin provides file I/O functionality for exporters that need to write
32
+ serialized data to local files, with support for file overwriting and rolling logs.
33
+
34
+ Automatically detects and prevents file path conflicts between multiple instances
35
+ by raising ResourceConflictError during initialization.
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ *args,
41
+ output_path,
42
+ project,
43
+ mode: FileMode = FileMode.APPEND,
44
+ enable_rolling: bool = False,
45
+ max_file_size: int = 10 * 1024 * 1024, # 10MB default
46
+ max_files: int = 5,
47
+ cleanup_on_init: bool = False,
48
+ **kwargs):
49
+ """Initialize the file exporter with the specified output_path and project.
50
+
51
+ Args:
52
+ output_path (str): The path to the output file or directory (if rolling enabled).
53
+ project (str): The project name for metadata.
54
+ mode (str): Either "append" or "overwrite". Defaults to "append".
55
+ enable_rolling (bool): Enable rolling log files. Defaults to False.
56
+ max_file_size (int): Maximum file size in bytes before rolling. Defaults to 10MB.
57
+ max_files (int): Maximum number of rolled files to keep. Defaults to 5.
58
+ cleanup_on_init (bool): Clean up old files during initialization. Defaults to False.
59
+
60
+ Raises:
61
+ ResourceConflictError: If another FileExportMixin instance is already using
62
+ the same file path or would create conflicting files.
63
+ """
64
+ self._filepath = Path(output_path)
65
+ self._project = project
66
+ self._mode = mode
67
+ self._enable_rolling = enable_rolling
68
+ self._max_file_size = max_file_size
69
+ self._max_files = max_files
70
+ self._cleanup_on_init = cleanup_on_init
71
+ self._lock = asyncio.Lock()
72
+ self._first_write = True
73
+
74
+ # Initialize file paths first, then check for conflicts via ResourceConflictMixin
75
+ self._setup_file_paths()
76
+
77
+ # This calls _register_resources() which will check for conflicts
78
+ super().__init__(*args, **kwargs)
79
+
80
+ def _setup_file_paths(self):
81
+ """Setup file paths using the project name."""
82
+
83
+ if self._enable_rolling:
84
+ # If rolling is enabled, output_path should be a directory
85
+ self._base_dir = self._filepath if self._filepath.is_dir(
86
+ ) or not self._filepath.suffix else self._filepath.parent
87
+ self._base_filename = self._filepath.stem if self._filepath.suffix else f"{self._project}_export"
88
+ self._file_extension = self._filepath.suffix or ".log"
89
+ self._base_dir.mkdir(parents=True, exist_ok=True)
90
+ self._current_file_path = self._base_dir / f"{self._base_filename}{self._file_extension}"
91
+
92
+ # Perform initial cleanup if requested
93
+ if self._cleanup_on_init:
94
+ self._cleanup_old_files_sync()
95
+ else:
96
+ # Traditional single file mode
97
+ self._filepath.parent.mkdir(parents=True, exist_ok=True)
98
+ self._current_file_path = self._filepath
99
+
100
+ # For single file mode with overwrite, remove existing file
101
+ if self._mode == FileMode.OVERWRITE and self._cleanup_on_init and self._current_file_path.exists():
102
+ try:
103
+ self._current_file_path.unlink()
104
+ logger.info("Cleaned up existing file: %s", self._current_file_path)
105
+ except OSError as e:
106
+ logger.error("Error removing existing file %s: %s", self._current_file_path, e)
107
+
108
+ def _get_resource_identifiers(self) -> dict[str, Any]:
109
+ """Return the file resources this instance will use.
110
+
111
+ Returns:
112
+ dict with file_path and optionally cleanup_pattern for rolling files.
113
+ """
114
+ identifiers = {"file_path": str(self._current_file_path.resolve())}
115
+
116
+ # Add cleanup pattern for rolling files
117
+ if self._enable_rolling:
118
+ cleanup_pattern = f"{self._base_filename}_*{self._file_extension}"
119
+ pattern_key = f"{self._base_dir.resolve()}:{cleanup_pattern}"
120
+ identifiers["cleanup_pattern"] = pattern_key
121
+
122
+ return identifiers
123
+
124
+ def _format_conflict_error(self, resource_type: str, identifier: Any, existing_instance: Any) -> str:
125
+ """Format user-friendly error messages for file conflicts."""
126
+ match resource_type:
127
+ case "file_path":
128
+ return (f"File path conflict detected: '{self._current_file_path}' is already in use by another "
129
+ f"FileExportMixin instance (project: '{existing_instance._project}'). "
130
+ f"Use different project names or output paths to avoid conflicts.")
131
+ case "cleanup_pattern":
132
+ return (f"Rolling file cleanup conflict detected: Both instances would use pattern "
133
+ f"'{self._base_filename}_*{self._file_extension}' in directory '{self._base_dir}', "
134
+ f"causing one to delete the other's files. "
135
+ f"Current instance (project: '{self._project}'), "
136
+ f"existing instance (project: '{existing_instance._project}'). "
137
+ f"Use different project names or directories to avoid conflicts.")
138
+ case _:
139
+ return f"Unknown file resource conflict: {resource_type} = {identifier}"
140
+
141
+ def _cleanup_old_files_sync(self) -> None:
142
+ """Synchronous version of cleanup for use during initialization."""
143
+ try:
144
+ # Find all rolled files matching our pattern
145
+ pattern = f"{self._base_filename}_*{self._file_extension}"
146
+ rolled_files = list(self._base_dir.glob(pattern))
147
+
148
+ # Sort by modification time (newest first)
149
+ rolled_files.sort(key=lambda f: f.stat().st_mtime, reverse=True)
150
+
151
+ # Remove files beyond max_files limit
152
+ for old_file in rolled_files[self._max_files:]:
153
+ try:
154
+ old_file.unlink()
155
+ logger.info("Cleaned up old log file during init: %s", old_file)
156
+ except OSError as e:
157
+ logger.error("Error removing old file %s: %s", old_file, e)
158
+
159
+ except Exception as e:
160
+ logger.error("Error during initialization cleanup: %s", e)
161
+
162
+ async def _should_roll_file(self) -> bool:
163
+ """Check if the current file should be rolled based on size."""
164
+ if not self._enable_rolling:
165
+ return False
166
+
167
+ try:
168
+ if self._current_file_path.exists():
169
+ stat = self._current_file_path.stat()
170
+ return stat.st_size >= self._max_file_size
171
+ except OSError:
172
+ pass
173
+ return False
174
+
175
+ async def _roll_file(self) -> None:
176
+ """Roll the current file by renaming it with a timestamp and cleaning up old files."""
177
+ if not self._current_file_path.exists():
178
+ return
179
+
180
+ # Generate timestamped filename with microsecond precision
181
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
182
+ rolled_filename = f"{self._base_filename}_{timestamp}{self._file_extension}"
183
+ rolled_path = self._base_dir / rolled_filename
184
+
185
+ try:
186
+ # Rename current file
187
+ self._current_file_path.rename(rolled_path)
188
+ logger.info("Rolled log file to: %s", rolled_path)
189
+
190
+ # Clean up old files
191
+ await self._cleanup_old_files()
192
+
193
+ except OSError as e:
194
+ logger.error("Error rolling file %s: %s", self._current_file_path, e)
195
+
196
+ async def _cleanup_old_files(self) -> None:
197
+ """Remove old rolled files beyond the maximum count."""
198
+ try:
199
+ # Find all rolled files matching our pattern
200
+ pattern = f"{self._base_filename}_*{self._file_extension}"
201
+ rolled_files = list(self._base_dir.glob(pattern))
202
+
203
+ # Sort by modification time (newest first)
204
+ rolled_files.sort(key=lambda f: f.stat().st_mtime, reverse=True)
205
+
206
+ # Remove files beyond max_files limit
207
+ for old_file in rolled_files[self._max_files:]:
208
+ try:
209
+ old_file.unlink()
210
+ logger.info("Cleaned up old log file: %s", old_file)
211
+ except OSError as e:
212
+ logger.error("Error removing old file %s: %s", old_file, e)
213
+
214
+ except Exception as e:
215
+ logger.error("Error during cleanup: %s", e)
216
+
217
+ async def export_processed(self, item: str | list[str]) -> None:
218
+ """Export a processed string or list of strings.
219
+
220
+ Args:
221
+ item (str | list[str]): The string or list of strings to export.
222
+ """
223
+ try:
224
+ # Lazy import to avoid slow startup times
225
+ import aiofiles
226
+
227
+ async with self._lock:
228
+ # Check if we need to roll the file
229
+ if await self._should_roll_file():
230
+ await self._roll_file()
231
+
232
+ # Determine file mode
233
+ if self._first_write and self._mode == FileMode.OVERWRITE:
234
+ file_mode = "w"
235
+ self._first_write = False
236
+ else:
237
+ file_mode = "a"
238
+
239
+ async with aiofiles.open(self._current_file_path, mode=file_mode) as f:
240
+ if isinstance(item, list):
241
+ # Handle list of strings
242
+ for single_item in item:
243
+ await f.write(single_item)
244
+ await f.write("\n")
245
+ else:
246
+ # Handle single string
247
+ await f.write(item)
248
+ await f.write("\n")
249
+
250
+ except Exception as e:
251
+ logger.error("Error exporting event: %s", e, exc_info=True)
252
+
253
+ def get_current_file_path(self) -> Path:
254
+ """Get the current file path being written to.
255
+
256
+ Returns:
257
+ Path: The current file path being written to.
258
+ """
259
+ return self._current_file_path
260
+
261
+ def get_file_info(self) -> dict:
262
+ """Get information about the current file and rolling configuration.
263
+
264
+ Returns:
265
+ dict: A dictionary containing the current file path, mode, rolling enabled, cleanup on init,
266
+ effective project name, and additional rolling configuration if enabled.
267
+ """
268
+ info = {
269
+ "current_file": str(self._current_file_path),
270
+ "mode": self._mode,
271
+ "rolling_enabled": self._enable_rolling,
272
+ "cleanup_on_init": self._cleanup_on_init,
273
+ "project": self._project,
274
+ "effective_project": self._project,
275
+ }
276
+
277
+ if self._enable_rolling:
278
+ info.update({
279
+ "max_file_size": self._max_file_size,
280
+ "max_files": self._max_files,
281
+ "base_directory": str(self._base_dir),
282
+ })
283
+
284
+ # Add current file size if it exists
285
+ if self._current_file_path.exists():
286
+ info["current_file_size"] = self._current_file_path.stat().st_size
287
+
288
+ return info