aiqtoolkit 1.2.0a20250707__py3-none-any.whl → 1.2.0rc1__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 (197) hide show
  1. aiq/agent/base.py +170 -8
  2. aiq/agent/dual_node.py +1 -1
  3. aiq/agent/react_agent/agent.py +112 -111
  4. aiq/agent/react_agent/register.py +31 -14
  5. aiq/agent/rewoo_agent/agent.py +36 -35
  6. aiq/agent/rewoo_agent/register.py +2 -2
  7. aiq/agent/tool_calling_agent/agent.py +3 -7
  8. aiq/authentication/__init__.py +14 -0
  9. aiq/authentication/api_key/__init__.py +14 -0
  10. aiq/authentication/api_key/api_key_auth_provider.py +92 -0
  11. aiq/authentication/api_key/api_key_auth_provider_config.py +124 -0
  12. aiq/authentication/api_key/register.py +26 -0
  13. aiq/authentication/exceptions/__init__.py +14 -0
  14. aiq/authentication/exceptions/api_key_exceptions.py +38 -0
  15. aiq/authentication/exceptions/auth_code_grant_exceptions.py +86 -0
  16. aiq/authentication/exceptions/call_back_exceptions.py +38 -0
  17. aiq/authentication/exceptions/request_exceptions.py +54 -0
  18. aiq/authentication/http_basic_auth/__init__.py +0 -0
  19. aiq/authentication/http_basic_auth/http_basic_auth_provider.py +81 -0
  20. aiq/authentication/http_basic_auth/register.py +30 -0
  21. aiq/authentication/interfaces.py +93 -0
  22. aiq/authentication/oauth2/__init__.py +14 -0
  23. aiq/authentication/oauth2/oauth2_auth_code_flow_provider.py +107 -0
  24. aiq/authentication/oauth2/oauth2_auth_code_flow_provider_config.py +39 -0
  25. aiq/authentication/oauth2/register.py +25 -0
  26. aiq/authentication/register.py +21 -0
  27. aiq/builder/builder.py +64 -2
  28. aiq/builder/component_utils.py +16 -3
  29. aiq/builder/context.py +26 -0
  30. aiq/builder/eval_builder.py +43 -2
  31. aiq/builder/function.py +32 -4
  32. aiq/builder/function_base.py +1 -1
  33. aiq/builder/intermediate_step_manager.py +6 -8
  34. aiq/builder/user_interaction_manager.py +3 -0
  35. aiq/builder/workflow.py +23 -18
  36. aiq/builder/workflow_builder.py +420 -73
  37. aiq/cli/commands/info/list_mcp.py +103 -16
  38. aiq/cli/commands/sizing/__init__.py +14 -0
  39. aiq/cli/commands/sizing/calc.py +294 -0
  40. aiq/cli/commands/sizing/sizing.py +27 -0
  41. aiq/cli/commands/start.py +1 -0
  42. aiq/cli/entrypoint.py +2 -0
  43. aiq/cli/register_workflow.py +80 -0
  44. aiq/cli/type_registry.py +151 -30
  45. aiq/data_models/api_server.py +123 -11
  46. aiq/data_models/authentication.py +231 -0
  47. aiq/data_models/common.py +35 -7
  48. aiq/data_models/component.py +17 -9
  49. aiq/data_models/component_ref.py +33 -0
  50. aiq/data_models/config.py +60 -3
  51. aiq/data_models/embedder.py +1 -0
  52. aiq/data_models/function_dependencies.py +8 -0
  53. aiq/data_models/interactive.py +10 -1
  54. aiq/data_models/intermediate_step.py +15 -5
  55. aiq/data_models/its_strategy.py +30 -0
  56. aiq/data_models/llm.py +1 -0
  57. aiq/data_models/memory.py +1 -0
  58. aiq/data_models/object_store.py +44 -0
  59. aiq/data_models/retry_mixin.py +35 -0
  60. aiq/data_models/span.py +187 -0
  61. aiq/data_models/telemetry_exporter.py +2 -2
  62. aiq/embedder/nim_embedder.py +2 -1
  63. aiq/embedder/openai_embedder.py +2 -1
  64. aiq/eval/config.py +19 -1
  65. aiq/eval/dataset_handler/dataset_handler.py +75 -1
  66. aiq/eval/evaluate.py +53 -10
  67. aiq/eval/rag_evaluator/evaluate.py +23 -12
  68. aiq/eval/remote_workflow.py +7 -2
  69. aiq/eval/runners/__init__.py +14 -0
  70. aiq/eval/runners/config.py +39 -0
  71. aiq/eval/runners/multi_eval_runner.py +54 -0
  72. aiq/eval/usage_stats.py +6 -0
  73. aiq/eval/utils/weave_eval.py +5 -1
  74. aiq/experimental/__init__.py +0 -0
  75. aiq/experimental/decorators/__init__.py +0 -0
  76. aiq/experimental/decorators/experimental_warning_decorator.py +130 -0
  77. aiq/experimental/inference_time_scaling/__init__.py +0 -0
  78. aiq/experimental/inference_time_scaling/editing/__init__.py +0 -0
  79. aiq/experimental/inference_time_scaling/editing/iterative_plan_refinement_editor.py +147 -0
  80. aiq/experimental/inference_time_scaling/editing/llm_as_a_judge_editor.py +204 -0
  81. aiq/experimental/inference_time_scaling/editing/motivation_aware_summarization.py +107 -0
  82. aiq/experimental/inference_time_scaling/functions/__init__.py +0 -0
  83. aiq/experimental/inference_time_scaling/functions/execute_score_select_function.py +105 -0
  84. aiq/experimental/inference_time_scaling/functions/its_tool_orchestration_function.py +205 -0
  85. aiq/experimental/inference_time_scaling/functions/its_tool_wrapper_function.py +146 -0
  86. aiq/experimental/inference_time_scaling/functions/plan_select_execute_function.py +224 -0
  87. aiq/experimental/inference_time_scaling/models/__init__.py +0 -0
  88. aiq/experimental/inference_time_scaling/models/editor_config.py +132 -0
  89. aiq/experimental/inference_time_scaling/models/its_item.py +48 -0
  90. aiq/experimental/inference_time_scaling/models/scoring_config.py +112 -0
  91. aiq/experimental/inference_time_scaling/models/search_config.py +120 -0
  92. aiq/experimental/inference_time_scaling/models/selection_config.py +154 -0
  93. aiq/experimental/inference_time_scaling/models/stage_enums.py +43 -0
  94. aiq/experimental/inference_time_scaling/models/strategy_base.py +66 -0
  95. aiq/experimental/inference_time_scaling/models/tool_use_config.py +41 -0
  96. aiq/experimental/inference_time_scaling/register.py +36 -0
  97. aiq/experimental/inference_time_scaling/scoring/__init__.py +0 -0
  98. aiq/experimental/inference_time_scaling/scoring/llm_based_agent_scorer.py +168 -0
  99. aiq/experimental/inference_time_scaling/scoring/llm_based_plan_scorer.py +168 -0
  100. aiq/experimental/inference_time_scaling/scoring/motivation_aware_scorer.py +111 -0
  101. aiq/experimental/inference_time_scaling/search/__init__.py +0 -0
  102. aiq/experimental/inference_time_scaling/search/multi_llm_planner.py +128 -0
  103. aiq/experimental/inference_time_scaling/search/multi_query_retrieval_search.py +122 -0
  104. aiq/experimental/inference_time_scaling/search/single_shot_multi_plan_planner.py +128 -0
  105. aiq/experimental/inference_time_scaling/selection/__init__.py +0 -0
  106. aiq/experimental/inference_time_scaling/selection/best_of_n_selector.py +63 -0
  107. aiq/experimental/inference_time_scaling/selection/llm_based_agent_output_selector.py +131 -0
  108. aiq/experimental/inference_time_scaling/selection/llm_based_output_merging_selector.py +159 -0
  109. aiq/experimental/inference_time_scaling/selection/llm_based_plan_selector.py +128 -0
  110. aiq/experimental/inference_time_scaling/selection/threshold_selector.py +58 -0
  111. aiq/front_ends/console/authentication_flow_handler.py +233 -0
  112. aiq/front_ends/console/console_front_end_plugin.py +11 -2
  113. aiq/front_ends/fastapi/auth_flow_handlers/__init__.py +0 -0
  114. aiq/front_ends/fastapi/auth_flow_handlers/http_flow_handler.py +27 -0
  115. aiq/front_ends/fastapi/auth_flow_handlers/websocket_flow_handler.py +107 -0
  116. aiq/front_ends/fastapi/fastapi_front_end_config.py +20 -0
  117. aiq/front_ends/fastapi/fastapi_front_end_controller.py +68 -0
  118. aiq/front_ends/fastapi/fastapi_front_end_plugin.py +14 -1
  119. aiq/front_ends/fastapi/fastapi_front_end_plugin_worker.py +353 -31
  120. aiq/front_ends/fastapi/html_snippets/__init__.py +14 -0
  121. aiq/front_ends/fastapi/html_snippets/auth_code_grant_success.py +35 -0
  122. aiq/front_ends/fastapi/main.py +2 -0
  123. aiq/front_ends/fastapi/message_handler.py +102 -84
  124. aiq/front_ends/fastapi/step_adaptor.py +2 -1
  125. aiq/llm/aws_bedrock_llm.py +2 -1
  126. aiq/llm/nim_llm.py +2 -1
  127. aiq/llm/openai_llm.py +2 -1
  128. aiq/object_store/__init__.py +20 -0
  129. aiq/object_store/in_memory_object_store.py +74 -0
  130. aiq/object_store/interfaces.py +84 -0
  131. aiq/object_store/models.py +36 -0
  132. aiq/object_store/register.py +20 -0
  133. aiq/observability/__init__.py +14 -0
  134. aiq/observability/exporter/__init__.py +14 -0
  135. aiq/observability/exporter/base_exporter.py +449 -0
  136. aiq/observability/exporter/exporter.py +78 -0
  137. aiq/observability/exporter/file_exporter.py +33 -0
  138. aiq/observability/exporter/processing_exporter.py +269 -0
  139. aiq/observability/exporter/raw_exporter.py +52 -0
  140. aiq/observability/exporter/span_exporter.py +264 -0
  141. aiq/observability/exporter_manager.py +335 -0
  142. aiq/observability/mixin/__init__.py +14 -0
  143. aiq/observability/mixin/batch_config_mixin.py +26 -0
  144. aiq/observability/mixin/collector_config_mixin.py +23 -0
  145. aiq/observability/mixin/file_mixin.py +288 -0
  146. aiq/observability/mixin/file_mode.py +23 -0
  147. aiq/observability/mixin/resource_conflict_mixin.py +134 -0
  148. aiq/observability/mixin/serialize_mixin.py +61 -0
  149. aiq/observability/mixin/type_introspection_mixin.py +183 -0
  150. aiq/observability/processor/__init__.py +14 -0
  151. aiq/observability/processor/batching_processor.py +316 -0
  152. aiq/observability/processor/intermediate_step_serializer.py +28 -0
  153. aiq/observability/processor/processor.py +68 -0
  154. aiq/observability/register.py +32 -116
  155. aiq/observability/utils/__init__.py +14 -0
  156. aiq/observability/utils/dict_utils.py +236 -0
  157. aiq/observability/utils/time_utils.py +31 -0
  158. aiq/profiler/calc/__init__.py +14 -0
  159. aiq/profiler/calc/calc_runner.py +623 -0
  160. aiq/profiler/calc/calculations.py +288 -0
  161. aiq/profiler/calc/data_models.py +176 -0
  162. aiq/profiler/calc/plot.py +345 -0
  163. aiq/profiler/data_models.py +2 -0
  164. aiq/profiler/profile_runner.py +16 -13
  165. aiq/runtime/loader.py +8 -2
  166. aiq/runtime/runner.py +23 -9
  167. aiq/runtime/session.py +16 -5
  168. aiq/tool/chat_completion.py +74 -0
  169. aiq/tool/code_execution/README.md +152 -0
  170. aiq/tool/code_execution/code_sandbox.py +151 -72
  171. aiq/tool/code_execution/local_sandbox/.gitignore +1 -0
  172. aiq/tool/code_execution/local_sandbox/local_sandbox_server.py +139 -24
  173. aiq/tool/code_execution/local_sandbox/sandbox.requirements.txt +3 -1
  174. aiq/tool/code_execution/local_sandbox/start_local_sandbox.sh +27 -2
  175. aiq/tool/code_execution/register.py +7 -3
  176. aiq/tool/code_execution/test_code_execution_sandbox.py +414 -0
  177. aiq/tool/mcp/exceptions.py +142 -0
  178. aiq/tool/mcp/mcp_client.py +17 -3
  179. aiq/tool/mcp/mcp_tool.py +1 -1
  180. aiq/tool/register.py +1 -0
  181. aiq/tool/server_tools.py +2 -2
  182. aiq/utils/exception_handlers/automatic_retries.py +289 -0
  183. aiq/utils/exception_handlers/mcp.py +211 -0
  184. aiq/utils/io/model_processing.py +28 -0
  185. aiq/utils/log_utils.py +37 -0
  186. aiq/utils/string_utils.py +38 -0
  187. aiq/utils/type_converter.py +18 -2
  188. aiq/utils/type_utils.py +87 -0
  189. {aiqtoolkit-1.2.0a20250707.dist-info → aiqtoolkit-1.2.0rc1.dist-info}/METADATA +37 -9
  190. {aiqtoolkit-1.2.0a20250707.dist-info → aiqtoolkit-1.2.0rc1.dist-info}/RECORD +195 -80
  191. {aiqtoolkit-1.2.0a20250707.dist-info → aiqtoolkit-1.2.0rc1.dist-info}/entry_points.txt +3 -0
  192. aiq/front_ends/fastapi/websocket.py +0 -153
  193. aiq/observability/async_otel_listener.py +0 -470
  194. {aiqtoolkit-1.2.0a20250707.dist-info → aiqtoolkit-1.2.0rc1.dist-info}/WHEEL +0 -0
  195. {aiqtoolkit-1.2.0a20250707.dist-info → aiqtoolkit-1.2.0rc1.dist-info}/licenses/LICENSE-3rd-party.txt +0 -0
  196. {aiqtoolkit-1.2.0a20250707.dist-info → aiqtoolkit-1.2.0rc1.dist-info}/licenses/LICENSE.md +0 -0
  197. {aiqtoolkit-1.2.0a20250707.dist-info → aiqtoolkit-1.2.0rc1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,233 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ import asyncio
17
+ import secrets
18
+ import webbrowser
19
+ from dataclasses import dataclass
20
+ from dataclasses import field
21
+
22
+ import click
23
+ import pkce
24
+ from authlib.integrations.httpx_client import AsyncOAuth2Client
25
+ from fastapi import FastAPI
26
+ from fastapi import Request
27
+
28
+ from aiq.authentication.interfaces import FlowHandlerBase
29
+ from aiq.authentication.oauth2.oauth2_auth_code_flow_provider_config import OAuth2AuthCodeFlowProviderConfig
30
+ from aiq.data_models.authentication import AuthenticatedContext
31
+ from aiq.data_models.authentication import AuthFlowType
32
+ from aiq.data_models.authentication import AuthProviderBaseConfig
33
+ from aiq.front_ends.fastapi.fastapi_front_end_controller import _FastApiFrontEndController
34
+
35
+
36
+ # --------------------------------------------------------------------------- #
37
+ # Helpers #
38
+ # --------------------------------------------------------------------------- #
39
+ @dataclass
40
+ class _FlowState:
41
+ future: asyncio.Future = field(default_factory=asyncio.Future, init=False)
42
+ challenge: str | None = None
43
+ verifier: str | None = None
44
+ token_url: str | None = None
45
+ use_pkce: bool | None = None
46
+
47
+
48
+ # --------------------------------------------------------------------------- #
49
+ # Main handler #
50
+ # --------------------------------------------------------------------------- #
51
+ class ConsoleAuthenticationFlowHandler(FlowHandlerBase):
52
+ """
53
+ Authentication helper for CLI / console environments. Supports:
54
+
55
+ • HTTP Basic (username/password)
56
+ • OAuth 2 Authorization‑Code with optional PKCE
57
+ """
58
+
59
+ # ----------------------------- lifecycle ----------------------------- #
60
+ def __init__(self) -> None:
61
+ super().__init__()
62
+ self._server_controller: _FastApiFrontEndController | None = None
63
+ self._redirect_app: FastAPI | None = None # ★ NEW
64
+ self._flows: dict[str, _FlowState] = {}
65
+ self._active_flows = 0
66
+ self._server_lock = asyncio.Lock()
67
+ self._oauth_client: AsyncOAuth2Client | None = None
68
+
69
+ # ----------------------------- public API ---------------------------- #
70
+ async def authenticate(
71
+ self,
72
+ config: AuthProviderBaseConfig,
73
+ method: AuthFlowType,
74
+ ) -> AuthenticatedContext:
75
+ if method == AuthFlowType.HTTP_BASIC:
76
+ return self._handle_http_basic()
77
+ if method == AuthFlowType.OAUTH2_AUTHORIZATION_CODE:
78
+ if (not isinstance(config, OAuth2AuthCodeFlowProviderConfig)):
79
+ raise ValueError("Requested OAuth2 Authorization Code Flow but passed invalid config")
80
+
81
+ return await self._handle_oauth2_auth_code_flow(config)
82
+
83
+ raise NotImplementedError(f"Auth method “{method}” not supported.")
84
+
85
+ # --------------------- OAuth2 helper factories ----------------------- #
86
+ def construct_oauth_client(self, cfg: OAuth2AuthCodeFlowProviderConfig) -> AsyncOAuth2Client:
87
+ """
88
+ Separated for easy overriding in tests (to inject ASGITransport).
89
+ """
90
+ client = AsyncOAuth2Client(
91
+ client_id=cfg.client_id,
92
+ client_secret=cfg.client_secret,
93
+ redirect_uri=cfg.redirect_uri,
94
+ scope=" ".join(cfg.scopes) if cfg.scopes else None,
95
+ token_endpoint=cfg.token_url,
96
+ token_endpoint_auth_method=cfg.token_endpoint_auth_method,
97
+ code_challenge_method="S256" if cfg.use_pkce else None,
98
+ )
99
+ self._oauth_client = client
100
+ return client
101
+
102
+ # --------------------------- HTTP Basic ------------------------------ #
103
+ @staticmethod
104
+ def _handle_http_basic() -> AuthenticatedContext:
105
+ username = click.prompt("Username", type=str)
106
+ password = click.prompt("Password", type=str, hide_input=True)
107
+
108
+ import base64
109
+ credentials = f"{username}:{password}"
110
+ encoded_credentials = base64.b64encode(credentials.encode("utf-8")).decode("ascii")
111
+
112
+ return AuthenticatedContext(
113
+ headers={"Authorization": f"Bearer {encoded_credentials}"},
114
+ metadata={
115
+ "username": username, "password": password
116
+ },
117
+ )
118
+
119
+ # --------------------- OAuth2 Authorization‑Code --------------------- #
120
+ async def _handle_oauth2_auth_code_flow(self, cfg: OAuth2AuthCodeFlowProviderConfig) -> AuthenticatedContext:
121
+ state = secrets.token_urlsafe(16)
122
+ flow_state = _FlowState()
123
+ client = self.construct_oauth_client(cfg)
124
+
125
+ flow_state.token_url = cfg.token_url
126
+ flow_state.use_pkce = cfg.use_pkce
127
+
128
+ # PKCE bits
129
+ if cfg.use_pkce:
130
+ verifier, challenge = pkce.generate_pkce_pair()
131
+ flow_state.verifier = verifier
132
+ flow_state.challenge = challenge
133
+
134
+ auth_url, _ = client.create_authorization_url(
135
+ cfg.authorization_url,
136
+ state=state,
137
+ code_verifier=flow_state.verifier if cfg.use_pkce else None,
138
+ code_challenge=flow_state.challenge if cfg.use_pkce else None,
139
+ **(cfg.authorization_kwargs or {})
140
+ )
141
+
142
+ # Register flow + maybe spin up redirect handler
143
+ async with self._server_lock:
144
+ if (not self._redirect_app):
145
+ self._redirect_app = await self._build_redirect_app()
146
+
147
+ await self._start_redirect_server()
148
+
149
+ self._flows[state] = flow_state
150
+ self._active_flows += 1
151
+
152
+ click.echo("Your browser has been opened for authentication.")
153
+ webbrowser.open(auth_url)
154
+
155
+ # Wait for the redirect to land
156
+ try:
157
+ token = await asyncio.wait_for(flow_state.future, timeout=300)
158
+ except asyncio.TimeoutError:
159
+ raise RuntimeError("Authentication timed out (5 min).")
160
+ finally:
161
+ async with self._server_lock:
162
+ self._flows.pop(state, None)
163
+ self._active_flows -= 1
164
+
165
+ if self._active_flows == 0:
166
+ await self._stop_redirect_server()
167
+
168
+ return AuthenticatedContext(
169
+ headers={"Authorization": f"Bearer {token['access_token']}"},
170
+ metadata={
171
+ "expires_at": token.get("expires_at"), "raw_token": token
172
+ },
173
+ )
174
+
175
+ # --------------- redirect server / in‑process app -------------------- #
176
+ async def _build_redirect_app(self) -> FastAPI:
177
+ """
178
+ * If cfg.run_redirect_local_server == True → start a uvicorn server (old behaviour).
179
+ * Else → only build the FastAPI app and save it to `self._redirect_app`
180
+ for in‑process testing with ASGITransport.
181
+ """
182
+ app = FastAPI()
183
+
184
+ @app.get("/auth/redirect")
185
+ async def handle_redirect(request: Request):
186
+ state = request.query_params.get("state")
187
+ if not state or state not in self._flows:
188
+ return "Invalid state; restart authentication."
189
+ flow_state = self._flows[state]
190
+ try:
191
+ token = await self._oauth_client.fetch_token( # type: ignore[arg-type]
192
+ url=flow_state.token_url,
193
+ authorization_response=str(request.url),
194
+ code_verifier=flow_state.verifier if flow_state.use_pkce else None,
195
+ state=state,
196
+ )
197
+ flow_state.future.set_result(token)
198
+ except Exception as exc: # noqa: BLE001
199
+ flow_state.future.set_exception(exc)
200
+ return "Authentication successful – you may close this tab."
201
+
202
+ return app
203
+
204
+ async def _start_redirect_server(self) -> None:
205
+ # If the server is already running, do nothing
206
+ if self._server_controller:
207
+ return
208
+ try:
209
+ if not self._redirect_app:
210
+ raise RuntimeError("Redirect app not built.")
211
+
212
+ self._server_controller = _FastApiFrontEndController(self._redirect_app)
213
+
214
+ asyncio.create_task(self._server_controller.start_server(host="localhost", port=8000))
215
+
216
+ # Give uvicorn a moment to bind sockets before we return
217
+ await asyncio.sleep(0.3)
218
+ except Exception as exc: # noqa: BLE001
219
+ raise RuntimeError(f"Failed to start redirect server: {exc}") from exc
220
+
221
+ async def _stop_redirect_server(self) -> None:
222
+ if self._server_controller:
223
+ await self._server_controller.stop_server()
224
+ self._server_controller = None
225
+
226
+ # ------------------------- test helpers ------------------------------ #
227
+ @property
228
+ def redirect_app(self) -> FastAPI | None:
229
+ """
230
+ In “test‑mode” (run_redirect_local_server=False) the in‑memory FastAPI
231
+ app is exposed so you can mount it on `httpx.ASGITransport`.
232
+ """
233
+ return self._redirect_app
@@ -25,6 +25,7 @@ from aiq.data_models.interactive import HumanPromptModelType
25
25
  from aiq.data_models.interactive import HumanResponse
26
26
  from aiq.data_models.interactive import HumanResponseText
27
27
  from aiq.data_models.interactive import InteractionPrompt
28
+ from aiq.front_ends.console.authentication_flow_handler import ConsoleAuthenticationFlowHandler
28
29
  from aiq.front_ends.console.console_front_end_config import ConsoleFrontEndConfig
29
30
  from aiq.front_ends.simple_base.simple_front_end_plugin_base import SimpleFrontEndPluginBase
30
31
  from aiq.runtime.session import AIQSessionManager
@@ -43,12 +44,18 @@ async def prompt_for_input_cli(question: InteractionPrompt) -> HumanResponse:
43
44
 
44
45
  return HumanResponseText(text=user_response)
45
46
 
46
- raise ValueError("Unsupported human propmt input type. The run command only supports the 'HumanPromptText' "
47
+ raise ValueError("Unsupported human prompt input type. The run command only supports the 'HumanPromptText' "
47
48
  "input type. Please use the 'serve' command to ensure full support for all input types.")
48
49
 
49
50
 
50
51
  class ConsoleFrontEndPlugin(SimpleFrontEndPluginBase[ConsoleFrontEndConfig]):
51
52
 
53
+ def __init__(self, full_config):
54
+ super().__init__(full_config=full_config)
55
+
56
+ # Set the authentication flow handler
57
+ self.auth_flow_handler = ConsoleAuthenticationFlowHandler()
58
+
52
59
  async def pre_run(self):
53
60
 
54
61
  if (not self.front_end_config.input_query and not self.front_end_config.input_file):
@@ -81,7 +88,9 @@ class ConsoleFrontEndPlugin(SimpleFrontEndPluginBase[ConsoleFrontEndConfig]):
81
88
 
82
89
  async def run_single_query(query):
83
90
 
84
- async with session_manager.session(user_input_callback=prompt_for_input_cli) as session:
91
+ async with session_manager.session(
92
+ user_input_callback=prompt_for_input_cli,
93
+ user_authentication_callback=self.auth_flow_handler.authenticate) as session:
85
94
  async with session.run(query) as runner:
86
95
  base_output = await runner.result(to_type=str)
87
96
 
File without changes
@@ -0,0 +1,27 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ from aiq.authentication.interfaces import FlowHandlerBase
17
+ from aiq.data_models.authentication import AuthenticatedContext
18
+ from aiq.data_models.authentication import AuthFlowType
19
+ from aiq.data_models.authentication import AuthProviderBaseConfig
20
+
21
+
22
+ class HTTPAuthenticationFlowHandler(FlowHandlerBase):
23
+
24
+ async def authenticate(self, config: AuthProviderBaseConfig, method: AuthFlowType) -> AuthenticatedContext:
25
+
26
+ raise NotImplementedError(f"Authentication method '{method}' is not supported by the HTTP frontend."
27
+ f" Do you have Websockets enabled?")
@@ -0,0 +1,107 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ import asyncio
17
+ import logging
18
+ import secrets
19
+ from collections.abc import Awaitable
20
+ from collections.abc import Callable
21
+ from dataclasses import dataclass
22
+ from dataclasses import field
23
+
24
+ import pkce
25
+ from authlib.integrations.httpx_client import AsyncOAuth2Client
26
+
27
+ from aiq.authentication.interfaces import FlowHandlerBase
28
+ from aiq.authentication.oauth2.oauth2_auth_code_flow_provider_config import OAuth2AuthCodeFlowProviderConfig
29
+ from aiq.data_models.authentication import AuthenticatedContext
30
+ from aiq.data_models.authentication import AuthFlowType
31
+ from aiq.data_models.interactive import _HumanPromptOAuthConsent
32
+ from aiq.front_ends.fastapi.message_handler import WebSocketMessageHandler
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ @dataclass
38
+ class FlowState:
39
+ future: asyncio.Future = field(default_factory=asyncio.Future, init=False)
40
+ challenge: str | None = None
41
+ verifier: str | None = None
42
+ client: AsyncOAuth2Client | None = None
43
+ config: OAuth2AuthCodeFlowProviderConfig | None = None
44
+
45
+
46
+ class WebSocketAuthenticationFlowHandler(FlowHandlerBase):
47
+
48
+ def __init__(self,
49
+ add_flow_cb: Callable[[str, FlowState], Awaitable[None]],
50
+ remove_flow_cb: Callable[[str], Awaitable[None]],
51
+ web_socket_message_handler: WebSocketMessageHandler):
52
+
53
+ self._add_flow_cb: Callable[[str, FlowState], Awaitable[None]] = add_flow_cb
54
+ self._remove_flow_cb: Callable[[str], Awaitable[None]] = remove_flow_cb
55
+ self._web_socket_message_handler: WebSocketMessageHandler = web_socket_message_handler
56
+
57
+ async def authenticate(self, config: OAuth2AuthCodeFlowProviderConfig,
58
+ method: AuthFlowType) -> AuthenticatedContext:
59
+ if method == AuthFlowType.OAUTH2_AUTHORIZATION_CODE:
60
+ return await self._handle_oauth2_auth_code_flow(config)
61
+
62
+ raise NotImplementedError(f"Authentication method '{method}' is not supported by the websocket frontend.")
63
+
64
+ def create_oauth_client(self, config: OAuth2AuthCodeFlowProviderConfig):
65
+ return AsyncOAuth2Client(client_id=config.client_id,
66
+ client_secret=config.client_secret,
67
+ redirect_uri=config.redirect_uri,
68
+ scope=" ".join(config.scopes) if config.scopes else None,
69
+ token_endpoint=config.token_url,
70
+ code_challenge_method='S256' if config.use_pkce else None,
71
+ token_endpoint_auth_method=config.token_endpoint_auth_method)
72
+
73
+ async def _handle_oauth2_auth_code_flow(self, config: OAuth2AuthCodeFlowProviderConfig) -> AuthenticatedContext:
74
+
75
+ state = secrets.token_urlsafe(16)
76
+ flow_state = FlowState(config=config)
77
+
78
+ flow_state.client = self.create_oauth_client(config)
79
+
80
+ if config.use_pkce:
81
+ verifier, challenge = pkce.generate_pkce_pair()
82
+ flow_state.verifier = verifier
83
+ flow_state.challenge = challenge
84
+
85
+ authorization_url, _ = flow_state.client.create_authorization_url(
86
+ config.authorization_url,
87
+ state=state,
88
+ code_verifier=flow_state.verifier if config.use_pkce else None,
89
+ code_challenge=flow_state.challenge if config.use_pkce else None,
90
+ **(config.authorization_kwargs or {})
91
+ )
92
+
93
+ await self._add_flow_cb(state, flow_state)
94
+ await self._web_socket_message_handler.create_websocket_message(_HumanPromptOAuthConsent(text=authorization_url)
95
+ )
96
+ try:
97
+ token = await asyncio.wait_for(flow_state.future, timeout=300)
98
+ except asyncio.TimeoutError:
99
+ raise RuntimeError("Authentication flow timed out after 5 minutes.")
100
+ finally:
101
+
102
+ await self._remove_flow_cb(state)
103
+
104
+ return AuthenticatedContext(headers={"Authorization": f"Bearer {token['access_token']}"},
105
+ metadata={
106
+ "expires_at": token.get("expires_at"), "raw_token": token
107
+ })
@@ -22,6 +22,7 @@ from pydantic import BaseModel
22
22
  from pydantic import Field
23
23
  from pydantic import field_validator
24
24
 
25
+ from aiq.data_models.component_ref import ObjectStoreRef
25
26
  from aiq.data_models.front_end import FrontEndBaseConfig
26
27
  from aiq.data_models.step_adaptor import StepAdaptorConfig
27
28
 
@@ -138,6 +139,13 @@ class FastApiFrontEndConfig(FrontEndBaseConfig, name="fastapi"):
138
139
  description=("Path for the default workflow using the OpenAI API Specification. "
139
140
  "If None, no workflow endpoint with the OpenAI API Specification is created."),
140
141
  )
142
+ openai_api_v1_path: str | None = Field(
143
+ default=None,
144
+ description=("Path for the OpenAI v1 Chat Completions API compatible endpoint. "
145
+ "If provided, creates a single endpoint that handles both streaming and "
146
+ "non-streaming requests based on the 'stream' parameter, following the "
147
+ "OpenAI Chat Completions API specification exactly."),
148
+ )
141
149
 
142
150
  class Endpoint(EndpointBase):
143
151
  function_name: str = Field(description="The name of the function to call for this endpoint")
@@ -183,6 +191,7 @@ class FastApiFrontEndConfig(FrontEndBaseConfig, name="fastapi"):
183
191
  path="/generate",
184
192
  websocket_path="/websocket",
185
193
  openai_api_path="/chat",
194
+ openai_api_v1_path="/v1/chat/completions",
186
195
  description="Executes the default AIQ Toolkit workflow from the loaded configuration ",
187
196
  )
