aiqtoolkit 1.1.0a20250503__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.

Potentially problematic release.


This version of aiqtoolkit might be problematic. Click here for more details.

Files changed (314) hide show
  1. aiq/agent/__init__.py +0 -0
  2. aiq/agent/base.py +76 -0
  3. aiq/agent/dual_node.py +67 -0
  4. aiq/agent/react_agent/__init__.py +0 -0
  5. aiq/agent/react_agent/agent.py +322 -0
  6. aiq/agent/react_agent/output_parser.py +104 -0
  7. aiq/agent/react_agent/prompt.py +46 -0
  8. aiq/agent/react_agent/register.py +148 -0
  9. aiq/agent/reasoning_agent/__init__.py +0 -0
  10. aiq/agent/reasoning_agent/reasoning_agent.py +224 -0
  11. aiq/agent/register.py +23 -0
  12. aiq/agent/rewoo_agent/__init__.py +0 -0
  13. aiq/agent/rewoo_agent/agent.py +410 -0
  14. aiq/agent/rewoo_agent/prompt.py +108 -0
  15. aiq/agent/rewoo_agent/register.py +158 -0
  16. aiq/agent/tool_calling_agent/__init__.py +0 -0
  17. aiq/agent/tool_calling_agent/agent.py +123 -0
  18. aiq/agent/tool_calling_agent/register.py +105 -0
  19. aiq/builder/__init__.py +0 -0
  20. aiq/builder/builder.py +223 -0
  21. aiq/builder/component_utils.py +303 -0
  22. aiq/builder/context.py +212 -0
  23. aiq/builder/embedder.py +24 -0
  24. aiq/builder/eval_builder.py +116 -0
  25. aiq/builder/evaluator.py +29 -0
  26. aiq/builder/framework_enum.py +24 -0
  27. aiq/builder/front_end.py +73 -0
  28. aiq/builder/function.py +297 -0
  29. aiq/builder/function_base.py +376 -0
  30. aiq/builder/function_info.py +627 -0
  31. aiq/builder/intermediate_step_manager.py +127 -0
  32. aiq/builder/llm.py +25 -0
  33. aiq/builder/retriever.py +25 -0
  34. aiq/builder/user_interaction_manager.py +71 -0
  35. aiq/builder/workflow.py +143 -0
  36. aiq/builder/workflow_builder.py +749 -0
  37. aiq/cli/__init__.py +14 -0
  38. aiq/cli/cli_utils/__init__.py +0 -0
  39. aiq/cli/cli_utils/config_override.py +233 -0
  40. aiq/cli/cli_utils/validation.py +37 -0
  41. aiq/cli/commands/__init__.py +0 -0
  42. aiq/cli/commands/configure/__init__.py +0 -0
  43. aiq/cli/commands/configure/channel/__init__.py +0 -0
  44. aiq/cli/commands/configure/channel/add.py +28 -0
  45. aiq/cli/commands/configure/channel/channel.py +36 -0
  46. aiq/cli/commands/configure/channel/remove.py +30 -0
  47. aiq/cli/commands/configure/channel/update.py +30 -0
  48. aiq/cli/commands/configure/configure.py +33 -0
  49. aiq/cli/commands/evaluate.py +139 -0
  50. aiq/cli/commands/info/__init__.py +14 -0
  51. aiq/cli/commands/info/info.py +37 -0
  52. aiq/cli/commands/info/list_channels.py +32 -0
  53. aiq/cli/commands/info/list_components.py +129 -0
  54. aiq/cli/commands/registry/__init__.py +14 -0
  55. aiq/cli/commands/registry/publish.py +88 -0
  56. aiq/cli/commands/registry/pull.py +118 -0
  57. aiq/cli/commands/registry/registry.py +38 -0
  58. aiq/cli/commands/registry/remove.py +108 -0
  59. aiq/cli/commands/registry/search.py +155 -0
  60. aiq/cli/commands/start.py +250 -0
  61. aiq/cli/commands/uninstall.py +83 -0
  62. aiq/cli/commands/validate.py +47 -0
  63. aiq/cli/commands/workflow/__init__.py +14 -0
  64. aiq/cli/commands/workflow/templates/__init__.py.j2 +0 -0
  65. aiq/cli/commands/workflow/templates/config.yml.j2 +16 -0
  66. aiq/cli/commands/workflow/templates/pyproject.toml.j2 +22 -0
  67. aiq/cli/commands/workflow/templates/register.py.j2 +5 -0
  68. aiq/cli/commands/workflow/templates/workflow.py.j2 +36 -0
  69. aiq/cli/commands/workflow/workflow.py +37 -0
  70. aiq/cli/commands/workflow/workflow_commands.py +313 -0
  71. aiq/cli/entrypoint.py +133 -0
  72. aiq/cli/main.py +44 -0
  73. aiq/cli/register_workflow.py +408 -0
  74. aiq/cli/type_registry.py +879 -0
  75. aiq/data_models/__init__.py +14 -0
  76. aiq/data_models/api_server.py +588 -0
  77. aiq/data_models/common.py +143 -0
  78. aiq/data_models/component.py +46 -0
  79. aiq/data_models/component_ref.py +135 -0
  80. aiq/data_models/config.py +349 -0
  81. aiq/data_models/dataset_handler.py +122 -0
  82. aiq/data_models/discovery_metadata.py +269 -0
  83. aiq/data_models/embedder.py +26 -0
  84. aiq/data_models/evaluate.py +104 -0
  85. aiq/data_models/evaluator.py +26 -0
  86. aiq/data_models/front_end.py +26 -0
  87. aiq/data_models/function.py +30 -0
  88. aiq/data_models/function_dependencies.py +64 -0
  89. aiq/data_models/interactive.py +237 -0
  90. aiq/data_models/intermediate_step.py +269 -0
  91. aiq/data_models/invocation_node.py +38 -0
  92. aiq/data_models/llm.py +26 -0
  93. aiq/data_models/logging.py +26 -0
  94. aiq/data_models/memory.py +26 -0
  95. aiq/data_models/profiler.py +53 -0
  96. aiq/data_models/registry_handler.py +26 -0
  97. aiq/data_models/retriever.py +30 -0
  98. aiq/data_models/step_adaptor.py +64 -0
  99. aiq/data_models/streaming.py +33 -0
  100. aiq/data_models/swe_bench_model.py +54 -0
  101. aiq/data_models/telemetry_exporter.py +26 -0
  102. aiq/embedder/__init__.py +0 -0
  103. aiq/embedder/langchain_client.py +41 -0
  104. aiq/embedder/nim_embedder.py +58 -0
  105. aiq/embedder/openai_embedder.py +42 -0
  106. aiq/embedder/register.py +24 -0
  107. aiq/eval/__init__.py +14 -0
  108. aiq/eval/config.py +42 -0
  109. aiq/eval/dataset_handler/__init__.py +0 -0
  110. aiq/eval/dataset_handler/dataset_downloader.py +106 -0
  111. aiq/eval/dataset_handler/dataset_filter.py +52 -0
  112. aiq/eval/dataset_handler/dataset_handler.py +169 -0
  113. aiq/eval/evaluate.py +323 -0
  114. aiq/eval/evaluator/__init__.py +14 -0
  115. aiq/eval/evaluator/evaluator_model.py +44 -0
  116. aiq/eval/intermediate_step_adapter.py +93 -0
  117. aiq/eval/rag_evaluator/__init__.py +0 -0
  118. aiq/eval/rag_evaluator/evaluate.py +138 -0
  119. aiq/eval/rag_evaluator/register.py +138 -0
  120. aiq/eval/register.py +23 -0
  121. aiq/eval/remote_workflow.py +128 -0
  122. aiq/eval/runtime_event_subscriber.py +52 -0
  123. aiq/eval/swe_bench_evaluator/__init__.py +0 -0
  124. aiq/eval/swe_bench_evaluator/evaluate.py +215 -0
  125. aiq/eval/swe_bench_evaluator/register.py +36 -0
  126. aiq/eval/trajectory_evaluator/__init__.py +0 -0
  127. aiq/eval/trajectory_evaluator/evaluate.py +118 -0
  128. aiq/eval/trajectory_evaluator/register.py +40 -0
  129. aiq/eval/tunable_rag_evaluator/__init__.py +0 -0
  130. aiq/eval/tunable_rag_evaluator/evaluate.py +263 -0
  131. aiq/eval/tunable_rag_evaluator/register.py +50 -0
  132. aiq/eval/utils/__init__.py +0 -0
  133. aiq/eval/utils/output_uploader.py +131 -0
  134. aiq/eval/utils/tqdm_position_registry.py +40 -0
  135. aiq/front_ends/__init__.py +14 -0
  136. aiq/front_ends/console/__init__.py +14 -0
  137. aiq/front_ends/console/console_front_end_config.py +32 -0
  138. aiq/front_ends/console/console_front_end_plugin.py +107 -0
  139. aiq/front_ends/console/register.py +25 -0
  140. aiq/front_ends/cron/__init__.py +14 -0
  141. aiq/front_ends/fastapi/__init__.py +14 -0
  142. aiq/front_ends/fastapi/fastapi_front_end_config.py +150 -0
  143. aiq/front_ends/fastapi/fastapi_front_end_plugin.py +103 -0
  144. aiq/front_ends/fastapi/fastapi_front_end_plugin_worker.py +607 -0
  145. aiq/front_ends/fastapi/intermediate_steps_subscriber.py +80 -0
  146. aiq/front_ends/fastapi/job_store.py +161 -0
  147. aiq/front_ends/fastapi/main.py +70 -0
  148. aiq/front_ends/fastapi/message_handler.py +279 -0
  149. aiq/front_ends/fastapi/message_validator.py +345 -0
  150. aiq/front_ends/fastapi/register.py +25 -0
  151. aiq/front_ends/fastapi/response_helpers.py +195 -0
  152. aiq/front_ends/fastapi/step_adaptor.py +315 -0
  153. aiq/front_ends/fastapi/websocket.py +148 -0
  154. aiq/front_ends/mcp/__init__.py +14 -0
  155. aiq/front_ends/mcp/mcp_front_end_config.py +32 -0
  156. aiq/front_ends/mcp/mcp_front_end_plugin.py +93 -0
  157. aiq/front_ends/mcp/register.py +27 -0
  158. aiq/front_ends/mcp/tool_converter.py +242 -0
  159. aiq/front_ends/register.py +22 -0
  160. aiq/front_ends/simple_base/__init__.py +14 -0
  161. aiq/front_ends/simple_base/simple_front_end_plugin_base.py +52 -0
  162. aiq/llm/__init__.py +0 -0
  163. aiq/llm/nim_llm.py +45 -0
  164. aiq/llm/openai_llm.py +45 -0
  165. aiq/llm/register.py +22 -0
  166. aiq/llm/utils/__init__.py +14 -0
  167. aiq/llm/utils/env_config_value.py +94 -0
  168. aiq/llm/utils/error.py +17 -0
  169. aiq/memory/__init__.py +20 -0
  170. aiq/memory/interfaces.py +183 -0
  171. aiq/memory/models.py +102 -0
  172. aiq/meta/module_to_distro.json +3 -0
  173. aiq/meta/pypi.md +59 -0
  174. aiq/observability/__init__.py +0 -0
  175. aiq/observability/async_otel_listener.py +433 -0
  176. aiq/observability/register.py +99 -0
  177. aiq/plugins/.namespace +1 -0
  178. aiq/profiler/__init__.py +0 -0
  179. aiq/profiler/callbacks/__init__.py +0 -0
  180. aiq/profiler/callbacks/agno_callback_handler.py +295 -0
  181. aiq/profiler/callbacks/base_callback_class.py +20 -0
  182. aiq/profiler/callbacks/langchain_callback_handler.py +278 -0
  183. aiq/profiler/callbacks/llama_index_callback_handler.py +205 -0
  184. aiq/profiler/callbacks/semantic_kernel_callback_handler.py +238 -0
  185. aiq/profiler/callbacks/token_usage_base_model.py +27 -0
  186. aiq/profiler/data_frame_row.py +51 -0
  187. aiq/profiler/decorators/__init__.py +0 -0
  188. aiq/profiler/decorators/framework_wrapper.py +131 -0
  189. aiq/profiler/decorators/function_tracking.py +254 -0
  190. aiq/profiler/forecasting/__init__.py +0 -0
  191. aiq/profiler/forecasting/config.py +18 -0
  192. aiq/profiler/forecasting/model_trainer.py +75 -0
  193. aiq/profiler/forecasting/models/__init__.py +22 -0
  194. aiq/profiler/forecasting/models/forecasting_base_model.py +40 -0
  195. aiq/profiler/forecasting/models/linear_model.py +196 -0
  196. aiq/profiler/forecasting/models/random_forest_regressor.py +268 -0
  197. aiq/profiler/inference_metrics_model.py +25 -0
  198. aiq/profiler/inference_optimization/__init__.py +0 -0
  199. aiq/profiler/inference_optimization/bottleneck_analysis/__init__.py +0 -0
  200. aiq/profiler/inference_optimization/bottleneck_analysis/nested_stack_analysis.py +452 -0
  201. aiq/profiler/inference_optimization/bottleneck_analysis/simple_stack_analysis.py +258 -0
  202. aiq/profiler/inference_optimization/data_models.py +386 -0
  203. aiq/profiler/inference_optimization/experimental/__init__.py +0 -0
  204. aiq/profiler/inference_optimization/experimental/concurrency_spike_analysis.py +468 -0
  205. aiq/profiler/inference_optimization/experimental/prefix_span_analysis.py +405 -0
  206. aiq/profiler/inference_optimization/llm_metrics.py +212 -0
  207. aiq/profiler/inference_optimization/prompt_caching.py +163 -0
  208. aiq/profiler/inference_optimization/token_uniqueness.py +107 -0
  209. aiq/profiler/inference_optimization/workflow_runtimes.py +72 -0
  210. aiq/profiler/intermediate_property_adapter.py +102 -0
  211. aiq/profiler/profile_runner.py +433 -0
  212. aiq/profiler/utils.py +184 -0
  213. aiq/registry_handlers/__init__.py +0 -0
  214. aiq/registry_handlers/local/__init__.py +0 -0
  215. aiq/registry_handlers/local/local_handler.py +176 -0
  216. aiq/registry_handlers/local/register_local.py +37 -0
  217. aiq/registry_handlers/metadata_factory.py +60 -0
  218. aiq/registry_handlers/package_utils.py +198 -0
  219. aiq/registry_handlers/pypi/__init__.py +0 -0
  220. aiq/registry_handlers/pypi/pypi_handler.py +251 -0
  221. aiq/registry_handlers/pypi/register_pypi.py +40 -0
  222. aiq/registry_handlers/register.py +21 -0
  223. aiq/registry_handlers/registry_handler_base.py +157 -0
  224. aiq/registry_handlers/rest/__init__.py +0 -0
  225. aiq/registry_handlers/rest/register_rest.py +56 -0
  226. aiq/registry_handlers/rest/rest_handler.py +237 -0
  227. aiq/registry_handlers/schemas/__init__.py +0 -0
  228. aiq/registry_handlers/schemas/headers.py +42 -0
  229. aiq/registry_handlers/schemas/package.py +68 -0
  230. aiq/registry_handlers/schemas/publish.py +63 -0
  231. aiq/registry_handlers/schemas/pull.py +82 -0
  232. aiq/registry_handlers/schemas/remove.py +36 -0
  233. aiq/registry_handlers/schemas/search.py +91 -0
  234. aiq/registry_handlers/schemas/status.py +47 -0
  235. aiq/retriever/__init__.py +0 -0
  236. aiq/retriever/interface.py +37 -0
  237. aiq/retriever/milvus/__init__.py +14 -0
  238. aiq/retriever/milvus/register.py +81 -0
  239. aiq/retriever/milvus/retriever.py +228 -0
  240. aiq/retriever/models.py +74 -0
  241. aiq/retriever/nemo_retriever/__init__.py +14 -0
  242. aiq/retriever/nemo_retriever/register.py +60 -0
  243. aiq/retriever/nemo_retriever/retriever.py +190 -0
  244. aiq/retriever/register.py +22 -0
  245. aiq/runtime/__init__.py +14 -0
  246. aiq/runtime/loader.py +188 -0
  247. aiq/runtime/runner.py +176 -0
  248. aiq/runtime/session.py +136 -0
  249. aiq/runtime/user_metadata.py +131 -0
  250. aiq/settings/__init__.py +0 -0
  251. aiq/settings/global_settings.py +318 -0
  252. aiq/test/.namespace +1 -0
  253. aiq/tool/__init__.py +0 -0
  254. aiq/tool/code_execution/__init__.py +0 -0
  255. aiq/tool/code_execution/code_sandbox.py +188 -0
  256. aiq/tool/code_execution/local_sandbox/Dockerfile.sandbox +60 -0
  257. aiq/tool/code_execution/local_sandbox/__init__.py +13 -0
  258. aiq/tool/code_execution/local_sandbox/local_sandbox_server.py +79 -0
  259. aiq/tool/code_execution/local_sandbox/sandbox.requirements.txt +4 -0
  260. aiq/tool/code_execution/local_sandbox/start_local_sandbox.sh +25 -0
  261. aiq/tool/code_execution/register.py +70 -0
  262. aiq/tool/code_execution/utils.py +100 -0
  263. aiq/tool/datetime_tools.py +42 -0
  264. aiq/tool/document_search.py +141 -0
  265. aiq/tool/github_tools/__init__.py +0 -0
  266. aiq/tool/github_tools/create_github_commit.py +133 -0
  267. aiq/tool/github_tools/create_github_issue.py +87 -0
  268. aiq/tool/github_tools/create_github_pr.py +106 -0
  269. aiq/tool/github_tools/get_github_file.py +106 -0
  270. aiq/tool/github_tools/get_github_issue.py +166 -0
  271. aiq/tool/github_tools/get_github_pr.py +256 -0
  272. aiq/tool/github_tools/update_github_issue.py +100 -0
  273. aiq/tool/mcp/__init__.py +14 -0
  274. aiq/tool/mcp/mcp_client.py +220 -0
  275. aiq/tool/mcp/mcp_tool.py +76 -0
  276. aiq/tool/memory_tools/__init__.py +0 -0
  277. aiq/tool/memory_tools/add_memory_tool.py +67 -0
  278. aiq/tool/memory_tools/delete_memory_tool.py +67 -0
  279. aiq/tool/memory_tools/get_memory_tool.py +72 -0
  280. aiq/tool/nvidia_rag.py +95 -0
  281. aiq/tool/register.py +36 -0
  282. aiq/tool/retriever.py +89 -0
  283. aiq/utils/__init__.py +0 -0
  284. aiq/utils/data_models/__init__.py +0 -0
  285. aiq/utils/data_models/schema_validator.py +58 -0
  286. aiq/utils/debugging_utils.py +43 -0
  287. aiq/utils/exception_handlers/__init__.py +0 -0
  288. aiq/utils/exception_handlers/schemas.py +114 -0
  289. aiq/utils/io/__init__.py +0 -0
  290. aiq/utils/io/yaml_tools.py +119 -0
  291. aiq/utils/metadata_utils.py +74 -0
  292. aiq/utils/optional_imports.py +142 -0
  293. aiq/utils/producer_consumer_queue.py +178 -0
  294. aiq/utils/reactive/__init__.py +0 -0
  295. aiq/utils/reactive/base/__init__.py +0 -0
  296. aiq/utils/reactive/base/observable_base.py +65 -0
  297. aiq/utils/reactive/base/observer_base.py +55 -0
  298. aiq/utils/reactive/base/subject_base.py +79 -0
  299. aiq/utils/reactive/observable.py +59 -0
  300. aiq/utils/reactive/observer.py +76 -0
  301. aiq/utils/reactive/subject.py +131 -0
  302. aiq/utils/reactive/subscription.py +49 -0
  303. aiq/utils/settings/__init__.py +0 -0
  304. aiq/utils/settings/global_settings.py +197 -0
  305. aiq/utils/type_converter.py +232 -0
  306. aiq/utils/type_utils.py +397 -0
  307. aiq/utils/url_utils.py +27 -0
  308. aiqtoolkit-1.1.0a20250503.dist-info/METADATA +330 -0
  309. aiqtoolkit-1.1.0a20250503.dist-info/RECORD +314 -0
  310. aiqtoolkit-1.1.0a20250503.dist-info/WHEEL +5 -0
  311. aiqtoolkit-1.1.0a20250503.dist-info/entry_points.txt +17 -0
  312. aiqtoolkit-1.1.0a20250503.dist-info/licenses/LICENSE-3rd-party.txt +3686 -0
  313. aiqtoolkit-1.1.0a20250503.dist-info/licenses/LICENSE.md +201 -0
  314. aiqtoolkit-1.1.0a20250503.dist-info/top_level.txt +1 -0
