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.

Files changed (197) hide show
  1. aiq/agent/base.py +171 -8
  2. aiq/agent/dual_node.py +1 -1
  3. aiq/agent/react_agent/agent.py +113 -113
  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 +117 -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.0a20250706.dist-info → aiqtoolkit-1.2.0a20250730.dist-info}/METADATA +37 -9
  190. {aiqtoolkit-1.2.0a20250706.dist-info → aiqtoolkit-1.2.0a20250730.dist-info}/RECORD +195 -80
  191. {aiqtoolkit-1.2.0a20250706.dist-info → aiqtoolkit-1.2.0a20250730.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.0a20250706.dist-info → aiqtoolkit-1.2.0a20250730.dist-info}/WHEEL +0 -0
  195. {aiqtoolkit-1.2.0a20250706.dist-info → aiqtoolkit-1.2.0a20250730.dist-info}/licenses/LICENSE-3rd-party.txt +0 -0
  196. {aiqtoolkit-1.2.0a20250706.dist-info → aiqtoolkit-1.2.0a20250730.dist-info}/licenses/LICENSE.md +0 -0
  197. {aiqtoolkit-1.2.0a20250706.dist-info → aiqtoolkit-1.2.0a20250730.dist-info}/top_level.txt +0 -0
@@ -20,8 +20,9 @@ import time
20
20
  import typing
21
21
  from abc import ABC
22
22
  from abc import abstractmethod
23
+ from collections.abc import Awaitable
24
+ from collections.abc import Callable
23
25
  from contextlib import asynccontextmanager
24
- from functools import partial
25
26
  from pathlib import Path
26
27
 
27
28
  from fastapi import BackgroundTasks
@@ -29,11 +30,13 @@ from fastapi import Body
29
30
  from fastapi import FastAPI
30
31
  from fastapi import Request
31
32
  from fastapi import Response
33
+ from fastapi import UploadFile
32
34
  from fastapi.exceptions import HTTPException
33
35
  from fastapi.middleware.cors import CORSMiddleware
34
36
  from fastapi.responses import StreamingResponse
35
37
  from pydantic import BaseModel
36
38
  from pydantic import Field
39
+ from starlette.websockets import WebSocket
37
40
 
38
41
  from aiq.builder.workflow_builder import WorkflowBuilder
39
42
  from aiq.data_models.api_server import AIQChatRequest
@@ -41,9 +44,14 @@ from aiq.data_models.api_server import AIQChatResponse
41
44
  from aiq.data_models.api_server import AIQChatResponseChunk
42
45
  from aiq.data_models.api_server import AIQResponseIntermediateStep
43
46
  from aiq.data_models.config import AIQConfig
47
+ from aiq.data_models.object_store import KeyAlreadyExistsError
48
+ from aiq.data_models.object_store import NoSuchKeyError
44
49
  from aiq.eval.config import EvaluationRunOutput
45
50
  from aiq.eval.evaluate import EvaluationRun
46
51
  from aiq.eval.evaluate import EvaluationRunConfig
52
+ from aiq.front_ends.fastapi.auth_flow_handlers.http_flow_handler import HTTPAuthenticationFlowHandler
53
+ from aiq.front_ends.fastapi.auth_flow_handlers.websocket_flow_handler import FlowState
54
+ from aiq.front_ends.fastapi.auth_flow_handlers.websocket_flow_handler import WebSocketAuthenticationFlowHandler
47
55
  from aiq.front_ends.fastapi.fastapi_front_end_config import AIQAsyncGenerateResponse
48
56
  from aiq.front_ends.fastapi.fastapi_front_end_config import AIQAsyncGenerationStatusResponse
49
57
  from aiq.front_ends.fastapi.fastapi_front_end_config import AIQEvaluateRequest
@@ -52,11 +60,12 @@ from aiq.front_ends.fastapi.fastapi_front_end_config import AIQEvaluateStatusRes
52
60
  from aiq.front_ends.fastapi.fastapi_front_end_config import FastApiFrontEndConfig