188
197
 
@@ -192,6 +201,10 @@ class FastApiFrontEndConfig(FrontEndBaseConfig, name="fastapi"):
192
201
  description="Evaluates the performance and accuracy of the workflow on a dataset",
193
202
  )
194
203
 
204
+ oauth2_callback_path: str | None = Field(
205
+ default="/auth/redirect",
206
+ description="OAuth2.0 authentication callback endpoint. If None, no OAuth2 callback endpoint is created.")
207
+
195
208
  endpoints: list[Endpoint] = Field(
196
209
  default_factory=list,
197
210
  description=(
@@ -212,3 +225,10 @@ class FastApiFrontEndConfig(FrontEndBaseConfig, name="fastapi"):
212
225
  "Each runner is responsible for loading and running the AIQ Toolkit workflow. "
213
226
  "Note: This is different from the worker class used by Gunicorn."),
214
227
  )
228
+
229
+ object_store: ObjectStoreRef | None = Field(
230
+ default=None,
231
+ description=(
232
+ "Object store reference for the FastAPI app. If present, static files can be uploaded via a POST "
233
+ "request to '/static' and files will be served from the object store. The files will be served from the "
234
+ "object store at '/static/{file_name}'."))
@@ -0,0 +1,68 @@
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 fastapi import FastAPI
20
+ from uvicorn import Config
21
+ from uvicorn import Server
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class _FastApiFrontEndController:
27
+ """
28
+ _FastApiFrontEndController class controls the spawing and tear down of the API server in environments where
29
+ the server is needed and not already running.
30
+ """
31
+
32
+ def __init__(self, app: FastAPI):
33
+ self._app: FastAPI = app
34
+ self._server: Server | None = None
35
+ self._server_background_task: asyncio.Task | None = None
36
+
37
+ async def start_server(self, host: str, port: int) -> None:
38
+ """Starts the API server."""
39
+
40
+ server_host = host
41
+ server_port = port
42
+
43
+ config = Config(app=self._app, host=server_host, port=server_port, log_level="warning")
44
+ self._server = Server(config=config)
45
+
46
+ try:
47
+ self._server_background_task = asyncio.create_task(self._server.serve())
48
+ except asyncio.CancelledError as e:
49
+ error_message = f"Task error occurred while starting API server: {str(e)}"
50
+ logger.error(error_message, exc_info=True)
51
+ raise RuntimeError(error_message) from e
52
+ except Exception as e:
53
+ error_message = f"Unexpected error occurred while starting API server: {str(e)}"
54
+ logger.error(error_message, exc_info=True)
55
+ raise RuntimeError(error_message) from e
56
+
57
+ async def stop_server(self) -> None:
58
+ """Stops the API server."""
59
+ if not self._server or not self._server_background_task:
60
+ return
61
+
62
+ try:
63
+ self._server.should_exit = True
64
+ await self._server_background_task
65
+ except asyncio.CancelledError as e:
66
+ logger.error("Server shutdown failed: %s", str(e), exc_info=True)
67
+ except Exception as e:
68
+ logger.error("Unexpected error occurred: %s", str(e), exc_info=True)
@@ -13,6 +13,7 @@
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
15
 