@@ -0,0 +1,607 @@
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 typing
20
+ from abc import ABC
21
+ from abc import abstractmethod
22
+ from contextlib import asynccontextmanager
23
+ from functools import partial
24
+ from pathlib import Path
25
+
26
+ from fastapi import BackgroundTasks
27
+ from fastapi import Body
28
+ from fastapi import FastAPI
29
+ from fastapi import Request
30
+ from fastapi import Response
31
+ from fastapi.exceptions import HTTPException
32
+ from fastapi.middleware.cors import CORSMiddleware
33
+ from fastapi.responses import StreamingResponse
34
+ from pydantic import BaseModel
35
+
36
+ from aiq.builder.workflow_builder import WorkflowBuilder
37
+ from aiq.data_models.api_server import AIQChatRequest
38
+ from aiq.data_models.api_server import AIQChatResponse
39
+ from aiq.data_models.api_server import AIQChatResponseChunk
40
+ from aiq.data_models.api_server import AIQResponseIntermediateStep
41
+ from aiq.data_models.config import AIQConfig
42
+ from aiq.eval.config import EvaluationRunOutput
43
+ from aiq.eval.evaluate import EvaluationRun
44
+ from aiq.eval.evaluate import EvaluationRunConfig
45
+ from aiq.front_ends.fastapi.fastapi_front_end_config import AIQEvaluateRequest
46
+ from aiq.front_ends.fastapi.fastapi_front_end_config import AIQEvaluateResponse
47
+ from aiq.front_ends.fastapi.fastapi_front_end_config import AIQEvaluateStatusResponse
48
+ from aiq.front_ends.fastapi.fastapi_front_end_config import FastApiFrontEndConfig
49
+ from aiq.front_ends.fastapi.job_store import JobInfo
50
+ from aiq.front_ends.fastapi.job_store import JobStore
51
+ from aiq.front_ends.fastapi.response_helpers import generate_single_response
52
+ from aiq.front_ends.fastapi.response_helpers import generate_streaming_response_as_str
53
+ from aiq.front_ends.fastapi.response_helpers import generate_streaming_response_full_as_str
54
+ from aiq.front_ends.fastapi.step_adaptor import StepAdaptor
55
+ from aiq.front_ends.fastapi.websocket import AIQWebSocket
56
+ from aiq.runtime.session import AIQSessionManager
57
+
58
+ logger = logging.getLogger(__name__)
59
+
60
+
61
+ class FastApiFrontEndPluginWorkerBase(ABC):
62
+
63
+ def __init__(self, config: AIQConfig):
64
+ self._config = config
65
+
66
+ assert isinstance(config.general.front_end,
67
+ FastApiFrontEndConfig), ("Front end config is not FastApiFrontEndConfig")
68
+
69
+ self._front_end_config = config.general.front_end
70
+
71
+ @property
72
+ def config(self) -> AIQConfig:
73
+ return self._config
74
+
75
+ @property
76
+ def front_end_config(self) -> FastApiFrontEndConfig:
77
+
78
+ return self._front_end_config
79
+
80
+ def build_app(self) -> FastAPI:
81
+
82
+ # Create the FastAPI app and configure it
83
+ @asynccontextmanager
84
+ async def lifespan(starting_app: FastAPI):
85
+
86
+ logger.debug("Starting AIQ Toolkit server from process %s", os.getpid())
87
+
88
+ async with WorkflowBuilder.from_config(self.config) as builder:
89
+
90
+ await self.configure(starting_app, builder)
91
+
92
+ yield
93
+
94
+ # If a cleanup task is running, cancel it
95
+ cleanup_task = getattr(starting_app.state, "cleanup_task", None)
96
+ if cleanup_task:
97
+ logger.info("Cancelling cleanup task")
98
+ cleanup_task.cancel()
99
+
100
+ logger.debug("Closing AIQ Toolkit server from process %s", os.getpid())
101
+
102
+ aiq_app = FastAPI(lifespan=lifespan)
103
+
104
+ self.set_cors_config(aiq_app)
105
+
106
+ return aiq_app
107
+
108
+ def set_cors_config(self, aiq_app: FastAPI) -> None:
109
+ """
110
+ Set the cross origin resource sharing configuration.
111
+ """
112
+ cors_kwargs = {}
113
+
114
+ if self.front_end_config.cors.allow_origins is not None:
115
+ cors_kwargs["allow_origins"] = self.front_end_config.cors.allow_origins
116
+
117
+ if self.front_end_config.cors.allow_origin_regex is not None:
118
+ cors_kwargs["allow_origin_regex"] = self.front_end_config.cors.allow_origin_regex
119
+
120
+ if self.front_end_config.cors.allow_methods is not None:
121
+ cors_kwargs["allow_methods"] = self.front_end_config.cors.allow_methods
122
+
123
+ if self.front_end_config.cors.allow_headers is not None:
124
+ cors_kwargs["allow_headers"] = self.front_end_config.cors.allow_headers
125
+
126
+ if self.front_end_config.cors.allow_credentials is not None:
127
+ cors_kwargs["allow_credentials"] = self.front_end_config.cors.allow_credentials
128
+
129
+ if self.front_end_config.cors.expose_headers is not None:
130
+ cors_kwargs["expose_headers"] = self.front_end_config.cors.expose_headers
131
+
132
+ if self.front_end_config.cors.max_age is not None:
133
+ cors_kwargs["max_age"] = self.front_end_config.cors.max_age
134
+
135
+ aiq_app.add_middleware(
136
+ CORSMiddleware,
137
+ **cors_kwargs,
138
+ )
139
+
140
+ @abstractmethod
141
+ async def configure(self, app: FastAPI, builder: WorkflowBuilder):
142
+ pass
143
+
144
+ @abstractmethod
145
+ def get_step_adaptor(self) -> StepAdaptor:
146
+ pass
147
+
148
+
149
+ class RouteInfo(BaseModel):
150
+
151
+ function_name: str | None
152
+
153
+
154
+ class FastApiFrontEndPluginWorker(FastApiFrontEndPluginWorkerBase):
155
+
156
+ def get_step_adaptor(self) -> StepAdaptor:
157
+
158
+ return StepAdaptor(self.front_end_config.step_adaptor)
159
+
160
+ async def configure(self, app: FastAPI, builder: WorkflowBuilder):
161
+
162
+ # Do things like setting the base URL and global configuration options
163
+ app.root_path = self.front_end_config.root_path
164
+
165
+ await self.add_routes(app, builder)
166
+
167
+ async def add_routes(self, app: FastAPI, builder: WorkflowBuilder):
168
+
169
+ await self.add_default_route(app, AIQSessionManager(builder.build()))
170
+ await self.add_evaluate_route(app, AIQSessionManager(builder.build()))
171
+
172
+ for ep in self.front_end_config.endpoints:
173
+
174
+ entry_workflow = builder.build(entry_function=ep.function_name)
175
+
176
+ await self.add_route(app, endpoint=ep, session_manager=AIQSessionManager(entry_workflow))
177
+
178
+ async def add_default_route(self, app: FastAPI, session_manager: AIQSessionManager):
179
+
180
+ await self.add_route(app, self.front_end_config.workflow, session_manager)
181
+
182
+ async def add_evaluate_route(self, app: FastAPI, session_manager: AIQSessionManager):
183
+ """Add the evaluate endpoint to the FastAPI app."""
184
+
185
+ response_500 = {
186
+ "description": "Internal Server Error",
187
+ "content": {
188
+ "application/json": {
189
+ "example": {
190
+ "detail": "Internal server error occurred"
191
+ }
192
+ }
193
+ },
194
+ }
195
+
196
+ # Create job store for tracking evaluation jobs
197
+ job_store = JobStore()
198
+ # Don't run multiple evaluations at the same time
199
+ evaluation_lock = asyncio.Lock()
200
+
201
+ async def periodic_cleanup(job_store: JobStore):
202
+ while True:
203
+ try:
204
+ job_store.cleanup_expired_jobs()
205
+ logger.debug("Expired jobs cleaned up")
206
+ except Exception as e:
207
+ logger.error("Error during job cleanup: %s", str(e))
208
+ await asyncio.sleep(300) # every 5 minutes
209
+
210
+ def create_cleanup_task():
211
+ # Schedule periodic cleanup of expired jobs on first job creation
212
+ if not hasattr(app.state, "cleanup_task"):
213
+ logger.info("Starting periodic cleanup task")
214
+ app.state.cleanup_task = asyncio.create_task(periodic_cleanup(job_store))
215
+
216
+ async def run_evaluation(job_id: str, config_file: str, reps: int, session_manager: AIQSessionManager):
217
+ """Background task to run the evaluation."""
218
+ async with evaluation_lock:
219
+ try:
220
+ # Create EvaluationRunConfig using the CLI defaults
221
+ eval_config = EvaluationRunConfig(config_file=Path(config_file), dataset=None, reps=reps)
222
+
223
+ # Create a new EvaluationRun with the evaluation-specific config
224
+ job_store.update_status(job_id, "running")
225
+ eval_runner = EvaluationRun(eval_config)
226
+ output: EvaluationRunOutput = await eval_runner.run_and_evaluate(session_manager=session_manager,
227
+ job_id=job_id)
228
+ if output.workflow_interrupted:
229
+ job_store.update_status(job_id, "interrupted")
230
+ else:
231
+ parent_dir = os.path.dirname(
232
+ output.workflow_output_file) if output.workflow_output_file else None
233
+
234
+ job_store.update_status(job_id, "success", output_path=str(parent_dir))
235
+ except Exception as e:
236
+ logger.error("Error in evaluation job %s: %s", job_id, str(e))
237
+ job_store.update_status(job_id, "failure", error=str(e))
238
+
239
+ async def start_evaluation(request: AIQEvaluateRequest,
240
+ background_tasks: BackgroundTasks,
241
+ http_request: Request):
242
+ """Handle evaluation requests."""
243
+
244
+ async with session_manager.session(request=http_request):
245
+
246
+ # if job_id is present and already exists return the job info
247
+ if request.job_id:
248
+ job = job_store.get_job(request.job_id)
249
+ if job:
250
+ return AIQEvaluateResponse(job_id=job.job_id, status=job.status)
251
+
252
+ job_id = job_store.create_job(request.config_file, request.job_id, request.expiry_seconds)
253
+ create_cleanup_task()
254
+ background_tasks.add_task(run_evaluation, job_id, request.config_file, request.reps, session_manager)
255
+
256
+ return AIQEvaluateResponse(job_id=job_id, status="submitted")
257
+
258
+ def translate_job_to_response(job: JobInfo) -> AIQEvaluateStatusResponse:
259
+ """Translate a JobInfo object to an AIQEvaluateStatusResponse."""
260
+ return AIQEvaluateStatusResponse(job_id=job.job_id,
261
+ status=job.status,
262
+ config_file=str(job.config_file),
263
+ error=job.error,
264
+ output_path=str(job.output_path),
265
+ created_at=job.created_at,
266
+ updated_at=job.updated_at,
267
+ expires_at=job_store.get_expires_at(job))
268
+
269
+ async def get_job_status(job_id: str, http_request: Request) -> AIQEvaluateStatusResponse:
270
+ """Get the status of an evaluation job."""
271
+ logger.info("Getting status for job %s", job_id)
272
+
273
+ async with session_manager.session(request=http_request):
274
+
275
+ job = job_store.get_job(job_id)
276
+ if not job:
277
+ logger.warning("Job %s not found", job_id)
278
+ raise HTTPException(status_code=404, detail=f"Job {job_id} not found")
279
+ logger.info(f"Found job {job_id} with status {job.status}")
280
+ return translate_job_to_response(job)
281
+
282
+ async def get_last_job_status(http_request: Request) -> AIQEvaluateStatusResponse:
283
+ """Get the status of the last created evaluation job."""
284
+ logger.info("Getting last job status")
285
+
286
+ async with session_manager.session(request=http_request):
287
+
288
+ job = job_store.get_last_job()
289
+ if not job:
290
+ logger.warning("No jobs found when requesting last job status")
291
+ raise HTTPException(status_code=404, detail="No jobs found")
292
+ logger.info("Found last job %s with status %s", job.job_id, job.status)
293
+ return translate_job_to_response(job)
294
+
295
+ async def get_jobs(http_request: Request, status: str | None = None) -> list[AIQEvaluateStatusResponse]:
296
+ """Get all jobs, optionally filtered by status."""
297
+
298
+ async with session_manager.session(request=http_request):
299
+
300
+ if status is None:
301
+ logger.info("Getting all jobs")
302
+ jobs = job_store.get_all_jobs()
303
+ else:
304
+ logger.info("Getting jobs with status %s", status)
305
+ jobs = job_store.get_jobs_by_status(status)
306
+ logger.info("Found %d jobs", len(jobs))
307
+ return [translate_job_to_response(job) for job in jobs]
308
+
309
+ if self.front_end_config.evaluate.path:
310
+ # Add last job endpoint first (most specific)
311
+ app.add_api_route(
312
+ path=f"{self.front_end_config.evaluate.path}/job/last",
313
+ endpoint=get_last_job_status,
314
+ methods=["GET"],
315
+ response_model=AIQEvaluateStatusResponse,
316
+ description="Get the status of the last created evaluation job",
317
+ responses={
318
+ 404: {
319
+ "description": "No jobs found"
320
+ }, 500: response_500
321
+ },
322
+ )
323
+
324
+ # Add specific job endpoint (least specific)
325
+ app.add_api_route(
326
+ path=f"{self.front_end_config.evaluate.path}/job/{{job_id}}",
327
+ endpoint=get_job_status,
328
+ methods=["GET"],
329
+ response_model=AIQEvaluateStatusResponse,
330
+ description="Get the status of an evaluation job",
331
+ responses={
332
+ 404: {
333
+ "description": "Job not found"
334
+ }, 500: response_500
335
+ },
336
+ )
337
+
338
+ # Add jobs endpoint with optional status query parameter
339
+ app.add_api_route(
340
+ path=f"{self.front_end_config.evaluate.path}/jobs",
341
+ endpoint=get_jobs,
342
+ methods=["GET"],
343
+ response_model=list[AIQEvaluateStatusResponse],
344
+ description="Get all jobs, optionally filtered by status",
345
+ responses={500: response_500},
346
+ )
347
+
348
+ # Add HTTP endpoint for evaluation
349
+ app.add_api_route(
350
+ path=self.front_end_config.evaluate.path,
351
+ endpoint=start_evaluation,
352
+ methods=[self.front_end_config.evaluate.method],
353
+ response_model=AIQEvaluateResponse,
354
+ description=self.front_end_config.evaluate.description,
355
+ responses={500: response_500},
356
+ )
357
+
358
+ async def add_route(self,
359
+ app: FastAPI,
360
+ endpoint: FastApiFrontEndConfig.EndpointBase,
361
+ session_manager: AIQSessionManager):
362
+
363
+ workflow = session_manager.workflow
364
+
365
+ if (endpoint.websocket_path):
366
+ app.add_websocket_route(endpoint.websocket_path,
367
+ partial(AIQWebSocket, session_manager, self.get_step_adaptor()))
368
+
369
+ GenerateBodyType = workflow.input_schema # pylint: disable=invalid-name
370
+ GenerateStreamResponseType = workflow.streaming_output_schema # pylint: disable=invalid-name
371
+ GenerateSingleResponseType = workflow.single_output_schema # pylint: disable=invalid-name
372
+
373
+ # Ensure that the input is in the body. POD types are treated as query parameters
374
+ if (not issubclass(GenerateBodyType, BaseModel)):
375
+ GenerateBodyType = typing.Annotated[GenerateBodyType, Body()]
376
+
377
+ response_500 = {
378
+ "description": "Internal Server Error",
379
+ "content": {
380
+ "application/json": {
381
+ "example": {
382
+ "detail": "Internal server error occurred"
383
+ }
384
+ }
385
+ },
386
+ }
387
+
388
+ def get_single_endpoint(result_type: type | None):
389
+
390
+ async def get_single(response: Response, request: Request):
391
+
392
+ response.headers["Content-Type"] = "application/json"
393
+
394
+ async with session_manager.session(request=request):
395
+
396
+ return await generate_single_response(None, session_manager, result_type=result_type)
397
+
398
+ return get_single
399
+
400
+ def get_streaming_endpoint(streaming: bool, result_type: type | None, output_type: type | None):
401
+
402
+ async def get_stream(request: Request):
403
+
404
+ async with session_manager.session(request=request):
405
+
406
+ return StreamingResponse(headers={"Content-Type": "text/event-stream; charset=utf-8"},
407
+ content=generate_streaming_response_as_str(
408
+ None,
409
+ session_manager=session_manager,
410
+ streaming=streaming,
411
+ step_adaptor=self.get_step_adaptor(),
412
+ result_type=result_type,
413
+ output_type=output_type))
414
+
415
+ return get_stream
416
+
417
+ def get_streaming_raw_endpoint(streaming: bool, result_type: type | None, output_type: type | None):
418
+
419
+ async def get_stream(filter_steps: str | None = None):
420
+
421
+ return StreamingResponse(headers={"Content-Type": "text/event-stream; charset=utf-8"},
422
+ content=generate_streaming_response_full_as_str(
423
+ None,
424
+ session_manager=session_manager,
425
+ streaming=streaming,
426
+ result_type=result_type,
427
+ output_type=output_type,
428
+ filter_steps=filter_steps))
429
+
430
+ return get_stream
431
+
432
+ def post_single_endpoint(request_type: type, result_type: type | None):
433
+
434
+ async def post_single(response: Response, request: Request, payload: request_type):
435
+
436
+ response.headers["Content-Type"] = "application/json"
437
+
438
+ async with session_manager.session(request=request):
439
+
440
+ return await generate_single_response(payload, session_manager, result_type=result_type)
441
+
442
+ return post_single
443
+
444
+ def post_streaming_endpoint(request_type: type,
445
+ streaming: bool,
446
+ result_type: type | None,
447
+ output_type: type | None):
448
+
449
+ async def post_stream(request: Request, payload: request_type):
450
+
451
+ async with session_manager.session(request=request):
452
+
453
+ return StreamingResponse(headers={"Content-Type": "text/event-stream; charset=utf-8"},
454
+ content=generate_streaming_response_as_str(
455
+ payload,
456
+ session_manager=session_manager,
457
+ streaming=streaming,
458
+ step_adaptor=self.get_step_adaptor(),
459
+ result_type=result_type,
460
+ output_type=output_type))
461
+
462
+ return post_stream
463
+
464
+ def post_streaming_raw_endpoint(request_type: type,
465
+ streaming: bool,
466
+ result_type: type | None,
467
+ output_type: type | None):
468
+ """
469
+ Stream raw intermediate steps without any step adaptor translations.
470
+ """
471
+
472
+ async def post_stream(payload: request_type, filter_steps: str | None = None):
473
+
474
+ return StreamingResponse(headers={"Content-Type": "text/event-stream; charset=utf-8"},
475
+ content=generate_streaming_response_full_as_str(
476
+ payload,
477
+ session_manager=session_manager,
478
+ streaming=streaming,
479
+ result_type=result_type,
480
+ output_type=output_type,
481
+ filter_steps=filter_steps))
482
+
483
+ return post_stream
484
+
485
+ if (endpoint.path):
486
+ if (endpoint.method == "GET"):
487
+
488
+ app.add_api_route(
489
+ path=endpoint.path,
490
+ endpoint=get_single_endpoint(result_type=GenerateSingleResponseType),
491
+ methods=[endpoint.method],
492
+ response_model=GenerateSingleResponseType,
493
+ description=endpoint.description,
494
+ responses={500: response_500},
495
+ )
496
+
497
+ app.add_api_route(
498
+ path=f"{endpoint.path}/stream",
499
+ endpoint=get_streaming_endpoint(streaming=True,
500
+ result_type=GenerateStreamResponseType,
501
+ output_type=GenerateStreamResponseType),
502
+ methods=[endpoint.method],
503
+ response_model=GenerateStreamResponseType,
504
+ description=endpoint.description,
505
+ responses={500: response_500},
506
+ )
507
+
508
+ app.add_api_route(
509
+ path=f"{endpoint.path}/full",
510
+ endpoint=get_streaming_raw_endpoint(streaming=True,
511
+ result_type=GenerateStreamResponseType,
512
+ output_type=GenerateStreamResponseType),
513
+ methods=[endpoint.method],
514
+ description="Stream raw intermediate steps without any step adaptor translations.\n"
515
+ "Use filter_steps query parameter to filter steps by type (comma-separated list) or\
516
+ set to 'none' to suppress all intermediate steps.",
517
+ )
518
+
519
+ elif (endpoint.method == "POST"):
520
+
521
+ app.add_api_route(
522
+ path=endpoint.path,
523
+ endpoint=post_single_endpoint(request_type=GenerateBodyType,
524
+ result_type=GenerateSingleResponseType),
525
+ methods=[endpoint.method],
526
+ response_model=GenerateSingleResponseType,
527
+ description=endpoint.description,
528
+ responses={500: response_500},
529
+ )
530
+
531
+ app.add_api_route(
532
+ path=f"{endpoint.path}/stream",
533
+ endpoint=post_streaming_endpoint(request_type=GenerateBodyType,
534
+ streaming=True,
535
+ result_type=GenerateStreamResponseType,
536
+ output_type=GenerateStreamResponseType),
537
+ methods=[endpoint.method],
538
+ response_model=GenerateStreamResponseType,
539
+ description=endpoint.description,
540
+ responses={500: response_500},
541
+ )
542
+
543
+ app.add_api_route(
544
+ path=f"{endpoint.path}/full",
545
+ endpoint=post_streaming_raw_endpoint(request_type=GenerateBodyType,
546
+ streaming=True,
547
+ result_type=GenerateStreamResponseType,
548
+ output_type=GenerateStreamResponseType),
549
+ methods=[endpoint.method],
550
+ response_model=GenerateStreamResponseType,
551
+ description="Stream raw intermediate steps without any step adaptor translations.\n"
552
+ "Use filter_steps query parameter to filter steps by type (comma-separated list) or \
553
+ set to 'none' to suppress all intermediate steps.",
554
+ responses={500: response_500},
555
+ )
556
+
557
+ else:
558
+ raise ValueError(f"Unsupported method {endpoint.method}")
559
+
560
+ if (endpoint.openai_api_path):
561
+ if (endpoint.method == "GET"):
562
+
563
+ app.add_api_route(
564
+ path=endpoint.openai_api_path,
565
+ endpoint=get_single_endpoint(result_type=AIQChatResponse),
566
+ methods=[endpoint.method],
567
+ response_model=AIQChatResponse,
568
+ description=endpoint.description,
569
+ responses={500: response_500},
570
+ )
571
+
572
+ app.add_api_route(
573
+ path=f"{endpoint.openai_api_path}/stream",
574
+ endpoint=get_streaming_endpoint(streaming=True,
575
+ result_type=AIQChatResponseChunk,
576
+ output_type=AIQChatResponseChunk),
577
+ methods=[endpoint.method],
578
+ response_model=AIQChatResponseChunk,
579
+ description=endpoint.description,
580
+ responses={500: response_500},
581
+ )
582
+
583
+ elif (endpoint.method == "POST"):
584
+
585
+ app.add_api_route(
586
+ path=endpoint.openai_api_path,
587
+ endpoint=post_single_endpoint(request_type=AIQChatRequest, result_type=AIQChatResponse),
588
+ methods=[endpoint.method],
589
+ response_model=AIQChatResponse,
590
+ description=endpoint.description,
591
+ responses={500: response_500},
592
+ )
593
+
594
+ app.add_api_route(
595
+ path=f"{endpoint.openai_api_path}/stream",
596
+ endpoint=post_streaming_endpoint(request_type=AIQChatRequest,
597
+ streaming=True,
598
+ result_type=AIQChatResponseChunk,
599
+ output_type=AIQChatResponseChunk),
600
+ methods=[endpoint.method],
601
+ response_model=AIQChatResponseChunk | AIQResponseIntermediateStep,
602
+ description=endpoint.description,
603
+ responses={500: response_500},
604
+ )
605
+
606
+ else:
607
+ raise ValueError(f"Unsupported method {endpoint.method}")
@@ -0,0 +1,80 @@
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
+
19
+ from aiq.builder.context import AIQContext
20
+ from aiq.data_models.api_server import AIQResponseIntermediateStep
21
+ from aiq.data_models.intermediate_step import IntermediateStep
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ async def pull_intermediate(_q, adapter):
27
+ """
28
+ Subscribes to the runner's event stream (which is now a simplified Observable)
29
+ using direct callbacks. Processes each event with the adapter and enqueues
30
+ results to `_q`.
31
+ """
32
+ intermediate_done = asyncio.Event()
33
+ context = AIQContext.get()
34
+ loop = asyncio.get_running_loop()
35
+
36
+ async def set_intermediate_done():
37
+ intermediate_done.set()
38
+
39
+ def on_next_cb(item: IntermediateStep):
40
+ """
41
+ Synchronously called whenever the runner publishes an event.
42
+ We process it, then place it into the async queue (via a small async task).
43
+ If adapter is None, convert the raw IntermediateStep into the complete
44
+ AIQResponseIntermediateStep and place it into the queue.
45
+ """
46
+ if adapter is None:
47
+ adapted = AIQResponseIntermediateStep(id=item.UUID,
48
+ type=item.event_type,
49
+ name=item.name or "",
50
+ parent_id=item.parent_id,
51
+ payload=item.payload.model_dump_json())
52
+ else:
53
+ adapted = adapter.process(item)
54
+
55
+ if adapted is not None:
56
+ loop.create_task(_q.put(adapted))
57
+
58
+ def on_error_cb(exc: Exception):
59
+ """
60
+ Called if the runner signals an error. We log it and unblock our wait.
61
+ """
62
+ logger.error("Hit on_error: %s", exc)
63
+
64
+ loop.create_task(set_intermediate_done())
65
+
66
+ def on_complete_cb():
67
+ """
68
+ Called once the runner signals no more items. We unblock our wait.
69
+ """
70
+ logger.debug("Completed reading intermediate steps")
71
+
72
+ loop.create_task(set_intermediate_done())
73
+
74
+ # Subscribe to the runner's "reactive_event_stream" (now a simple Observable)
75
+ _ = context.intermediate_step_manager.subscribe(on_next=on_next_cb,
76
+ on_error=on_error_cb,
77
+ on_complete=on_complete_cb)
78
+
79
+ # Wait until on_complete or on_error sets intermediate_done
80
+ return intermediate_done