53
61
  from aiq.front_ends.fastapi.job_store import JobInfo
54
62
  from aiq.front_ends.fastapi.job_store import JobStore
63
+ from aiq.front_ends.fastapi.message_handler import WebSocketMessageHandler
55
64
  from aiq.front_ends.fastapi.response_helpers import generate_single_response
56
65
  from aiq.front_ends.fastapi.response_helpers import generate_streaming_response_as_str
57
66
  from aiq.front_ends.fastapi.response_helpers import generate_streaming_response_full_as_str
58
67
  from aiq.front_ends.fastapi.step_adaptor import StepAdaptor
59
- from aiq.front_ends.fastapi.websocket import AIQWebSocket
68
+ from aiq.object_store.models import ObjectStoreItem
60
69
  from aiq.runtime.session import AIQSessionManager
61
70
 
62
71
  logger = logging.getLogger(__name__)
@@ -74,6 +83,7 @@ class FastApiFrontEndPluginWorkerBase(ABC):
74
83
 
75
84
  self._cleanup_tasks: list[str] = []
76
85
  self._cleanup_tasks_lock = asyncio.Lock()
86
+ self._http_flow_handler: HTTPAuthenticationFlowHandler | None = HTTPAuthenticationFlowHandler()
77
87
 
78
88
  @property
79
89
  def config(self) -> AIQConfig:
@@ -81,7 +91,6 @@ class FastApiFrontEndPluginWorkerBase(ABC):
81
91
 
82
92
  @property
83
93
  def front_end_config(self) -> FastApiFrontEndConfig:
84
-
85
94
  return self._front_end_config
86
95
 
87
96
  def build_app(self) -> FastAPI:
@@ -116,8 +125,13 @@ class FastApiFrontEndPluginWorkerBase(ABC):
116
125
 
117
126
  aiq_app = FastAPI(lifespan=lifespan)
118
127
 
128
+ # Configure app CORS.
119
129
  self.set_cors_config(aiq_app)
120
130
 
131
+ @aiq_app.middleware("http")
132
+ async def authentication_log_filter(request: Request, call_next: Callable[[Request], Awaitable[Response]]):
133
+ return await self._suppress_authentication_logs(request, call_next)
134
+
121
135
  return aiq_app
122
136
 
123
137
  def set_cors_config(self, aiq_app: FastAPI) -> None:
@@ -152,6 +166,26 @@ class FastApiFrontEndPluginWorkerBase(ABC):
152
166
  **cors_kwargs,
153
167
  )
154
168
 
169
+ async def _suppress_authentication_logs(self, request: Request,
170
+ call_next: Callable[[Request], Awaitable[Response]]) -> Response:
171
+ """
172
+ Intercepts authentication request and supreses logs that contain sensitive data.
173
+ """
174
+ from aiq.utils.log_utils import LogFilter
175
+
176
+ logs_to_suppress: list[str] = []
177
+
178
+ if (self.front_end_config.oauth2_callback_path):
179
+ logs_to_suppress.append(self.front_end_config.oauth2_callback_path)
180
+
181
+ logging.getLogger("uvicorn.access").addFilter(LogFilter(logs_to_suppress))
182
+ try:
183
+ response = await call_next(request)
184
+ finally:
185
+ logging.getLogger("uvicorn.access").removeFilter(LogFilter(logs_to_suppress))
186
+
187
+ return response
188
+
155
189
  @abstractmethod
156
190
  async def configure(self, app: FastAPI, builder: WorkflowBuilder):
157
191
  pass
@@ -168,6 +202,12 @@ class RouteInfo(BaseModel):
168
202
 
169
203
  class FastApiFrontEndPluginWorker(FastApiFrontEndPluginWorkerBase):
170
204
 
205
+ def __init__(self, config: AIQConfig):
206
+ super().__init__(config)
207
+
208
+ self._outstanding_flows: dict[str, FlowState] = {}
209
+ self._outstanding_flows_lock = asyncio.Lock()
210
+
171
211
  @staticmethod