16
+ import logging
16
17
  import os
17
18
  import tempfile
18
19
  import typing
@@ -23,6 +24,8 @@ from aiq.front_ends.fastapi.fastapi_front_end_plugin_worker import FastApiFrontE
23
24
  from aiq.front_ends.fastapi.main import get_app
24
25
  from aiq.utils.io.yaml_tools import yaml_dump
25
26
 
27
+ logger = logging.getLogger(__name__)
28
+
26
29
 
27
30
  class FastApiFrontEndPlugin(FrontEndBase[FastApiFrontEndConfig]):
28
31
 
@@ -44,7 +47,7 @@ class FastApiFrontEndPlugin(FrontEndBase[FastApiFrontEndConfig]):
44
47
  async def run(self):
45
48
 
46
49
  # Write the entire config to a temporary file
47
- with tempfile.NamedTemporaryFile(mode="w", prefix="aiq_config", suffix=".yml", delete=True) as config_file:
50
+ with tempfile.NamedTemporaryFile(mode="w", prefix="aiq_config", suffix=".yml", delete=False) as config_file:
48
51
 
49
52
  # Get as dict
50
53
  config_dict = self.full_config.model_dump(mode="json", by_alias=True, round_trip=True)
@@ -52,12 +55,16 @@ class FastApiFrontEndPlugin(FrontEndBase[FastApiFrontEndConfig]):
52
55
  # Write to YAML file
53
56
  yaml_dump(config_dict, config_file)
54
57
 
58
+ # Save the config file path for cleanup (required on Windows due to delete=False workaround)
59
+ config_file_name = config_file.name
60
+
55
61
  # Set the config file in the environment
56
62
  os.environ["AIQ_CONFIG_FILE"] = str(config_file.name)
57
63
 
58
64
  # Set the worker class in the environment
59
65
  os.environ["AIQ_FRONT_END_WORKER"] = self.get_worker_class_name()
60
66
 
67
+ try:
61
68
  if not self.front_end_config.use_gunicorn:
62
69
  import uvicorn
63
70
 
@@ -101,3 +108,9 @@ class FastApiFrontEndPlugin(FrontEndBase[FastApiFrontEndConfig]):
101
108
  }
102
109
 
103
110
  StandaloneApplication(app, options=options).run()
111
+
112
+ finally:
113
+ try:
114
+ os.remove(config_file_name)
115
+ except OSError as e:
116
+ logger.error(f"Warning: Failed to delete temp file {config_file_name}: {e}")