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,1087 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ import asyncio
17
+ import logging
18
+ import os
19
+ import time
20
+ import typing
21
+ from abc import ABC
22
+ from abc import abstractmethod
23
+ from collections.abc import Awaitable
24
+ from collections.abc import Callable
25
+ from contextlib import asynccontextmanager
26
+ from pathlib import Path
27
+
28
+ from fastapi import BackgroundTasks
29
+ from fastapi import Body
30
+ from fastapi import FastAPI
31
+ from fastapi import Request
32
+ from fastapi import Response
33
+ from fastapi import UploadFile
34
+ from fastapi.exceptions import HTTPException
35
+ from fastapi.middleware.cors import CORSMiddleware
36
+ from fastapi.responses import StreamingResponse
37
+ from pydantic import BaseModel
38
+ from pydantic import Field
39
+ from starlette.websockets import WebSocket
40
+
41
+ from nat.builder.workflow_builder import WorkflowBuilder
42
+ from nat.data_models.api_server import ChatRequest
43
+ from nat.data_models.api_server import ChatResponse
44
+ from nat.data_models.api_server import ChatResponseChunk
45
+ from nat.data_models.api_server import ResponseIntermediateStep
46
+ from nat.data_models.config import Config
47
+ from nat.data_models.object_store import KeyAlreadyExistsError
48
+ from nat.data_models.object_store import NoSuchKeyError
49
+ from nat.eval.config import EvaluationRunOutput
50
+ from nat.eval.evaluate import EvaluationRun
51
+ from nat.eval.evaluate import EvaluationRunConfig
52
+ from nat.front_ends.fastapi.auth_flow_handlers.http_flow_handler import HTTPAuthenticationFlowHandler
53
+ from nat.front_ends.fastapi.auth_flow_handlers.websocket_flow_handler import FlowState
54
+ from nat.front_ends.fastapi.auth_flow_handlers.websocket_flow_handler import WebSocketAuthenticationFlowHandler
55
+ from nat.front_ends.fastapi.fastapi_front_end_config import AsyncGenerateResponse
56
+ from nat.front_ends.fastapi.fastapi_front_end_config import AsyncGenerationStatusResponse
57
+ from nat.front_ends.fastapi.fastapi_front_end_config import EvaluateRequest
58
+ from nat.front_ends.fastapi.fastapi_front_end_config import EvaluateResponse
59
+ from nat.front_ends.fastapi.fastapi_front_end_config import EvaluateStatusResponse
60
+ from nat.front_ends.fastapi.fastapi_front_end_config import FastApiFrontEndConfig
61
+ from nat.front_ends.fastapi.job_store import JobInfo
62
+ from nat.front_ends.fastapi.job_store import JobStore
63
+ from nat.front_ends.fastapi.message_handler import WebSocketMessageHandler
64
+ from nat.front_ends.fastapi.response_helpers import generate_single_response
65
+ from nat.front_ends.fastapi.response_helpers import generate_streaming_response_as_str
66
+ from nat.front_ends.fastapi.response_helpers import generate_streaming_response_full_as_str
67
+ from nat.front_ends.fastapi.step_adaptor import StepAdaptor
68
+ from nat.object_store.models import ObjectStoreItem
69
+ from nat.runtime.session import SessionManager
70
+
71
+ logger = logging.getLogger(__name__)
72
+
73
+
74
+ class FastApiFrontEndPluginWorkerBase(ABC):
75
+
76
+ def __init__(self, config: Config):
77
+ self._config = config
78
+
79
+ assert isinstance(config.general.front_end,
80
+ FastApiFrontEndConfig), ("Front end config is not FastApiFrontEndConfig")
81
+
82
+ self._front_end_config = config.general.front_end
83
+
84
+ self._cleanup_tasks: list[str] = []
85
+ self._cleanup_tasks_lock = asyncio.Lock()
86
+ self._http_flow_handler: HTTPAuthenticationFlowHandler | None = HTTPAuthenticationFlowHandler()
87
+
88
+ @property
89
+ def config(self) -> Config:
90
+ return self._config
91
+
92
+ @property
93
+ def front_end_config(self) -> FastApiFrontEndConfig:
94
+ return self._front_end_config
95
+
96
+ def build_app(self) -> FastAPI:
97
+
98
+ # Create the FastAPI app and configure it
99
+ @asynccontextmanager
100
+ async def lifespan(starting_app: FastAPI):
101
+
102
+ logger.debug("Starting NAT server from process %s", os.getpid())
103
+
104
+ async with WorkflowBuilder.from_config(self.config) as builder:
105
+
106
+ await self.configure(starting_app, builder)
107
+
108
+ yield
109
+
110
+ # If a cleanup task is running, cancel it
111
+ async with self._cleanup_tasks_lock:
112
+
113
+ # Cancel all cleanup tasks
114
+ for task_name in self._cleanup_tasks:
115
+ cleanup_task: asyncio.Task | None = getattr(starting_app.state, task_name, None)
116
+ if cleanup_task is not None:
117
+ logger.info("Cancelling %s cleanup task", task_name)
118
+ cleanup_task.cancel()
119
+ else:
120
+ logger.warning("No cleanup task found for %s", task_name)
121
+
122
+ self._cleanup_tasks.clear()
123
+
124
+ logger.debug("Closing NAT server from process %s", os.getpid())
125
+
126
+ nat_app = FastAPI(lifespan=lifespan)
127
+
128
+ # Configure app CORS.
129
+ self.set_cors_config(nat_app)
130
+
131
+ @nat_app.middleware("http")
132
+ async def authentication_log_filter(request: Request, call_next: Callable[[Request], Awaitable[Response]]):
133
+ return await self._suppress_authentication_logs(request, call_next)
134
+
135
+ return nat_app
136
+
137
+ def set_cors_config(self, nat_app: FastAPI) -> None:
138
+ """
139
+ Set the cross origin resource sharing configuration.
140
+ """
141
+ cors_kwargs = {}
142
+
143
+ if self.front_end_config.cors.allow_origins is not None:
144
+ cors_kwargs["allow_origins"] = self.front_end_config.cors.allow_origins
145
+
146
+ if self.front_end_config.cors.allow_origin_regex is not None:
147
+ cors_kwargs["allow_origin_regex"] = self.front_end_config.cors.allow_origin_regex
148
+
149
+ if self.front_end_config.cors.allow_methods is not None:
150
+ cors_kwargs["allow_methods"] = self.front_end_config.cors.allow_methods
151
+
152
+ if self.front_end_config.cors.allow_headers is not None:
153
+ cors_kwargs["allow_headers"] = self.front_end_config.cors.allow_headers
154
+
155
+ if self.front_end_config.cors.allow_credentials is not None:
156
+ cors_kwargs["allow_credentials"] = self.front_end_config.cors.allow_credentials
157
+
158
+ if self.front_end_config.cors.expose_headers is not None:
159
+ cors_kwargs["expose_headers"] = self.front_end_config.cors.expose_headers
160
+
161
+ if self.front_end_config.cors.max_age is not None:
162
+ cors_kwargs["max_age"] = self.front_end_config.cors.max_age
163
+
164
+ nat_app.add_middleware(
165
+ CORSMiddleware,
166
+ **cors_kwargs,
167
+ )
168
+
169
+ async def _suppress_authentication_logs(self, request: Request,
170
+ call_next: Callable[[Request], Awaitable[Response]]) -> Response:
171
+ """
172
+ Intercepts authentication request and supreses logs that contain sensitive data.
173
+ """
174
+ from nat.utils.log_utils import LogFilter
175
+
176
+ logs_to_suppress: list[str] = []
177
+
178
+ if (self.front_end_config.oauth2_callback_path):
179
+ logs_to_suppress.append(self.front_end_config.oauth2_callback_path)
180
+
181
+ logging.getLogger("uvicorn.access").addFilter(LogFilter(logs_to_suppress))
182
+ try:
183
+ response = await call_next(request)
184
+ finally:
185
+ logging.getLogger("uvicorn.access").removeFilter(LogFilter(logs_to_suppress))
186
+
187
+ return response
188
+
189
+ @abstractmethod
190
+ async def configure(self, app: FastAPI, builder: WorkflowBuilder):
191
+ pass
192
+
193
+ @abstractmethod
194
+ def get_step_adaptor(self) -> StepAdaptor:
195
+ pass
196
+
197
+
198
+ class RouteInfo(BaseModel):
199
+
200
+ function_name: str | None
201
+
202
+
203
+ class FastApiFrontEndPluginWorker(FastApiFrontEndPluginWorkerBase):
204
+
205
+ def __init__(self, config: Config):
206
+ super().__init__(config)
207
+
208
+ self._outstanding_flows: dict[str, FlowState] = {}
209
+ self._outstanding_flows_lock = asyncio.Lock()
210
+
211
+ @staticmethod
212
+ async def _periodic_cleanup(name: str, job_store: JobStore, sleep_time_sec: int = 300):
213
+ while True:
214
+ try:
215
+ job_store.cleanup_expired_jobs()
216
+ logger.debug("Expired %s jobs cleaned up", name)
217
+ except Exception as e:
218
+ logger.error("Error during %s job cleanup: %s", name, e)
219
+ await asyncio.sleep(sleep_time_sec)
220
+
221
+ async def create_cleanup_task(self, app: FastAPI, name: str, job_store: JobStore, sleep_time_sec: int = 300):
222
+ # Schedule periodic cleanup of expired jobs on first job creation
223
+ attr_name = f"{name}_cleanup_task"
224
+
225
+ # Cheap check, if it doesn't exist, we will need to re-check after we acquire the lock
226
+ if not hasattr(app.state, attr_name):
227
+ async with self._cleanup_tasks_lock:
228
+ if not hasattr(app.state, attr_name):
229
+ logger.info("Starting %s periodic cleanup task", name)
230
+ setattr(
231
+ app.state,
232
+ attr_name,
233
+ asyncio.create_task(
234
+ self._periodic_cleanup(name=name, job_store=job_store, sleep_time_sec=sleep_time_sec)))
235
+ self._cleanup_tasks.append(attr_name)
236
+
237
+ def get_step_adaptor(self) -> StepAdaptor:
238
+
239
+ return StepAdaptor(self.front_end_config.step_adaptor)
240
+
241
+ async def configure(self, app: FastAPI, builder: WorkflowBuilder):
242
+
243
+ # Do things like setting the base URL and global configuration options
244
+ app.root_path = self.front_end_config.root_path
245
+
246
+ await self.add_routes(app, builder)
247
+
248
+ async def add_routes(self, app: FastAPI, builder: WorkflowBuilder):
249
+
250
+ await self.add_default_route(app, SessionManager(builder.build()))
251
+ await self.add_evaluate_route(app, SessionManager(builder.build()))
252
+ await self.add_static_files_route(app, builder)
253
+ await self.add_authorization_route(app)
254
+
255
+ for ep in self.front_end_config.endpoints:
256
+
257
+ entry_workflow = builder.build(entry_function=ep.function_name)
258
+
259
+ await self.add_route(app, endpoint=ep, session_manager=SessionManager(entry_workflow))
260
+
261
+ async def add_default_route(self, app: FastAPI, session_manager: SessionManager):
262
+
263
+ await self.add_route(app, self.front_end_config.workflow, session_manager)
264
+
265
+ async def add_evaluate_route(self, app: FastAPI, session_manager: SessionManager):
266
+ """Add the evaluate endpoint to the FastAPI app."""
267
+
268
+ response_500 = {
269
+ "description": "Internal Server Error",
270
+ "content": {
271
+ "application/json": {
272
+ "example": {
273
+ "detail": "Internal server error occurred"
274
+ }
275
+ }
276
+ },
277
+ }
278
+
279
+ # Create job store for tracking evaluation jobs
280
+ job_store = JobStore()
281
+ # Don't run multiple evaluations at the same time
282
+ evaluation_lock = asyncio.Lock()
283
+
284
+ async def run_evaluation(job_id: str, config_file: str, reps: int, session_manager: SessionManager):
285
+ """Background task to run the evaluation."""
286
+ async with evaluation_lock:
287
+ try:
288
+ # Create EvaluationRunConfig using the CLI defaults
289
+ eval_config = EvaluationRunConfig(config_file=Path(config_file), dataset=None, reps=reps)
290
+
291
+ # Create a new EvaluationRun with the evaluation-specific config
292
+ job_store.update_status(job_id, "running")
293
+ eval_runner = EvaluationRun(eval_config)
294
+ output: EvaluationRunOutput = await eval_runner.run_and_evaluate(session_manager=session_manager,
295
+ job_id=job_id)
296
+ if output.workflow_interrupted:
297
+ job_store.update_status(job_id, "interrupted")
298
+ else:
299
+ parent_dir = os.path.dirname(
300
+ output.workflow_output_file) if output.workflow_output_file else None
301
+
302
+ job_store.update_status(job_id, "success", output_path=str(parent_dir))
303
+ except Exception as e:
304
+ logger.error("Error in evaluation job %s: %s", job_id, str(e))
305
+ job_store.update_status(job_id, "failure", error=str(e))
306
+
307
+ async def start_evaluation(request: EvaluateRequest, background_tasks: BackgroundTasks, http_request: Request):
308
+ """Handle evaluation requests."""
309
+
310
+ async with session_manager.session(request=http_request):
311
+
312
+ # if job_id is present and already exists return the job info
313
+ if request.job_id:
314
+ job = job_store.get_job(request.job_id)
315
+ if job:
316
+ return EvaluateResponse(job_id=job.job_id, status=job.status)
317
+
318
+ job_id = job_store.create_job(request.config_file, request.job_id, request.expiry_seconds)
319
+ await self.create_cleanup_task(app=app, name="async_evaluation", job_store=job_store)
320
+ background_tasks.add_task(run_evaluation, job_id, request.config_file, request.reps, session_manager)
321
+
322
+ return EvaluateResponse(job_id=job_id, status="submitted")
323
+
324
+ def translate_job_to_response(job: JobInfo) -> EvaluateStatusResponse:
325
+ """Translate a JobInfo object to an EvaluateStatusResponse."""
326
+ return EvaluateStatusResponse(job_id=job.job_id,
327
+ status=job.status,
328
+ config_file=str(job.config_file),
329
+ error=job.error,
330
+ output_path=str(job.output_path),
331
+ created_at=job.created_at,
332
+ updated_at=job.updated_at,
333
+ expires_at=job_store.get_expires_at(job))
334
+
335
+ async def get_job_status(job_id: str, http_request: Request) -> EvaluateStatusResponse:
336
+ """Get the status of an evaluation job."""
337
+ logger.info("Getting status for job %s", job_id)
338
+
339
+ async with session_manager.session(request=http_request):
340
+
341
+ job = job_store.get_job(job_id)
342
+ if not job:
343
+ logger.warning("Job %s not found", job_id)
344
+ raise HTTPException(status_code=404, detail=f"Job {job_id} not found")
345
+ logger.info("Found job %s with status %s", job_id, job.status)
346
+ return translate_job_to_response(job)
347
+
348
+ async def get_last_job_status(http_request: Request) -> EvaluateStatusResponse:
349
+ """Get the status of the last created evaluation job."""
350
+ logger.info("Getting last job status")
351
+
352
+ async with session_manager.session(request=http_request):
353
+
354
+ job = job_store.get_last_job()
355
+ if not job:
356
+ logger.warning("No jobs found when requesting last job status")
357
+ raise HTTPException(status_code=404, detail="No jobs found")
358
+ logger.info("Found last job %s with status %s", job.job_id, job.status)
359
+ return translate_job_to_response(job)
360
+
361
+ async def get_jobs(http_request: Request, status: str | None = None) -> list[EvaluateStatusResponse]:
362
+ """Get all jobs, optionally filtered by status."""
363
+
364
+ async with session_manager.session(request=http_request):
365
+
366
+ if status is None:
367
+ logger.info("Getting all jobs")
368
+ jobs = job_store.get_all_jobs()
369
+ else:
370
+ logger.info("Getting jobs with status %s", status)
371
+ jobs = job_store.get_jobs_by_status(status)
372
+ logger.info("Found %d jobs", len(jobs))
373
+ return [translate_job_to_response(job) for job in jobs]
374
+
375
+ if self.front_end_config.evaluate.path:
376
+ # Add last job endpoint first (most specific)
377
+ app.add_api_route(
378
+ path=f"{self.front_end_config.evaluate.path}/job/last",
379
+ endpoint=get_last_job_status,
380
+ methods=["GET"],
381
+ response_model=EvaluateStatusResponse,
382
+ description="Get the status of the last created evaluation job",
383
+ responses={
384
+ 404: {
385
+ "description": "No jobs found"
386
+ }, 500: response_500
387
+ },
388
+ )
389
+
390
+ # Add specific job endpoint (least specific)
391
+ app.add_api_route(
392
+ path=f"{self.front_end_config.evaluate.path}/job/{{job_id}}",
393
+ endpoint=get_job_status,
394
+ methods=["GET"],
395
+ response_model=EvaluateStatusResponse,
396
+ description="Get the status of an evaluation job",
397
+ responses={
398
+ 404: {
399
+ "description": "Job not found"
400
+ }, 500: response_500
401
+ },
402
+ )
403
+
404
+ # Add jobs endpoint with optional status query parameter
405
+ app.add_api_route(
406
+ path=f"{self.front_end_config.evaluate.path}/jobs",
407
+ endpoint=get_jobs,
408
+ methods=["GET"],
409
+ response_model=list[EvaluateStatusResponse],
410
+ description="Get all jobs, optionally filtered by status",
411
+ responses={500: response_500},
412
+ )
413
+
414
+ # Add HTTP endpoint for evaluation
415
+ app.add_api_route(
416
+ path=self.front_end_config.evaluate.path,
417
+ endpoint=start_evaluation,
418
+ methods=[self.front_end_config.evaluate.method],
419
+ response_model=EvaluateResponse,
420
+ description=self.front_end_config.evaluate.description,
421
+ responses={500: response_500},
422
+ )
423
+
424
+ async def add_static_files_route(self, app: FastAPI, builder: WorkflowBuilder):
425
+
426
+ if not self.front_end_config.object_store:
427
+ logger.debug("No object store configured, skipping static files route")
428
+ return
429
+
430
+ object_store_client = await builder.get_object_store_client(self.front_end_config.object_store)
431
+
432
+ def sanitize_path(path: str) -> str:
433
+ sanitized_path = os.path.normpath(path.strip("/"))
434
+ if sanitized_path == ".":
435
+ raise HTTPException(status_code=400, detail="Invalid file path.")
436
+ filename = os.path.basename(sanitized_path)
437
+ if not filename:
438
+ raise HTTPException(status_code=400, detail="Filename cannot be empty.")
439
+ return sanitized_path
440
+
441
+ # Upload static files to the object store; if key is present, it will fail with 409 Conflict
442
+ async def add_static_file(file_path: str, file: UploadFile):
443
+ sanitized_file_path = sanitize_path(file_path)
444
+ file_data = await file.read()
445
+
446
+ try:
447
+ await object_store_client.put_object(sanitized_file_path,
448
+ ObjectStoreItem(data=file_data, content_type=file.content_type))
449
+ except KeyAlreadyExistsError as e:
450
+ raise HTTPException(status_code=409, detail=str(e)) from e
451
+
452
+ return {"filename": sanitized_file_path}
453
+
454
+ # Upsert static files to the object store; if key is present, it will overwrite the file
455
+ async def upsert_static_file(file_path: str, file: UploadFile):
456
+ sanitized_file_path = sanitize_path(file_path)
457
+ file_data = await file.read()
458
+
459
+ await object_store_client.upsert_object(sanitized_file_path,
460
+ ObjectStoreItem(data=file_data, content_type=file.content_type))
461
+
462
+ return {"filename": sanitized_file_path}
463
+
464
+ # Get static files from the object store
465
+ async def get_static_file(file_path: str):
466
+
467
+ try:
468
+ file_data = await object_store_client.get_object(file_path)
469
+ except NoSuchKeyError as e:
470
+ raise HTTPException(status_code=404, detail=str(e)) from e
471
+
472
+ filename = file_path.split("/")[-1]
473
+
474
+ async def reader():
475
+ yield file_data.data
476
+
477
+ return StreamingResponse(reader(),
478
+ media_type=file_data.content_type,
479
+ headers={"Content-Disposition": f"attachment; filename={filename}"})
480
+
481
+ async def delete_static_file(file_path: str):
482
+ try:
483
+ await object_store_client.delete_object(file_path)
484
+ except NoSuchKeyError as e:
485
+ raise HTTPException(status_code=404, detail=str(e)) from e
486
+
487
+ return Response(status_code=204)
488
+
489
+ # Add the static files route to the FastAPI app
490
+ app.add_api_route(
491
+ path="/static/{file_path:path}",
492
+ endpoint=add_static_file,
493
+ methods=["POST"],
494
+ description="Upload a static file to the object store",
495
+ )
496
+
497
+ app.add_api_route(
498
+ path="/static/{file_path:path}",
499
+ endpoint=upsert_static_file,
500
+ methods=["PUT"],
501
+ description="Upsert a static file to the object store",
502
+ )
503
+
504
+ app.add_api_route(
505
+ path="/static/{file_path:path}",
506
+ endpoint=get_static_file,
507
+ methods=["GET"],
508
+ description="Get a static file from the object store",
509
+ )
510
+
511
+ app.add_api_route(
512
+ path="/static/{file_path:path}",
513
+ endpoint=delete_static_file,
514
+ methods=["DELETE"],
515
+ description="Delete a static file from the object store",
516
+ )
517
+
518
+ async def add_route(self,
519
+ app: FastAPI,
520
+ endpoint: FastApiFrontEndConfig.EndpointBase,
521
+ session_manager: SessionManager):
522
+
523
+ workflow = session_manager.workflow
524
+
525
+ GenerateBodyType = workflow.input_schema # pylint: disable=invalid-name
526
+ GenerateStreamResponseType = workflow.streaming_output_schema # pylint: disable=invalid-name
527
+ GenerateSingleResponseType = workflow.single_output_schema # pylint: disable=invalid-name
528
+
529
+ # Append job_id and expiry_seconds to the input schema, this effectively makes these reserved keywords
530
+ # Consider prefixing these with "nat_" to avoid conflicts
531
+ class AsyncGenerateRequest(GenerateBodyType):
532
+ job_id: str | None = Field(default=None, description="Unique identifier for the evaluation job")
533
+ sync_timeout: int = Field(
534
+ default=0,
535
+ ge=0,
536
+ le=300,
537
+ description="Attempt to perform the job synchronously up until `sync_timeout` sectonds, "
538
+ "if the job hasn't been completed by then a job_id will be returned with a status code of 202.")
539
+ expiry_seconds: int = Field(default=JobStore.DEFAULT_EXPIRY,
540
+ ge=JobStore.MIN_EXPIRY,
541
+ le=JobStore.MAX_EXPIRY,
542
+ description="Optional time (in seconds) before the job expires. "
543
+ "Clamped between 600 (10 min) and 86400 (24h).")
544
+
545
+ # Ensure that the input is in the body. POD types are treated as query parameters
546
+ if (not issubclass(GenerateBodyType, BaseModel)):
547
+ GenerateBodyType = typing.Annotated[GenerateBodyType, Body()]
548
+ else:
549
+ logger.info("Expecting generate request payloads in the following format: %s",
550
+ GenerateBodyType.model_fields)
551
+
552
+ response_500 = {
553
+ "description": "Internal Server Error",
554
+ "content": {
555
+ "application/json": {
556
+ "example": {
557
+ "detail": "Internal server error occurred"
558
+ }
559
+ }
560
+ },
561
+ }
562
+
563
+ # Create job store for tracking async generation jobs
564
+ job_store = JobStore()
565
+
566
+ # Run up to max_running_async_jobs jobs at the same time
567
+ async_job_concurrency = asyncio.Semaphore(self._front_end_config.max_running_async_jobs)
568
+
569
+ def get_single_endpoint(result_type: type | None):
570
+
571
+ async def get_single(response: Response, request: Request):
572
+
573
+ response.headers["Content-Type"] = "application/json"
574
+
575
+ async with session_manager.session(request=request,
576
+ user_authentication_callback=self._http_flow_handler.authenticate):
577
+
578
+ return await generate_single_response(None, session_manager, result_type=result_type)
579
+
580
+ return get_single
581
+
582
+ def get_streaming_endpoint(streaming: bool, result_type: type | None, output_type: type | None):
583
+
584
+ async def get_stream(request: Request):
585
+
586
+ async with session_manager.session(request=request,
587
+ user_authentication_callback=self._http_flow_handler.authenticate):
588
+
589
+ return StreamingResponse(headers={"Content-Type": "text/event-stream; charset=utf-8"},
590
+ content=generate_streaming_response_as_str(
591
+ None,
592
+ session_manager=session_manager,
593
+ streaming=streaming,
594
+ step_adaptor=self.get_step_adaptor(),
595
+ result_type=result_type,
596
+ output_type=output_type))
597
+
598
+ return get_stream
599
+
600
+ def get_streaming_raw_endpoint(streaming: bool, result_type: type | None, output_type: type | None):
601
+
602
+ async def get_stream(filter_steps: str | None = None):
603
+
604
+ return StreamingResponse(headers={"Content-Type": "text/event-stream; charset=utf-8"},
605
+ content=generate_streaming_response_full_as_str(
606
+ None,
607
+ session_manager=session_manager,
608
+ streaming=streaming,
609
+ result_type=result_type,
610
+ output_type=output_type,
611
+ filter_steps=filter_steps))
612
+
613
+ return get_stream
614
+
615
+ def post_single_endpoint(request_type: type, result_type: type | None):
616
+
617
+ async def post_single(response: Response, request: Request, payload: request_type):
618
+
619
+ response.headers["Content-Type"] = "application/json"
620
+
621
+ async with session_manager.session(request=request,
622
+ user_authentication_callback=self._http_flow_handler.authenticate):
623
+
624
+ return await generate_single_response(payload, session_manager, result_type=result_type)
625
+
626
+ return post_single
627
+
628
+ def post_streaming_endpoint(request_type: type,
629
+ streaming: bool,
630
+ result_type: type | None,
631
+ output_type: type | None):
632
+
633
+ async def post_stream(request: Request, payload: request_type):
634
+
635
+ async with session_manager.session(request=request,
636
+ user_authentication_callback=self._http_flow_handler.authenticate):
637
+
638
+ return StreamingResponse(headers={"Content-Type": "text/event-stream; charset=utf-8"},
639
+ content=generate_streaming_response_as_str(
640
+ payload,
641
+ session_manager=session_manager,
642
+ streaming=streaming,
643
+ step_adaptor=self.get_step_adaptor(),
644
+ result_type=result_type,
645
+ output_type=output_type))
646
+
647
+ return post_stream
648
+
649
+ def post_streaming_raw_endpoint(request_type: type,
650
+ streaming: bool,
651
+ result_type: type | None,
652
+ output_type: type | None):
653
+ """
654
+ Stream raw intermediate steps without any step adaptor translations.
655
+ """
656
+
657
+ async def post_stream(payload: request_type, filter_steps: str | None = None):
658
+
659
+ return StreamingResponse(headers={"Content-Type": "text/event-stream; charset=utf-8"},
660
+ content=generate_streaming_response_full_as_str(
661
+ payload,
662
+ session_manager=session_manager,
663
+ streaming=streaming,
664
+ result_type=result_type,
665
+ output_type=output_type,
666
+ filter_steps=filter_steps))
667
+
668
+ return post_stream
669
+
670
+ def post_openai_api_compatible_endpoint(request_type: type):
671
+ """
672
+ OpenAI-compatible endpoint that handles both streaming and non-streaming
673
+ based on the 'stream' parameter in the request.
674
+ """
675
+
676
+ async def post_openai_api_compatible(response: Response, request: Request, payload: request_type):
677
+ # Check if streaming is requested
678
+ stream_requested = getattr(payload, 'stream', False)
679
+
680
+ async with session_manager.session(request=request):
681
+ if stream_requested:
682
+ # Return streaming response
683
+ return StreamingResponse(headers={"Content-Type": "text/event-stream; charset=utf-8"},
684
+ content=generate_streaming_response_as_str(
685
+ payload,
686
+ session_manager=session_manager,
687
+ streaming=True,
688
+ step_adaptor=self.get_step_adaptor(),
689
+ result_type=ChatResponseChunk,
690
+ output_type=ChatResponseChunk))
691
+ else:
692
+ # Return single response - check if workflow supports non-streaming
693
+ try:
694
+ response.headers["Content-Type"] = "application/json"
695
+ return await generate_single_response(payload, session_manager, result_type=ChatResponse)
696
+ except ValueError as e:
697
+ if "Cannot get a single output value for streaming workflows" in str(e):
698
+ # Workflow only supports streaming, but client requested non-streaming
699
+ # Fall back to streaming and collect the result
700
+ chunks = []
701
+ async for chunk_str in generate_streaming_response_as_str(
702
+ payload,
703
+ session_manager=session_manager,
704
+ streaming=True,
705
+ step_adaptor=self.get_step_adaptor(),
706
+ result_type=ChatResponseChunk,
707
+ output_type=ChatResponseChunk):
708
+ if chunk_str.startswith("data: ") and not chunk_str.startswith("data: [DONE]"):
709
+ chunk_data = chunk_str[6:].strip() # Remove "data: " prefix
710
+ if chunk_data:
711
+ try:
712
+ chunk_json = ChatResponseChunk.model_validate_json(chunk_data)
713
+ if (chunk_json.choices and len(chunk_json.choices) > 0
714
+ and chunk_json.choices[0].delta
715
+ and chunk_json.choices[0].delta.content is not None):
716
+ chunks.append(chunk_json.choices[0].delta.content)
717
+ except Exception:
718
+ continue
719
+
720
+ # Create a single response from collected chunks
721
+ content = "".join(chunks)
722
+ single_response = ChatResponse.from_string(content)
723
+ response.headers["Content-Type"] = "application/json"
724
+ return single_response
725
+ else:
726
+ raise
727
+
728
+ return post_openai_api_compatible
729
+
730
+ async def run_generation(job_id: str, payload: typing.Any, session_manager: SessionManager, result_type: type):
731
+ """Background task to run the evaluation."""
732
+ async with async_job_concurrency:
733
+ try:
734
+ result = await generate_single_response(payload=payload,
735
+ session_manager=session_manager,
736
+ result_type=result_type)
737
+ job_store.update_status(job_id, "success", output=result)
738
+ except Exception as e:
739
+ logger.error("Error in evaluation job %s: %s", job_id, e)
740
+ job_store.update_status(job_id, "failure", error=str(e))
741
+
742
+ def _job_status_to_response(job: JobInfo) -> AsyncGenerationStatusResponse:
743
+ job_output = job.output
744
+ if job_output is not None:
745
+ job_output = job_output.model_dump()
746
+ return AsyncGenerationStatusResponse(job_id=job.job_id,
747
+ status=job.status,
748
+ error=job.error,
749
+ output=job_output,
750
+ created_at=job.created_at,
751
+ updated_at=job.updated_at,
752
+ expires_at=job_store.get_expires_at(job))
753
+
754
+ def post_async_generation(request_type: type, final_result_type: type):
755
+
756
+ async def start_async_generation(
757
+ request: request_type, background_tasks: BackgroundTasks, response: Response,
758
+ http_request: Request) -> AsyncGenerateResponse | AsyncGenerationStatusResponse:
759
+ """Handle async generation requests."""
760
+
761
+ async with session_manager.session(request=http_request):
762
+
763
+ # if job_id is present and already exists return the job info
764
+ if request.job_id:
765
+ job = job_store.get_job(request.job_id)
766
+ if job:
767
+ return AsyncGenerateResponse(job_id=job.job_id, status=job.status)
768
+
769
+ job_id = job_store.create_job(job_id=request.job_id, expiry_seconds=request.expiry_seconds)
770
+ await self.create_cleanup_task(app=app, name="async_generation", job_store=job_store)
771
+
772
+ # The fastapi/starlette background tasks won't begin executing until after the response is sent
773
+ # to the client, so we need to wrap the task in a function, alowing us to start the task now,
774
+ # and allowing the background task function to await the results.
775
+ task = asyncio.create_task(
776
+ run_generation(job_id=job_id,
777
+ payload=request,
778
+ session_manager=session_manager,
779
+ result_type=final_result_type))
780
+
781
+ async def wrapped_task(t: asyncio.Task):
782
+ return await t
783
+
784
+ background_tasks.add_task(wrapped_task, task)
785
+
786
+ now = time.time()
787
+ sync_timeout = now + request.sync_timeout
788
+ while time.time() < sync_timeout:
789
+ job = job_store.get_job(job_id)
790
+ if job is not None and job.status not in job_store.ACTIVE_STATUS:
791
+ # If the job is done, return the result
792
+ response.status_code = 200
793
+ return _job_status_to_response(job)
794
+
795
+ # Sleep for a short time before checking again
796
+ await asyncio.sleep(0.1)
797
+
798
+ response.status_code = 202
799
+ return AsyncGenerateResponse(job_id=job_id, status="submitted")
800
+
801
+ return start_async_generation
802
+
803
+ async def get_async_job_status(job_id: str, http_request: Request) -> AsyncGenerationStatusResponse:
804
+ """Get the status of an async job."""
805
+ logger.info("Getting status for job %s", job_id)
806
+
807
+ async with session_manager.session(request=http_request):
808
+
809
+ job = job_store.get_job(job_id)
810
+ if not job:
811
+ logger.warning("Job %s not found", job_id)
812
+ raise HTTPException(status_code=404, detail=f"Job {job_id} not found")
813
+
814
+ logger.info("Found job %s with status %s", job_id, job.status)
815
+ return _job_status_to_response(job)
816
+
817
+ async def websocket_endpoint(websocket: WebSocket):
818
+
819
+ # Universal cookie handling: works for both cross-origin and same-origin connections
820
+ session_id = websocket.query_params.get("session")
821
+ if session_id:
822
+ headers = list(websocket.scope.get("headers", []))
823
+ cookie_header = f"nat-session={session_id}"
824
+
825
+ # Check if the session cookie already exists to avoid duplicates
826
+ cookie_exists = False
827
+ existing_session_cookie = False
828
+
829
+ for i, (name, value) in enumerate(headers):
830
+ if name == b"cookie":
831
+ cookie_exists = True
832
+ cookie_str = value.decode()
833
+
834
+ # Check if nat-session already exists in cookies
835
+ if "nat-session=" in cookie_str:
836
+ existing_session_cookie = True
837
+ logger.info("WebSocket: Session cookie already present in headers (same-origin)")
838
+ else:
839
+ # Append to existing cookie header (cross-origin case)
840
+ headers[i] = (name, f"{cookie_str}; {cookie_header}".encode())
841
+ logger.info("WebSocket: Added session cookie to existing cookie header: %s",
842
+ session_id[:10] + "...")
843
+ break
844
+
845
+ # Add new cookie header only if no cookies exist and no session cookie found
846
+ if not cookie_exists and not existing_session_cookie:
847
+ headers.append((b"cookie", cookie_header.encode()))
848
+ logger.info("WebSocket: Added new session cookie header: %s", session_id[:10] + "...")
849
+
850
+ # Update the websocket scope with the modified headers
851
+ websocket.scope["headers"] = headers
852
+
853
+ async with WebSocketMessageHandler(websocket, session_manager, self.get_step_adaptor()) as handler:
854
+
855
+ flow_handler = WebSocketAuthenticationFlowHandler(self._add_flow, self._remove_flow, handler)
856
+
857
+ # Ugly hack to set the flow handler on the message handler. Both need eachother to be set.
858
+ handler.set_flow_handler(flow_handler)
859
+
860
+ await handler.run()
861
+
862
+ if (endpoint.websocket_path):
863
+ app.add_websocket_route(endpoint.websocket_path, websocket_endpoint)
864
+
865
+ if (endpoint.path):
866
+
867
+ if (endpoint.method == "GET"):
868
+
869
+ app.add_api_route(
870
+ path=endpoint.path,
871
+ endpoint=get_single_endpoint(result_type=GenerateSingleResponseType),
872
+ methods=[endpoint.method],
873
+ response_model=GenerateSingleResponseType,
874
+ description=endpoint.description,
875
+ responses={500: response_500},
876
+ )
877
+
878
+ app.add_api_route(
879
+ path=f"{endpoint.path}/stream",
880
+ endpoint=get_streaming_endpoint(streaming=True,
881
+ result_type=GenerateStreamResponseType,
882
+ output_type=GenerateStreamResponseType),
883
+ methods=[endpoint.method],
884
+ response_model=GenerateStreamResponseType,
885
+ description=endpoint.description,
886
+ responses={500: response_500},
887
+ )
888
+
889
+ app.add_api_route(
890
+ path=f"{endpoint.path}/full",
891
+ endpoint=get_streaming_raw_endpoint(streaming=True,
892
+ result_type=GenerateStreamResponseType,
893
+ output_type=GenerateStreamResponseType),
894
+ methods=[endpoint.method],
895
+ description="Stream raw intermediate steps without any step adaptor translations.\n"
896
+ "Use filter_steps query parameter to filter steps by type (comma-separated list) or\
897
+ set to 'none' to suppress all intermediate steps.",
898
+ )
899
+
900
+ elif (endpoint.method == "POST"):
901
+
902
+ app.add_api_route(
903
+ path=endpoint.path,
904
+ endpoint=post_single_endpoint(request_type=GenerateBodyType,
905
+ result_type=GenerateSingleResponseType),
906
+ methods=[endpoint.method],
907
+ response_model=GenerateSingleResponseType,
908
+ description=endpoint.description,
909
+ responses={500: response_500},
910
+ )
911
+
912
+ app.add_api_route(
913
+ path=f"{endpoint.path}/stream",
914
+ endpoint=post_streaming_endpoint(request_type=GenerateBodyType,
915
+ streaming=True,
916
+ result_type=GenerateStreamResponseType,
917
+ output_type=GenerateStreamResponseType),
918
+ methods=[endpoint.method],
919
+ response_model=GenerateStreamResponseType,
920
+ description=endpoint.description,
921
+ responses={500: response_500},
922
+ )
923
+
924
+ app.add_api_route(
925
+ path=f"{endpoint.path}/full",
926
+ endpoint=post_streaming_raw_endpoint(request_type=GenerateBodyType,
927
+ streaming=True,
928
+ result_type=GenerateStreamResponseType,
929
+ output_type=GenerateStreamResponseType),
930
+ methods=[endpoint.method],
931
+ response_model=GenerateStreamResponseType,
932
+ description="Stream raw intermediate steps without any step adaptor translations.\n"
933
+ "Use filter_steps query parameter to filter steps by type (comma-separated list) or \
934
+ set to 'none' to suppress all intermediate steps.",
935
+ responses={500: response_500},
936
+ )
937
+
938
+ app.add_api_route(
939
+ path=f"{endpoint.path}/async",
940
+ endpoint=post_async_generation(request_type=AsyncGenerateRequest,
941
+ final_result_type=GenerateSingleResponseType),
942
+ methods=[endpoint.method],
943
+ response_model=AsyncGenerateResponse | AsyncGenerationStatusResponse,
944
+ description="Start an async generate job",
945
+ responses={500: response_500},
946
+ )
947
+ else:
948
+ raise ValueError(f"Unsupported method {endpoint.method}")
949
+
950
+ app.add_api_route(
951
+ path=f"{endpoint.path}/async/job/{{job_id}}",
952
+ endpoint=get_async_job_status,
953
+ methods=["GET"],
954
+ response_model=AsyncGenerationStatusResponse,
955
+ description="Get the status of an async job",
956
+ responses={
957
+ 404: {
958
+ "description": "Job not found"
959
+ }, 500: response_500
960
+ },
961
+ )
962
+
963
+ if (endpoint.openai_api_path):
964
+ if (endpoint.method == "GET"):
965
+
966
+ app.add_api_route(
967
+ path=endpoint.openai_api_path,
968
+ endpoint=get_single_endpoint(result_type=ChatResponse),
969
+ methods=[endpoint.method],
970
+ response_model=ChatResponse,
971
+ description=endpoint.description,
972
+ responses={500: response_500},
973
+ )
974
+
975
+ app.add_api_route(
976
+ path=f"{endpoint.openai_api_path}/stream",
977
+ endpoint=get_streaming_endpoint(streaming=True,
978
+ result_type=ChatResponseChunk,
979
+ output_type=ChatResponseChunk),
980
+ methods=[endpoint.method],
981
+ response_model=ChatResponseChunk,
982
+ description=endpoint.description,
983
+ responses={500: response_500},
984
+ )
985
+
986
+ elif (endpoint.method == "POST"):
987
+
988
+ # Check if OpenAI v1 compatible endpoint is configured
989
+ openai_v1_path = getattr(endpoint, 'openai_api_v1_path', None)
990
+
991
+ # Always create legacy endpoints for backward compatibility (unless they conflict with v1 path)
992
+ if not openai_v1_path or openai_v1_path != endpoint.openai_api_path:
993
+ # <openai_api_path> = non-streaming (legacy behavior)
994
+ app.add_api_route(
995
+ path=endpoint.openai_api_path,
996
+ endpoint=post_single_endpoint(request_type=ChatRequest, result_type=ChatResponse),
997
+ methods=[endpoint.method],
998
+ response_model=ChatResponse,
999
+ description=endpoint.description,
1000
+ responses={500: response_500},
1001
+ )
1002
+
1003
+ # <openai_api_path>/stream = streaming (legacy behavior)
1004
+ app.add_api_route(
1005
+ path=f"{endpoint.openai_api_path}/stream",
1006
+ endpoint=post_streaming_endpoint(request_type=ChatRequest,
1007
+ streaming=True,
1008
+ result_type=ChatResponseChunk,
1009
+ output_type=ChatResponseChunk),
1010
+ methods=[endpoint.method],
1011
+ response_model=ChatResponseChunk | ResponseIntermediateStep,
1012
+ description=endpoint.description,
1013
+ responses={500: response_500},
1014
+ )
1015
+
1016
+ # Create OpenAI v1 compatible endpoint if configured
1017
+ if openai_v1_path:
1018
+ # OpenAI v1 Compatible Mode: Create single endpoint that handles both streaming and non-streaming
1019
+ app.add_api_route(
1020
+ path=openai_v1_path,
1021
+ endpoint=post_openai_api_compatible_endpoint(request_type=ChatRequest),
1022
+ methods=[endpoint.method],
1023
+ response_model=ChatResponse | ChatResponseChunk,
1024
+ description=f"{endpoint.description} (OpenAI Chat Completions API compatible)",
1025
+ responses={500: response_500},
1026
+ )
1027
+
1028
+ else:
1029
+ raise ValueError(f"Unsupported method {endpoint.method}")
1030
+
1031
+ async def add_authorization_route(self, app: FastAPI):
1032
+
1033
+ from fastapi.responses import HTMLResponse
1034
+
1035
+ from nat.front_ends.fastapi.html_snippets.auth_code_grant_success import AUTH_REDIRECT_SUCCESS_HTML
1036
+
1037
+ async def redirect_uri(request: Request):
1038
+ """
1039
+ Handle the redirect URI for OAuth2 authentication.
1040
+ Args:
1041
+ request: The FastAPI request object containing query parameters.
1042
+
1043
+ Returns:
1044
+ HTMLResponse: A response indicating the success of the authentication flow.
1045
+ """
1046
+ state = request.query_params.get("state")
1047
+
1048
+ async with self._outstanding_flows_lock:
1049
+ if not state or state not in self._outstanding_flows:
1050
+ return "Invalid state. Please restart the authentication process."
1051
+
1052
+ flow_state = self._outstanding_flows[state]
1053
+
1054
+ config = flow_state.config
1055
+ verifier = flow_state.verifier
1056
+ client = flow_state.client
1057
+
1058
+ try:
1059
+ res = await client.fetch_token(url=config.token_url,
1060
+ authorization_response=str(request.url),
1061
+ code_verifier=verifier,
1062
+ state=state)
1063
+ flow_state.future.set_result(res)
1064
+ except Exception as e:
1065
+ flow_state.future.set_exception(e)
1066
+
1067
+ return HTMLResponse(content=AUTH_REDIRECT_SUCCESS_HTML,
1068
+ status_code=200,
1069
+ headers={
1070
+ "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-cache"
1071
+ })
1072
+
1073
+ if (self.front_end_config.oauth2_callback_path):
1074
+ # Add the redirect URI route
1075
+ app.add_api_route(
1076
+ path=self.front_end_config.oauth2_callback_path,
1077
+ endpoint=redirect_uri,
1078
+ methods=["GET"],
1079
+ description="Handles the authorization code and state returned from the Authorization Code Grant Flow.")
1080
+
1081
+ async def _add_flow(self, state: str, flow_state: FlowState):
1082
+ async with self._outstanding_flows_lock:
1083
+ self._outstanding_flows[state] = flow_state
1084
+
1085
+ async def _remove_flow(self, state: str):
1086
+ async with self._outstanding_flows_lock:
1087
+ del self._outstanding_flows[state]