aiqtoolkit 1.2.0a20250706__py3-none-any.whl → 1.2.0a20250730__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.
- aiq/agent/base.py +171 -8
- aiq/agent/dual_node.py +1 -1
- aiq/agent/react_agent/agent.py +113 -113
- aiq/agent/react_agent/register.py +31 -14
- aiq/agent/rewoo_agent/agent.py +36 -35
- aiq/agent/rewoo_agent/register.py +2 -2
- aiq/agent/tool_calling_agent/agent.py +3 -7
- aiq/authentication/__init__.py +14 -0
- aiq/authentication/api_key/__init__.py +14 -0
- aiq/authentication/api_key/api_key_auth_provider.py +92 -0
- aiq/authentication/api_key/api_key_auth_provider_config.py +124 -0
- aiq/authentication/api_key/register.py +26 -0
- aiq/authentication/exceptions/__init__.py +14 -0
- aiq/authentication/exceptions/api_key_exceptions.py +38 -0
- aiq/authentication/exceptions/auth_code_grant_exceptions.py +86 -0
- aiq/authentication/exceptions/call_back_exceptions.py +38 -0
- aiq/authentication/exceptions/request_exceptions.py +54 -0
- aiq/authentication/http_basic_auth/__init__.py +0 -0
- aiq/authentication/http_basic_auth/http_basic_auth_provider.py +81 -0
- aiq/authentication/http_basic_auth/register.py +30 -0
- aiq/authentication/interfaces.py +93 -0
- aiq/authentication/oauth2/__init__.py +14 -0
- aiq/authentication/oauth2/oauth2_auth_code_flow_provider.py +107 -0
- aiq/authentication/oauth2/oauth2_auth_code_flow_provider_config.py +39 -0
- aiq/authentication/oauth2/register.py +25 -0
- aiq/authentication/register.py +21 -0
- aiq/builder/builder.py +64 -2
- aiq/builder/component_utils.py +16 -3
- aiq/builder/context.py +26 -0
- aiq/builder/eval_builder.py +43 -2
- aiq/builder/function.py +32 -4
- aiq/builder/function_base.py +1 -1
- aiq/builder/intermediate_step_manager.py +6 -8
- aiq/builder/user_interaction_manager.py +3 -0
- aiq/builder/workflow.py +23 -18
- aiq/builder/workflow_builder.py +420 -73
- aiq/cli/commands/info/list_mcp.py +103 -16
- aiq/cli/commands/sizing/__init__.py +14 -0
- aiq/cli/commands/sizing/calc.py +294 -0
- aiq/cli/commands/sizing/sizing.py +27 -0
- aiq/cli/commands/start.py +1 -0
- aiq/cli/entrypoint.py +2 -0
- aiq/cli/register_workflow.py +80 -0
- aiq/cli/type_registry.py +151 -30
- aiq/data_models/api_server.py +117 -11
- aiq/data_models/authentication.py +231 -0
- aiq/data_models/common.py +35 -7
- aiq/data_models/component.py +17 -9
- aiq/data_models/component_ref.py +33 -0
- aiq/data_models/config.py +60 -3
- aiq/data_models/embedder.py +1 -0
- aiq/data_models/function_dependencies.py +8 -0
- aiq/data_models/interactive.py +10 -1
- aiq/data_models/intermediate_step.py +15 -5
- aiq/data_models/its_strategy.py +30 -0
- aiq/data_models/llm.py +1 -0
- aiq/data_models/memory.py +1 -0
- aiq/data_models/object_store.py +44 -0
- aiq/data_models/retry_mixin.py +35 -0
- aiq/data_models/span.py +187 -0
- aiq/data_models/telemetry_exporter.py +2 -2
- aiq/embedder/nim_embedder.py +2 -1
- aiq/embedder/openai_embedder.py +2 -1
- aiq/eval/config.py +19 -1
- aiq/eval/dataset_handler/dataset_handler.py +75 -1
- aiq/eval/evaluate.py +53 -10
- aiq/eval/rag_evaluator/evaluate.py +23 -12
- aiq/eval/remote_workflow.py +7 -2
- aiq/eval/runners/__init__.py +14 -0
- aiq/eval/runners/config.py +39 -0
- aiq/eval/runners/multi_eval_runner.py +54 -0
- aiq/eval/usage_stats.py +6 -0
- aiq/eval/utils/weave_eval.py +5 -1
- aiq/experimental/__init__.py +0 -0
- aiq/experimental/decorators/__init__.py +0 -0
- aiq/experimental/decorators/experimental_warning_decorator.py +130 -0
- aiq/experimental/inference_time_scaling/__init__.py +0 -0
- aiq/experimental/inference_time_scaling/editing/__init__.py +0 -0
- aiq/experimental/inference_time_scaling/editing/iterative_plan_refinement_editor.py +147 -0
- aiq/experimental/inference_time_scaling/editing/llm_as_a_judge_editor.py +204 -0
- aiq/experimental/inference_time_scaling/editing/motivation_aware_summarization.py +107 -0
- aiq/experimental/inference_time_scaling/functions/__init__.py +0 -0
- aiq/experimental/inference_time_scaling/functions/execute_score_select_function.py +105 -0
- aiq/experimental/inference_time_scaling/functions/its_tool_orchestration_function.py +205 -0
- aiq/experimental/inference_time_scaling/functions/its_tool_wrapper_function.py +146 -0
- aiq/experimental/inference_time_scaling/functions/plan_select_execute_function.py +224 -0
- aiq/experimental/inference_time_scaling/models/__init__.py +0 -0
- aiq/experimental/inference_time_scaling/models/editor_config.py +132 -0
- aiq/experimental/inference_time_scaling/models/its_item.py +48 -0
- aiq/experimental/inference_time_scaling/models/scoring_config.py +112 -0
- aiq/experimental/inference_time_scaling/models/search_config.py +120 -0
- aiq/experimental/inference_time_scaling/models/selection_config.py +154 -0
- aiq/experimental/inference_time_scaling/models/stage_enums.py +43 -0
- aiq/experimental/inference_time_scaling/models/strategy_base.py +66 -0
- aiq/experimental/inference_time_scaling/models/tool_use_config.py +41 -0
- aiq/experimental/inference_time_scaling/register.py +36 -0
- aiq/experimental/inference_time_scaling/scoring/__init__.py +0 -0
- aiq/experimental/inference_time_scaling/scoring/llm_based_agent_scorer.py +168 -0
- aiq/experimental/inference_time_scaling/scoring/llm_based_plan_scorer.py +168 -0
- aiq/experimental/inference_time_scaling/scoring/motivation_aware_scorer.py +111 -0
- aiq/experimental/inference_time_scaling/search/__init__.py +0 -0
- aiq/experimental/inference_time_scaling/search/multi_llm_planner.py +128 -0
- aiq/experimental/inference_time_scaling/search/multi_query_retrieval_search.py +122 -0
- aiq/experimental/inference_time_scaling/search/single_shot_multi_plan_planner.py +128 -0
- aiq/experimental/inference_time_scaling/selection/__init__.py +0 -0
- aiq/experimental/inference_time_scaling/selection/best_of_n_selector.py +63 -0
- aiq/experimental/inference_time_scaling/selection/llm_based_agent_output_selector.py +131 -0
- aiq/experimental/inference_time_scaling/selection/llm_based_output_merging_selector.py +159 -0
- aiq/experimental/inference_time_scaling/selection/llm_based_plan_selector.py +128 -0
- aiq/experimental/inference_time_scaling/selection/threshold_selector.py +58 -0
- aiq/front_ends/console/authentication_flow_handler.py +233 -0
- aiq/front_ends/console/console_front_end_plugin.py +11 -2
- aiq/front_ends/fastapi/auth_flow_handlers/__init__.py +0 -0
- aiq/front_ends/fastapi/auth_flow_handlers/http_flow_handler.py +27 -0
- aiq/front_ends/fastapi/auth_flow_handlers/websocket_flow_handler.py +107 -0
- aiq/front_ends/fastapi/fastapi_front_end_config.py +20 -0
- aiq/front_ends/fastapi/fastapi_front_end_controller.py +68 -0
- aiq/front_ends/fastapi/fastapi_front_end_plugin.py +14 -1
- aiq/front_ends/fastapi/fastapi_front_end_plugin_worker.py +353 -31
- aiq/front_ends/fastapi/html_snippets/__init__.py +14 -0
- aiq/front_ends/fastapi/html_snippets/auth_code_grant_success.py +35 -0
- aiq/front_ends/fastapi/main.py +2 -0
- aiq/front_ends/fastapi/message_handler.py +102 -84
- aiq/front_ends/fastapi/step_adaptor.py +2 -1
- aiq/llm/aws_bedrock_llm.py +2 -1
- aiq/llm/nim_llm.py +2 -1
- aiq/llm/openai_llm.py +2 -1
- aiq/object_store/__init__.py +20 -0
- aiq/object_store/in_memory_object_store.py +74 -0
- aiq/object_store/interfaces.py +84 -0
- aiq/object_store/models.py +36 -0
- aiq/object_store/register.py +20 -0
- aiq/observability/__init__.py +14 -0
- aiq/observability/exporter/__init__.py +14 -0
- aiq/observability/exporter/base_exporter.py +449 -0
- aiq/observability/exporter/exporter.py +78 -0
- aiq/observability/exporter/file_exporter.py +33 -0
- aiq/observability/exporter/processing_exporter.py +269 -0
- aiq/observability/exporter/raw_exporter.py +52 -0
- aiq/observability/exporter/span_exporter.py +264 -0
- aiq/observability/exporter_manager.py +335 -0
- aiq/observability/mixin/__init__.py +14 -0
- aiq/observability/mixin/batch_config_mixin.py +26 -0
- aiq/observability/mixin/collector_config_mixin.py +23 -0
- aiq/observability/mixin/file_mixin.py +288 -0
- aiq/observability/mixin/file_mode.py +23 -0
- aiq/observability/mixin/resource_conflict_mixin.py +134 -0
- aiq/observability/mixin/serialize_mixin.py +61 -0
- aiq/observability/mixin/type_introspection_mixin.py +183 -0
- aiq/observability/processor/__init__.py +14 -0
- aiq/observability/processor/batching_processor.py +316 -0
- aiq/observability/processor/intermediate_step_serializer.py +28 -0
- aiq/observability/processor/processor.py +68 -0
- aiq/observability/register.py +32 -116
- aiq/observability/utils/__init__.py +14 -0
- aiq/observability/utils/dict_utils.py +236 -0
- aiq/observability/utils/time_utils.py +31 -0
- aiq/profiler/calc/__init__.py +14 -0
- aiq/profiler/calc/calc_runner.py +623 -0
- aiq/profiler/calc/calculations.py +288 -0
- aiq/profiler/calc/data_models.py +176 -0
- aiq/profiler/calc/plot.py +345 -0
- aiq/profiler/data_models.py +2 -0
- aiq/profiler/profile_runner.py +16 -13
- aiq/runtime/loader.py +8 -2
- aiq/runtime/runner.py +23 -9
- aiq/runtime/session.py +16 -5
- aiq/tool/chat_completion.py +74 -0
- aiq/tool/code_execution/README.md +152 -0
- aiq/tool/code_execution/code_sandbox.py +151 -72
- aiq/tool/code_execution/local_sandbox/.gitignore +1 -0
- aiq/tool/code_execution/local_sandbox/local_sandbox_server.py +139 -24
- aiq/tool/code_execution/local_sandbox/sandbox.requirements.txt +3 -1
- aiq/tool/code_execution/local_sandbox/start_local_sandbox.sh +27 -2
- aiq/tool/code_execution/register.py +7 -3
- aiq/tool/code_execution/test_code_execution_sandbox.py +414 -0
- aiq/tool/mcp/exceptions.py +142 -0
- aiq/tool/mcp/mcp_client.py +17 -3
- aiq/tool/mcp/mcp_tool.py +1 -1
- aiq/tool/register.py +1 -0
- aiq/tool/server_tools.py +2 -2
- aiq/utils/exception_handlers/automatic_retries.py +289 -0
- aiq/utils/exception_handlers/mcp.py +211 -0
- aiq/utils/io/model_processing.py +28 -0
- aiq/utils/log_utils.py +37 -0
- aiq/utils/string_utils.py +38 -0
- aiq/utils/type_converter.py +18 -2
- aiq/utils/type_utils.py +87 -0
- {aiqtoolkit-1.2.0a20250706.dist-info → aiqtoolkit-1.2.0a20250730.dist-info}/METADATA +37 -9
- {aiqtoolkit-1.2.0a20250706.dist-info → aiqtoolkit-1.2.0a20250730.dist-info}/RECORD +195 -80
- {aiqtoolkit-1.2.0a20250706.dist-info → aiqtoolkit-1.2.0a20250730.dist-info}/entry_points.txt +3 -0
- aiq/front_ends/fastapi/websocket.py +0 -153
- aiq/observability/async_otel_listener.py +0 -470
- {aiqtoolkit-1.2.0a20250706.dist-info → aiqtoolkit-1.2.0a20250730.dist-info}/WHEEL +0 -0
- {aiqtoolkit-1.2.0a20250706.dist-info → aiqtoolkit-1.2.0a20250730.dist-info}/licenses/LICENSE-3rd-party.txt +0 -0
- {aiqtoolkit-1.2.0a20250706.dist-info → aiqtoolkit-1.2.0a20250730.dist-info}/licenses/LICENSE.md +0 -0
- {aiqtoolkit-1.2.0a20250706.dist-info → aiqtoolkit-1.2.0a20250730.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
|
|
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(
|
|
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=
|
|
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}")
|