172
212
  async def _periodic_cleanup(name: str, job_store: JobStore, sleep_time_sec: int = 300):
173
213
  while True:
@@ -209,6 +249,8 @@ class FastApiFrontEndPluginWorker(FastApiFrontEndPluginWorkerBase):
209
249
 
210
250
  await self.add_default_route(app, AIQSessionManager(builder.build()))
211
251
  await self.add_evaluate_route(app, AIQSessionManager(builder.build()))
252
+ await self.add_static_files_route(app, builder)
253
+ await self.add_authorization_route(app)
212
254
 
213
255
  for ep in self.front_end_config.endpoints:
214
256
 
@@ -381,6 +423,100 @@ class FastApiFrontEndPluginWorker(FastApiFrontEndPluginWorkerBase):
381
423
  responses={500: response_500},
382
424
  )
383
425
 
426
+ async def add_static_files_route(self, app: FastAPI, builder: WorkflowBuilder):
427
+
428
+ if not self.front_end_config.object_store:
429
+ logger.debug("No object store configured, skipping static files route")
430
+ return
431
+
432
+ object_store_client = await builder.get_object_store_client(self.front_end_config.object_store)
433
+
434
+ def sanitize_path(path: str) -> str:
435
+ sanitized_path = os.path.normpath(path.strip("/"))
436
+ if sanitized_path == ".":
437
+ raise HTTPException(status_code=400, detail="Invalid file path.")
438
+ filename = os.path.basename(sanitized_path)
439
+ if not filename:
440
+ raise HTTPException(status_code=400, detail="Filename cannot be empty.")
441
+ return sanitized_path
442
+
443
+ # Upload static files to the object store; if key is present, it will fail with 409 Conflict
444
+ async def add_static_file(file_path: str, file: UploadFile):
445
+ sanitized_file_path = sanitize_path(file_path)
446
+ file_data = await file.read()
447
+
448
+ try:
449
+ await object_store_client.put_object(sanitized_file_path,
450
+ ObjectStoreItem(data=file_data, content_type=file.content_type))
451
+ except KeyAlreadyExistsError as e:
452
+ raise HTTPException(status_code=409, detail=str(e)) from e
453
+
454
+ return {"filename": sanitized_file_path}
455
+
456
+ # Upsert static files to the object store; if key is present, it will overwrite the file
457
+ async def upsert_static_file(file_path: str, file: UploadFile):
458
+ sanitized_file_path = sanitize_path(file_path)
459
+ file_data = await file.read()
460
+
461
+ await object_store_client.upsert_object(sanitized_file_path,
462
+ ObjectStoreItem(data=file_data, content_type=file.content_type))
463
+
464
+ return {"filename": sanitized_file_path}
465
+
466
+ # Get static files from the object store
467
+ async def get_static_file(file_path: str):
468
+
469
+ try:
470
+ file_data = await object_store_client.get_object(file_path)
471
+ except NoSuchKeyError as e:
472
+ raise HTTPException(status_code=404, detail=str(e)) from e
473
+
474
+ filename = file_path.split("/")[-1]
475
+
476
+ async def reader():
477
+ yield file_data.data
478
+
479
+ return StreamingResponse(reader(),
480
+ media_type=file_data.content_type,
481
+ headers={"Content-Disposition": f"attachment; filename={filename}"})
482
+
483
+ async def delete_static_file(file_path: str):
484
+ try:
485
+ await object_store_client.delete_object(file_path)
486
+ except NoSuchKeyError as e:
487
+ raise HTTPException(status_code=404, detail=str(e)) from e
488
+
489
+ return Response(status_code=204)
490
+
491
+ # Add the static files route to the FastAPI app
492
+ app.add_api_route(
493
+ path="/static/{file_path:path}",
494
+ endpoint=add_static_file,
495
+ methods=["POST"],
496
+ description="Upload a static file to the object store",
497
+ )
498
+
499
+ app.add_api_route(
500
+ path="/static/{file_path:path}",
501
+ endpoint=upsert_static_file,
502
+ methods=["PUT"],
503
+ description="Upsert a static file to the object store",
504
+ )
505
+
506
+ app.add_api_route(
507
+ path="/static/{file_path:path}",
508
+ endpoint=get_static_file,
509
+ methods=["GET"],
510
+ description="Get a static file from the object store",
511
+ )
512
+
513
+ app.add_api_route(
514
+ path="/static/{file_path:path}",
515
+ endpoint=delete_static_file,
516
+ methods=["DELETE"],
517
+ description="Delete a static file from the object store",
518
+ )
519
+
384
520
  async def add_route(self,
385
521
  app: FastAPI,
386
522
  endpoint: FastApiFrontEndConfig.EndpointBase,
@@ -388,10 +524,6 @@ class FastApiFrontEndPluginWorker(FastApiFrontEndPluginWorkerBase):
388
524
 
389
525
  workflow = session_manager.workflow
390
526
 
391
- if (endpoint.websocket_path):
392
- app.add_websocket_route(endpoint.websocket_path,
393
- partial(AIQWebSocket, session_manager, self.get_step_adaptor()))
394
-
395
527
  GenerateBodyType = workflow.input_schema # pylint: disable=invalid-name
396
528
  GenerateStreamResponseType = workflow.streaming_output_schema # pylint: disable=invalid-name
397
529
  GenerateSingleResponseType = workflow.single_output_schema # pylint: disable=invalid-name
@@ -442,7 +574,8 @@ class FastApiFrontEndPluginWorker(FastApiFrontEndPluginWorkerBase):
442
574
 
443
575
  response.headers["Content-Type"] = "application/json"
444
576
 
445
- async with session_manager.session(request=request):
577
+ async with session_manager.session(request=request,
578
+ user_authentication_callback=self._http_flow_handler.authenticate):
446
579
 
447
580
  return await generate_single_response(None, session_manager, result_type=result_type)
448
581
 
@@ -452,7 +585,8 @@ class FastApiFrontEndPluginWorker(FastApiFrontEndPluginWorkerBase):
452
585
 
453
586
  async def get_stream(request: Request):
454
587
 
455
- async with session_manager.session(request=request):
588
+ async with session_manager.session(request=request,
589
+ user_authentication_callback=self._http_flow_handler.authenticate):
456
590
 
457
591
  return StreamingResponse(headers={"Content-Type": "text/event-stream; charset=utf-8"},
458
592
  content=generate_streaming_response_as_str(
@@ -486,7 +620,8 @@ class FastApiFrontEndPluginWorker(FastApiFrontEndPluginWorkerBase):
486
620
 
487
621
  response.headers["Content-Type"] = "application/json"
488
622
 
489
- async with session_manager.session(request=request):
623
+ async with session_manager.session(request=request,
624
+ user_authentication_callback=self._http_flow_handler.authenticate):
490
625
 
491
626
  return await generate_single_response(payload, session_manager, result_type=result_type)
492
627
 
@@ -499,7 +634,8 @@ class FastApiFrontEndPluginWorker(FastApiFrontEndPluginWorkerBase):
499
634
 
500
635
  async def post_stream(request: Request, payload: request_type):
501
636
 
502
- async with session_manager.session(request=request):
637
+ async with session_manager.session(request=request,
638
+ user_authentication_callback=self._http_flow_handler.authenticate):
503
639
 
504
640
  return StreamingResponse(headers={"Content-Type": "text/event-stream; charset=utf-8"},
505
641
  content=generate_streaming_response_as_str(
@@ -533,6 +669,66 @@ class FastApiFrontEndPluginWorker(FastApiFrontEndPluginWorkerBase):
533
669
 
534
670
  return post_stream
535
671
 
672
+ def post_openai_api_compatible_endpoint(request_type: type):
673
+ """
674
+ OpenAI-compatible endpoint that handles both streaming and non-streaming
675
+ based on the 'stream' parameter in the request.
676
+ """
677
+
678
+ async def post_openai_api_compatible(response: Response, request: Request, payload: request_type):
679
+ # Check if streaming is requested
680
+ stream_requested = getattr(payload, 'stream', False)
681
+
682
+ async with session_manager.session(request=request):
683
+ if stream_requested:
684
+ # Return streaming response
685
+ return StreamingResponse(headers={"Content-Type": "text/event-stream; charset=utf-8"},
686
+ content=generate_streaming_response_as_str(
687
+ payload,
688
+ session_manager=session_manager,
689
+ streaming=True,
690
+ step_adaptor=self.get_step_adaptor(),
691
+ result_type=AIQChatResponseChunk,
692
+ output_type=AIQChatResponseChunk))
693
+ else:
694
+ # Return single response - check if workflow supports non-streaming
695
+ try:
696
+ response.headers["Content-Type"] = "application/json"
697
+ return await generate_single_response(payload, session_manager, result_type=AIQChatResponse)
698
+ except ValueError as e:
699
+ if "Cannot get a single output value for streaming workflows" in str(e):
700
+ # Workflow only supports streaming, but client requested non-streaming
701
+ # Fall back to streaming and collect the result
702
+ chunks = []
703
+ async for chunk_str in generate_streaming_response_as_str(
704
+ payload,
705
+ session_manager=session_manager,
706
+ streaming=True,
707
+ step_adaptor=self.get_step_adaptor(),
708
+ result_type=AIQChatResponseChunk,
709
+ output_type=AIQChatResponseChunk):
710
+ if chunk_str.startswith("data: ") and not chunk_str.startswith("data: [DONE]"):
711
+ chunk_data = chunk_str[6:].strip() # Remove "data: " prefix
712
+ if chunk_data:
713
+ try:
714
+ chunk_json = AIQChatResponseChunk.model_validate_json(chunk_data)
715
+ if (chunk_json.choices and len(chunk_json.choices) > 0
716
+ and chunk_json.choices[0].delta
717
+ and chunk_json.choices[0].delta.content is not None):
718
+ chunks.append(chunk_json.choices[0].delta.content)
719
+ except Exception:
720
+ continue
721
+
722
+ # Create a single response from collected chunks
723
+ content = "".join(chunks)
724
+ single_response = AIQChatResponse.from_string(content)
725
+ response.headers["Content-Type"] = "application/json"
726
+ return single_response
727
+ else:
728
+ raise
729
+
730
+ return post_openai_api_compatible
731
+
536
732
  async def run_generation(job_id: str,
537
733
  payload: typing.Any,
538
734
  session_manager: AIQSessionManager,
@@ -623,7 +819,56 @@ class FastApiFrontEndPluginWorker(FastApiFrontEndPluginWorkerBase):
623
819
  logger.info("Found job %s with status %s", job_id, job.status)
624
820
  return _job_status_to_response(job)
625
821
 
822
+ async def websocket_endpoint(websocket: WebSocket):
823
+
824
+ # Universal cookie handling: works for both cross-origin and same-origin connections
825
+ session_id = websocket.query_params.get("session")
826
+ if session_id:
827
+ headers = list(websocket.scope.get("headers", []))
828
+ cookie_header = f"aiqtoolkit-session={session_id}"
829
+
830
+ # Check if the session cookie already exists to avoid duplicates
831
+ cookie_exists = False
832
+ existing_session_cookie = False
833
+
834
+ for i, (name, value) in enumerate(headers):
835
+ if name == b"cookie":
836
+ cookie_exists = True
837
+ cookie_str = value.decode()
838
+
839
+ # Check if aiqtoolkit-session already exists in cookies
840
+ if "aiqtoolkit-session=" in cookie_str:
841
+ existing_session_cookie = True
842
+ logger.info("WebSocket: Session cookie already present in headers (same-origin)")
843
+ else:
844
+ # Append to existing cookie header (cross-origin case)
845
+ headers[i] = (name, f"{cookie_str}; {cookie_header}".encode())
846
+ logger.info("WebSocket: Added session cookie to existing cookie header: %s",
847
+ session_id[:10] + "...")
848
+ break
849
+
850
+ # Add new cookie header only if no cookies exist and no session cookie found
851
+ if not cookie_exists and not existing_session_cookie:
852
+ headers.append((b"cookie", cookie_header.encode()))
853
+ logger.info("WebSocket: Added new session cookie header: %s", session_id[:10] + "...")
854
+
855
+ # Update the websocket scope with the modified headers
856
+ websocket.scope["headers"] = headers
857
+
858
+ async with WebSocketMessageHandler(websocket, session_manager, self.get_step_adaptor()) as handler:
859
+
860
+ flow_handler = WebSocketAuthenticationFlowHandler(self._add_flow, self._remove_flow, handler)
861
+
862
+ # Ugly hack to set the flow handler on the message handler. Both need eachother to be set.
863
+ handler.set_flow_handler(flow_handler)
864
+
865
+ await handler.run()
866
+
867
+ if (endpoint.websocket_path):
868
+ app.add_websocket_route(endpoint.websocket_path, websocket_endpoint)
869
+
626
870
  if (endpoint.path):
871
+
627
872
  if (endpoint.method == "GET"):
628
873
 
629
874
  app.add_api_route(
@@ -745,26 +990,103 @@ class FastApiFrontEndPluginWorker(FastApiFrontEndPluginWorkerBase):
745
990
 
746
991
  elif (endpoint.method == "POST"):
747
992
 
748
- app.add_api_route(
749
- path=endpoint.openai_api_path,
750
- endpoint=post_single_endpoint(request_type=AIQChatRequest, result_type=AIQChatResponse),
751
- methods=[endpoint.method],
752
- response_model=AIQChatResponse,
753
- description=endpoint.description,
754
- responses={500: response_500},
755
- )
756
-
757
- app.add_api_route(
758
- path=f"{endpoint.openai_api_path}/stream",
759
- endpoint=post_streaming_endpoint(request_type=AIQChatRequest,
760
- streaming=True,
761
- result_type=AIQChatResponseChunk,
762
- output_type=AIQChatResponseChunk),
763
- methods=[endpoint.method],
764
- response_model=AIQChatResponseChunk | AIQResponseIntermediateStep,
765
- description=endpoint.description,
766
- responses={500: response_500},
767
- )
993
+ # Check if OpenAI v1 compatible endpoint is configured
994
+ openai_v1_path = getattr(endpoint, 'openai_api_v1_path', None)
995
+
996
+ # Always create legacy endpoints for backward compatibility (unless they conflict with v1 path)
997
+ if not openai_v1_path or openai_v1_path != endpoint.openai_api_path:
998
+ # <openai_api_path> = non-streaming (legacy behavior)
999
+ app.add_api_route(
1000
+ path=endpoint.openai_api_path,
1001
+ endpoint=post_single_endpoint(request_type=AIQChatRequest, result_type=AIQChatResponse),
1002
+ methods=[endpoint.method],
1003
+ response_model=AIQChatResponse,
1004
+ description=endpoint.description,
1005
+ responses={500: response_500},
1006
+ )
1007
+
1008
+ # <openai_api_path>/stream = streaming (legacy behavior)
1009
+ app.add_api_route(
1010
+ path=f"{endpoint.openai_api_path}/stream",
1011
+ endpoint=post_streaming_endpoint(request_type=AIQChatRequest,
1012
+ streaming=True,
1013
+ result_type=AIQChatResponseChunk,
1014
+ output_type=AIQChatResponseChunk),
1015
+ methods=[endpoint.method],
1016
+ response_model=AIQChatResponseChunk | AIQResponseIntermediateStep,
1017
+ description=endpoint.description,
1018
+ responses={500: response_500},
1019
+ )
1020
+
1021
+ # Create OpenAI v1 compatible endpoint if configured
1022
+ if openai_v1_path:
1023
+ # OpenAI v1 Compatible Mode: Create single endpoint that handles both streaming and non-streaming
1024
+ app.add_api_route(
1025
+ path=openai_v1_path,
1026
+ endpoint=post_openai_api_compatible_endpoint(request_type=AIQChatRequest),
1027
+ methods=[endpoint.method],
1028
+ response_model=AIQChatResponse | AIQChatResponseChunk,
1029
+ description=f"{endpoint.description} (OpenAI Chat Completions API compatible)",
1030
+ responses={500: response_500},
1031
+ )
768
1032
 
769
1033
  else:
770
1034
  raise ValueError(f"Unsupported method {endpoint.method}")
1035
+
1036
+ async def add_authorization_route(self, app: FastAPI):
1037
+
1038
+ from fastapi.responses import HTMLResponse
1039
+
1040
+ from aiq.front_ends.fastapi.html_snippets.auth_code_grant_success import AUTH_REDIRECT_SUCCESS_HTML
1041
+
1042
+ async def redirect_uri(request: Request):
1043
+ """
1044
+ Handle the redirect URI for OAuth2 authentication.
1045
+ Args:
1046
+ request: The FastAPI request object containing query parameters.
1047
+
1048
+ Returns:
1049
+ HTMLResponse: A response indicating the success of the authentication flow.
1050
+ """
1051
+ state = request.query_params.get("state")
1052
+
1053
+ async with self._outstanding_flows_lock:
1054
+ if not state or state not in self._outstanding_flows:
1055
+ return "Invalid state. Please restart the authentication process."
1056
+
1057
+ flow_state = self._outstanding_flows[state]
1058
+
1059
+ config = flow_state.config
1060
+ verifier = flow_state.verifier
1061
+ client = flow_state.client
1062
+
1063
+ try:
1064
+ res = await client.fetch_token(url=config.token_url,
1065
+ authorization_response=str(request.url),
1066
+ code_verifier=verifier,
1067
+ state=state)
1068
+ flow_state.future.set_result(res)
1069
+ except Exception as e:
1070
+ flow_state.future.set_exception(e)
1071
+
1072
+ return HTMLResponse(content=AUTH_REDIRECT_SUCCESS_HTML,
1073
+ status_code=200,
1074
+ headers={
1075
+ "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-cache"
1076
+ })
1077
+
1078
+ if (self.front_end_config.oauth2_callback_path):
1079
+ # Add the redirect URI route
1080
+ app.add_api_route(
1081
+ path=self.front_end_config.oauth2_callback_path,
1082
+ endpoint=redirect_uri,
1083
+ methods=["GET"],
1084
+ description="Handles the authorization code and state returned from the Authorization Code Grant Flow.")
1085
+
1086
+ async def _add_flow(self, state: str, flow_state: FlowState):
1087
+ async with self._outstanding_flows_lock:
1088
+ self._outstanding_flows[state] = flow_state
1089
+
1090
+ async def _remove_flow(self, state: str):
1091
+ async with self._outstanding_flows_lock:
1092
+ del self._outstanding_flows[state]
@@ -0,0 +1,14 @@
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.
@@ -0,0 +1,35 @@
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
+ AUTH_REDIRECT_SUCCESS_HTML = """
17
+ <!DOCTYPE html>
18
+ <html>
19
+ <head>
20
+ <title>Authentication Complete</title>
21
+ <script>
22
+ (function () {
23
+ window.history.replaceState(null, "", window.location.pathname);
24
+
25
+ window.opener?.postMessage({ type: 'AUTH_SUCCESS' }, '*');
26
+
27
+ window.close();
28
+ })();
29
+ </script>
30
+ </head>
31
+ <body>
32
+ <p>Authentication complete. You may now close this window.</p>
33
+ </body>
34
+ </html>
35
+ """
@@ -68,3 +68,5 @@ def get_app():
68
68
 
69
69
  except ImportError as e:
70
70
  raise ValueError(f"Front end worker {front_end_worker_full_name} not found.") from e
71
+ except Exception as e:
72
+ raise ValueError(f"Error loading front end worker {front_end_worker_full_name}: {e}") from e