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