aiqtoolkit 1.2.0.dev0__py3-none-any.whl → 1.2.0rc2__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 (220) hide show
  1. aiq/agent/base.py +170 -8
  2. aiq/agent/dual_node.py +1 -1
  3. aiq/agent/react_agent/agent.py +146 -112
  4. aiq/agent/react_agent/prompt.py +1 -6
  5. aiq/agent/react_agent/register.py +36 -35
  6. aiq/agent/rewoo_agent/agent.py +36 -35
  7. aiq/agent/rewoo_agent/register.py +2 -2
  8. aiq/agent/tool_calling_agent/agent.py +3 -7
  9. aiq/agent/tool_calling_agent/register.py +1 -1
  10. aiq/authentication/__init__.py +14 -0
  11. aiq/authentication/api_key/__init__.py +14 -0
  12. aiq/authentication/api_key/api_key_auth_provider.py +92 -0
  13. aiq/authentication/api_key/api_key_auth_provider_config.py +124 -0
  14. aiq/authentication/api_key/register.py +26 -0
  15. aiq/authentication/exceptions/__init__.py +14 -0
  16. aiq/authentication/exceptions/api_key_exceptions.py +38 -0
  17. aiq/authentication/exceptions/auth_code_grant_exceptions.py +86 -0
  18. aiq/authentication/exceptions/call_back_exceptions.py +38 -0
  19. aiq/authentication/exceptions/request_exceptions.py +54 -0
  20. aiq/authentication/http_basic_auth/__init__.py +0 -0
  21. aiq/authentication/http_basic_auth/http_basic_auth_provider.py +81 -0
  22. aiq/authentication/http_basic_auth/register.py +30 -0
  23. aiq/authentication/interfaces.py +93 -0
  24. aiq/authentication/oauth2/__init__.py +14 -0
  25. aiq/authentication/oauth2/oauth2_auth_code_flow_provider.py +107 -0
  26. aiq/authentication/oauth2/oauth2_auth_code_flow_provider_config.py +39 -0
  27. aiq/authentication/oauth2/register.py +25 -0
  28. aiq/authentication/register.py +21 -0
  29. aiq/builder/builder.py +64 -2
  30. aiq/builder/component_utils.py +16 -3
  31. aiq/builder/context.py +37 -0
  32. aiq/builder/eval_builder.py +43 -2
  33. aiq/builder/function.py +44 -12
  34. aiq/builder/function_base.py +1 -1
  35. aiq/builder/intermediate_step_manager.py +6 -8
  36. aiq/builder/user_interaction_manager.py +3 -0
  37. aiq/builder/workflow.py +23 -18
  38. aiq/builder/workflow_builder.py +421 -61
  39. aiq/cli/commands/info/list_mcp.py +103 -16
  40. aiq/cli/commands/sizing/__init__.py +14 -0
  41. aiq/cli/commands/sizing/calc.py +294 -0
  42. aiq/cli/commands/sizing/sizing.py +27 -0
  43. aiq/cli/commands/start.py +2 -1
  44. aiq/cli/entrypoint.py +2 -0
  45. aiq/cli/register_workflow.py +80 -0
  46. aiq/cli/type_registry.py +151 -30
  47. aiq/data_models/api_server.py +124 -12
  48. aiq/data_models/authentication.py +231 -0
  49. aiq/data_models/common.py +35 -7
  50. aiq/data_models/component.py +17 -9
  51. aiq/data_models/component_ref.py +33 -0
  52. aiq/data_models/config.py +60 -3
  53. aiq/data_models/dataset_handler.py +2 -1
  54. aiq/data_models/embedder.py +1 -0
  55. aiq/data_models/evaluate.py +23 -0
  56. aiq/data_models/function_dependencies.py +8 -0
  57. aiq/data_models/interactive.py +10 -1
  58. aiq/data_models/intermediate_step.py +38 -5
  59. aiq/data_models/its_strategy.py +30 -0
  60. aiq/data_models/llm.py +1 -0
  61. aiq/data_models/memory.py +1 -0
  62. aiq/data_models/object_store.py +44 -0
  63. aiq/data_models/profiler.py +1 -0
  64. aiq/data_models/retry_mixin.py +35 -0
  65. aiq/data_models/span.py +187 -0
  66. aiq/data_models/telemetry_exporter.py +2 -2
  67. aiq/embedder/nim_embedder.py +2 -1
  68. aiq/embedder/openai_embedder.py +2 -1
  69. aiq/eval/config.py +19 -1
  70. aiq/eval/dataset_handler/dataset_handler.py +87 -2
  71. aiq/eval/evaluate.py +208 -27
  72. aiq/eval/evaluator/base_evaluator.py +73 -0
  73. aiq/eval/evaluator/evaluator_model.py +1 -0
  74. aiq/eval/intermediate_step_adapter.py +11 -5
  75. aiq/eval/rag_evaluator/evaluate.py +55 -15
  76. aiq/eval/rag_evaluator/register.py +6 -1
  77. aiq/eval/remote_workflow.py +7 -2
  78. aiq/eval/runners/__init__.py +14 -0
  79. aiq/eval/runners/config.py +39 -0
  80. aiq/eval/runners/multi_eval_runner.py +54 -0
  81. aiq/eval/trajectory_evaluator/evaluate.py +22 -65
  82. aiq/eval/tunable_rag_evaluator/evaluate.py +150 -168
  83. aiq/eval/tunable_rag_evaluator/register.py +2 -0
  84. aiq/eval/usage_stats.py +41 -0
  85. aiq/eval/utils/output_uploader.py +10 -1
  86. aiq/eval/utils/weave_eval.py +184 -0
  87. aiq/experimental/__init__.py +0 -0
  88. aiq/experimental/decorators/__init__.py +0 -0
  89. aiq/experimental/decorators/experimental_warning_decorator.py +130 -0
  90. aiq/experimental/inference_time_scaling/__init__.py +0 -0
  91. aiq/experimental/inference_time_scaling/editing/__init__.py +0 -0
  92. aiq/experimental/inference_time_scaling/editing/iterative_plan_refinement_editor.py +147 -0
  93. aiq/experimental/inference_time_scaling/editing/llm_as_a_judge_editor.py +204 -0
  94. aiq/experimental/inference_time_scaling/editing/motivation_aware_summarization.py +107 -0
  95. aiq/experimental/inference_time_scaling/functions/__init__.py +0 -0
  96. aiq/experimental/inference_time_scaling/functions/execute_score_select_function.py +105 -0
  97. aiq/experimental/inference_time_scaling/functions/its_tool_orchestration_function.py +205 -0
  98. aiq/experimental/inference_time_scaling/functions/its_tool_wrapper_function.py +146 -0
  99. aiq/experimental/inference_time_scaling/functions/plan_select_execute_function.py +224 -0
  100. aiq/experimental/inference_time_scaling/models/__init__.py +0 -0
  101. aiq/experimental/inference_time_scaling/models/editor_config.py +132 -0
  102. aiq/experimental/inference_time_scaling/models/its_item.py +48 -0
  103. aiq/experimental/inference_time_scaling/models/scoring_config.py +112 -0
  104. aiq/experimental/inference_time_scaling/models/search_config.py +120 -0
  105. aiq/experimental/inference_time_scaling/models/selection_config.py +154 -0
  106. aiq/experimental/inference_time_scaling/models/stage_enums.py +43 -0
  107. aiq/experimental/inference_time_scaling/models/strategy_base.py +66 -0
  108. aiq/experimental/inference_time_scaling/models/tool_use_config.py +41 -0
  109. aiq/experimental/inference_time_scaling/register.py +36 -0
  110. aiq/experimental/inference_time_scaling/scoring/__init__.py +0 -0
  111. aiq/experimental/inference_time_scaling/scoring/llm_based_agent_scorer.py +168 -0
  112. aiq/experimental/inference_time_scaling/scoring/llm_based_plan_scorer.py +168 -0
  113. aiq/experimental/inference_time_scaling/scoring/motivation_aware_scorer.py +111 -0
  114. aiq/experimental/inference_time_scaling/search/__init__.py +0 -0
  115. aiq/experimental/inference_time_scaling/search/multi_llm_planner.py +128 -0
  116. aiq/experimental/inference_time_scaling/search/multi_query_retrieval_search.py +122 -0
  117. aiq/experimental/inference_time_scaling/search/single_shot_multi_plan_planner.py +128 -0
  118. aiq/experimental/inference_time_scaling/selection/__init__.py +0 -0
  119. aiq/experimental/inference_time_scaling/selection/best_of_n_selector.py +63 -0
  120. aiq/experimental/inference_time_scaling/selection/llm_based_agent_output_selector.py +131 -0
  121. aiq/experimental/inference_time_scaling/selection/llm_based_output_merging_selector.py +159 -0
  122. aiq/experimental/inference_time_scaling/selection/llm_based_plan_selector.py +128 -0
  123. aiq/experimental/inference_time_scaling/selection/threshold_selector.py +58 -0
  124. aiq/front_ends/console/authentication_flow_handler.py +233 -0
  125. aiq/front_ends/console/console_front_end_plugin.py +11 -2
  126. aiq/front_ends/fastapi/auth_flow_handlers/__init__.py +0 -0
  127. aiq/front_ends/fastapi/auth_flow_handlers/http_flow_handler.py +27 -0
  128. aiq/front_ends/fastapi/auth_flow_handlers/websocket_flow_handler.py +107 -0
  129. aiq/front_ends/fastapi/fastapi_front_end_config.py +93 -9
  130. aiq/front_ends/fastapi/fastapi_front_end_controller.py +68 -0
  131. aiq/front_ends/fastapi/fastapi_front_end_plugin.py +14 -1
  132. aiq/front_ends/fastapi/fastapi_front_end_plugin_worker.py +537 -52
  133. aiq/front_ends/fastapi/html_snippets/__init__.py +14 -0
  134. aiq/front_ends/fastapi/html_snippets/auth_code_grant_success.py +35 -0
  135. aiq/front_ends/fastapi/job_store.py +47 -25
  136. aiq/front_ends/fastapi/main.py +2 -0
  137. aiq/front_ends/fastapi/message_handler.py +108 -89
  138. aiq/front_ends/fastapi/step_adaptor.py +2 -1
  139. aiq/llm/aws_bedrock_llm.py +57 -0
  140. aiq/llm/nim_llm.py +2 -1
  141. aiq/llm/openai_llm.py +3 -2
  142. aiq/llm/register.py +1 -0
  143. aiq/meta/pypi.md +12 -12
  144. aiq/object_store/__init__.py +20 -0
  145. aiq/object_store/in_memory_object_store.py +74 -0
  146. aiq/object_store/interfaces.py +84 -0
  147. aiq/object_store/models.py +36 -0
  148. aiq/object_store/register.py +20 -0
  149. aiq/observability/__init__.py +14 -0
  150. aiq/observability/exporter/__init__.py +14 -0
  151. aiq/observability/exporter/base_exporter.py +449 -0
  152. aiq/observability/exporter/exporter.py +78 -0
  153. aiq/observability/exporter/file_exporter.py +33 -0
  154. aiq/observability/exporter/processing_exporter.py +269 -0
  155. aiq/observability/exporter/raw_exporter.py +52 -0
  156. aiq/observability/exporter/span_exporter.py +264 -0
  157. aiq/observability/exporter_manager.py +335 -0
  158. aiq/observability/mixin/__init__.py +14 -0
  159. aiq/observability/mixin/batch_config_mixin.py +26 -0
  160. aiq/observability/mixin/collector_config_mixin.py +23 -0
  161. aiq/observability/mixin/file_mixin.py +288 -0
  162. aiq/observability/mixin/file_mode.py +23 -0
  163. aiq/observability/mixin/resource_conflict_mixin.py +134 -0
  164. aiq/observability/mixin/serialize_mixin.py +61 -0
  165. aiq/observability/mixin/type_introspection_mixin.py +183 -0
  166. aiq/observability/processor/__init__.py +14 -0
  167. aiq/observability/processor/batching_processor.py +316 -0
  168. aiq/observability/processor/intermediate_step_serializer.py +28 -0
  169. aiq/observability/processor/processor.py +68 -0
  170. aiq/observability/register.py +36 -39
  171. aiq/observability/utils/__init__.py +14 -0
  172. aiq/observability/utils/dict_utils.py +236 -0
  173. aiq/observability/utils/time_utils.py +31 -0
  174. aiq/profiler/calc/__init__.py +14 -0
  175. aiq/profiler/calc/calc_runner.py +623 -0
  176. aiq/profiler/calc/calculations.py +288 -0
  177. aiq/profiler/calc/data_models.py +176 -0
  178. aiq/profiler/calc/plot.py +345 -0
  179. aiq/profiler/callbacks/langchain_callback_handler.py +22 -10
  180. aiq/profiler/data_models.py +24 -0
  181. aiq/profiler/inference_metrics_model.py +3 -0
  182. aiq/profiler/inference_optimization/bottleneck_analysis/nested_stack_analysis.py +8 -0
  183. aiq/profiler/inference_optimization/data_models.py +2 -2
  184. aiq/profiler/inference_optimization/llm_metrics.py +2 -2
  185. aiq/profiler/profile_runner.py +61 -21
  186. aiq/runtime/loader.py +9 -3
  187. aiq/runtime/runner.py +23 -9
  188. aiq/runtime/session.py +25 -7
  189. aiq/runtime/user_metadata.py +2 -3
  190. aiq/tool/chat_completion.py +74 -0
  191. aiq/tool/code_execution/README.md +152 -0
  192. aiq/tool/code_execution/code_sandbox.py +151 -72
  193. aiq/tool/code_execution/local_sandbox/.gitignore +1 -0
  194. aiq/tool/code_execution/local_sandbox/local_sandbox_server.py +139 -24
  195. aiq/tool/code_execution/local_sandbox/sandbox.requirements.txt +3 -1
  196. aiq/tool/code_execution/local_sandbox/start_local_sandbox.sh +27 -2
  197. aiq/tool/code_execution/register.py +7 -3
  198. aiq/tool/code_execution/test_code_execution_sandbox.py +414 -0
  199. aiq/tool/mcp/exceptions.py +142 -0
  200. aiq/tool/mcp/mcp_client.py +41 -6
  201. aiq/tool/mcp/mcp_tool.py +3 -2
  202. aiq/tool/register.py +1 -0
  203. aiq/tool/server_tools.py +6 -3
  204. aiq/utils/exception_handlers/automatic_retries.py +289 -0
  205. aiq/utils/exception_handlers/mcp.py +211 -0
  206. aiq/utils/io/model_processing.py +28 -0
  207. aiq/utils/log_utils.py +37 -0
  208. aiq/utils/string_utils.py +38 -0
  209. aiq/utils/type_converter.py +18 -2
  210. aiq/utils/type_utils.py +87 -0
  211. {aiqtoolkit-1.2.0.dev0.dist-info → aiqtoolkit-1.2.0rc2.dist-info}/METADATA +53 -21
  212. aiqtoolkit-1.2.0rc2.dist-info/RECORD +436 -0
  213. {aiqtoolkit-1.2.0.dev0.dist-info → aiqtoolkit-1.2.0rc2.dist-info}/WHEEL +1 -1
  214. {aiqtoolkit-1.2.0.dev0.dist-info → aiqtoolkit-1.2.0rc2.dist-info}/entry_points.txt +3 -0
  215. aiq/front_ends/fastapi/websocket.py +0 -148
  216. aiq/observability/async_otel_listener.py +0 -429
  217. aiqtoolkit-1.2.0.dev0.dist-info/RECORD +0 -316
  218. {aiqtoolkit-1.2.0.dev0.dist-info → aiqtoolkit-1.2.0rc2.dist-info}/licenses/LICENSE-3rd-party.txt +0 -0
  219. {aiqtoolkit-1.2.0.dev0.dist-info → aiqtoolkit-1.2.0rc2.dist-info}/licenses/LICENSE.md +0 -0
  220. {aiqtoolkit-1.2.0.dev0.dist-info → aiqtoolkit-1.2.0rc2.dist-info}/top_level.txt +0 -0
@@ -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
+ """
@@ -16,6 +16,7 @@
16
16
  import logging
17
17
  import os
18
18
  import shutil
19
+ import threading
19
20
  from datetime import UTC
20
21
  from datetime import datetime
21
22
  from datetime import timedelta
@@ -40,12 +41,13 @@ class JobStatus(str, Enum):
40
41
  class JobInfo(BaseModel):
41
42
  job_id: str
42
43
  status: JobStatus
43
- config_file: str
44
+ config_file: str | None
44
45
  error: str | None
45
46
  output_path: str | None
46
47
  created_at: datetime
47
48
  updated_at: datetime
48
49
  expiry_seconds: int
50
+ output: BaseModel | None = None
49
51
 
50
52
 
51
53
  class JobStore:
@@ -59,8 +61,12 @@ class JobStore:
59
61
 
60
62
  def __init__(self):
61
63
  self._jobs = {}
64
+ self._lock = threading.Lock() # Ensure thread safety for job operations
62
65
 
63
- def create_job(self, config_file: str, job_id: str | None = None, expiry_seconds: int = DEFAULT_EXPIRY) -> str:
66
+ def create_job(self,
67
+ config_file: str | None = None,
68
+ job_id: str | None = None,
69
+ expiry_seconds: int = DEFAULT_EXPIRY) -> str:
64
70
  if job_id is None:
65
71
  job_id = str(uuid4())
66
72
 
@@ -76,46 +82,62 @@ class JobStore:
76
82
  error=None,
77
83
  output_path=None,
78
84
  expiry_seconds=clamped_expiry)
79
- self._jobs[job_id] = job
85
+
86
+ with self._lock:
87
+ self._jobs[job_id] = job
88
+
80
89
  logger.info("Created new job %s with config %s", job_id, config_file)
81
90
  return job_id
82
91
 
83
- def update_status(self, job_id: str, status: str, error: str | None = None, output_path: str | None = None):
92
+ def update_status(self,
93
+ job_id: str,
94
+ status: str,
95
+ error: str | None = None,
96
+ output_path: str | None = None,
97
+ output: BaseModel | None = None):
84
98
  if job_id not in self._jobs:
85
99
  raise ValueError(f"Job {job_id} not found")
86
100
 
87
- job = self._jobs[job_id]
88
- job.status = status
89
- job.error = error
90
- job.output_path = output_path
91
- job.updated_at = datetime.now(UTC)
101
+ with self._lock:
102
+ job = self._jobs[job_id]
103
+ job.status = status
104
+ job.error = error
105
+ job.output_path = output_path
106
+ job.updated_at = datetime.now(UTC)
107
+ job.output = output
92
108
 
93
109
  def get_status(self, job_id: str) -> JobInfo | None:
94
- return self._jobs.get(job_id)
110
+ with self._lock:
111
+ return self._jobs.get(job_id)
95
112
 
96
113
  def list_jobs(self):
97
- return self._jobs
114
+ with self._lock:
115
+ return self._jobs
98
116
 
99
117
  def get_job(self, job_id: str) -> JobInfo | None:
100
118
  """Get a job by its ID."""
101
- return self._jobs.get(job_id)
119
+ with self._lock:
120
+ return self._jobs.get(job_id)
102
121
 
103
122
  def get_last_job(self) -> JobInfo | None:
104
123
  """Get the last created job."""
105
- if not self._jobs:
106
- logger.info("No jobs found in job store")
107
- return None
108
- last_job = max(self._jobs.values(), key=lambda job: job.created_at)
109
- logger.info("Retrieved last job %s created at %s", last_job.job_id, last_job.created_at)
110
- return last_job
124
+ with self._lock:
125
+ if not self._jobs:
126
+ logger.info("No jobs found in job store")
127
+ return None
128
+ last_job = max(self._jobs.values(), key=lambda job: job.created_at)
129
+ logger.info("Retrieved last job %s created at %s", last_job.job_id, last_job.created_at)
130
+ return last_job
111
131
 
112
132
  def get_jobs_by_status(self, status: str) -> list[JobInfo]:
113
133
  """Get all jobs with the specified status."""
114
- return [job for job in self._jobs.values() if job.status == status]
134
+ with self._lock:
135
+ return [job for job in self._jobs.values() if job.status == status]
115
136
 
116
137
  def get_all_jobs(self) -> list[JobInfo]:
117
138
  """Get all jobs in the store."""
118
- return list(self._jobs.values())
139
+ with self._lock:
140
+ return list(self._jobs.values())
119
141
 
120
142
  def get_expires_at(self, job: JobInfo) -> datetime | None:
121
143
  """Get the time for a job to expire."""
@@ -132,7 +154,8 @@ class JobStore:
132
154
  now = datetime.now(UTC)
133
155
 
134
156
  # Filter out active jobs
135
- finished_jobs = {job_id: job for job_id, job in self._jobs.items() if job.status not in self.ACTIVE_STATUS}
157
+ with self._lock:
158
+ finished_jobs = {job_id: job for job_id, job in self._jobs.items() if job.status not in self.ACTIVE_STATUS}
136
159
 
137
160
  # Sort finished jobs by updated_at descending
138
161
  sorted_finished = sorted(finished_jobs.items(), key=lambda item: item[1].updated_at, reverse=True)
@@ -155,7 +178,6 @@ class JobStore:
155
178
  elif os.path.isdir(job.output_path):
156
179
  shutil.rmtree(job.output_path)
157
180
 
158
- for job_id in expired_ids:
159
- # cleanup output dir if present
160
-
161
- del self._jobs[job_id]
181
+ with self._lock:
182
+ for job_id in expired_ids:
183
+ del self._jobs[job_id]
@@ -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
@@ -15,14 +15,19 @@
15
15
 
16
16
  import asyncio
17
17
  import logging
18
+ import typing
18
19
  import uuid
19
20
  from typing import Any
20
21
 
21
22
  from fastapi import WebSocket
22
23
  from pydantic import BaseModel
23
24
  from pydantic import ValidationError
24
- from starlette.endpoints import WebSocketEndpoint
25
+ from starlette.websockets import WebSocketDisconnect
25
26
 
27
+ from aiq.authentication.interfaces import FlowHandlerBase
28
+ from aiq.data_models.api_server import AIQChatResponse
29
+ from aiq.data_models.api_server import AIQResponsePayloadOutput
30
+ from aiq.data_models.api_server import AIQResponseSerializable
26
31
  from aiq.data_models.api_server import Error
27
32
  from aiq.data_models.api_server import ErrorTypes
28
33
  from aiq.data_models.api_server import SystemResponseContent
@@ -39,74 +44,72 @@ from aiq.data_models.interactive import HumanResponse
39
44
  from aiq.data_models.interactive import HumanResponseNotification
40
45
  from aiq.data_models.interactive import InteractionPrompt
41
46
  from aiq.front_ends.fastapi.message_validator import MessageValidator
47
+ from aiq.front_ends.fastapi.response_helpers import generate_streaming_response
48
+ from aiq.front_ends.fastapi.step_adaptor import StepAdaptor
49
+ from aiq.runtime.session import AIQSessionManager
42
50
 
43
51
  logger = logging.getLogger(__name__)
44
52
 
45
53
 
46
- class MessageHandler:
54
+ class WebSocketMessageHandler:
55
+
56
+ def __init__(self, socket: WebSocket, session_manager: AIQSessionManager, step_adaptor: StepAdaptor):
57
+ self._socket: WebSocket = socket
58
+ self._session_manager: AIQSessionManager = session_manager
59
+ self._step_adaptor: StepAdaptor = step_adaptor
47
60
 
48
- def __init__(self, websocket_reference: WebSocketEndpoint):
49
- self._websocket_reference: WebSocketEndpoint = websocket_reference
50
61
  self._message_validator: MessageValidator = MessageValidator()
51
- self._messages_queue: asyncio.Queue[dict[str, str]] = asyncio.Queue()
52
- self._out_going_messages_queue: asyncio.Queue[dict] = asyncio.Queue()
53
- self._process_messages_task: asyncio.Task | None = None
54
- self._process_out_going_messages_task: asyncio.Task = None
55
- self._background_task: asyncio.Task = None
62
+ self._running_workflow_task: asyncio.Task | None = None
56
63
  self._message_parent_id: str = "default_id"
57
64
  self._workflow_schema_type: str = None
58
- self._user_interaction_response: asyncio.Future[TextContent] = asyncio.Future()
65
+ self._user_interaction_response: asyncio.Future[HumanResponse] | None = None
59
66
 
60
- @property
61
- def messages_queue(self) -> asyncio.Queue[dict[str, str]]:
62
- return self._messages_queue
67
+ self._flow_handler: FlowHandlerBase | None = None
63
68
 
64
- @property
65
- def background_task(self) -> asyncio.Task:
66
- return self._background_task
69
+ def set_flow_handler(self, flow_handler: FlowHandlerBase) -> None:
70
+ self._flow_handler = flow_handler
67
71
 
68
- @property
69
- def process_messages_task(self) -> asyncio.Task | None:
70
- return self._process_messages_task
72
+ async def __aenter__(self) -> "WebSocketMessageHandler":
73
+ await self._socket.accept()
71
74
 
72
- @process_messages_task.setter
73
- def process_messages_task(self, process_messages_task) -> None:
74
- self._process_messages_task = process_messages_task
75
+ return self
75
76
 
76
- @property
77
- def process_out_going_messages_task(self) -> asyncio.Task:
78
- return self._process_out_going_messages_task
77
+ async def __aexit__(self, exc_type, exc_value, traceback) -> None:
79
78
 
80
- @process_out_going_messages_task.setter
81
- def process_out_going_messages_task(self, process_out_going_messages_task) -> None:
82
- self._process_out_going_messages_task = process_out_going_messages_task
79
+ # TODO: Handle the exit
80
+ pass
83
81
 
84
- async def process_messages(self) -> None:
82
+ async def run(self) -> None:
85
83
  """
86
84
  Processes received messages from websocket and routes them appropriately.
87
85
  """
88
86
  while True:
89
87
 
90
88
  try:
91
- message: dict[str, Any] = await self._messages_queue.get()
89
+
90
+ message: dict[str, Any] = await self._socket.receive_json()
92
91
 
93
92
  validated_message: BaseModel = await self._message_validator.validate_message(message)
94
93
 
94
+ # Received a request to start a workflow
95
95
  if (isinstance(validated_message, WebSocketUserMessage)):
96
- await self.process_user_message(validated_message)
96
+ await self.process_workflow_request(validated_message)
97
97
 
98
- if isinstance(
98
+ elif isinstance(
99
99
  validated_message,
100
100
  ( # noqa: E131
101
101
  WebSocketSystemResponseTokenMessage,
102
102
  WebSocketSystemIntermediateStepMessage,
103
103
  WebSocketSystemInteractionMessage)):
104
- await self._out_going_messages_queue.put(validated_message.model_dump())
104
+ # These messages are already handled by self.create_websocket_message(data_model=value, …)
105
+ # No further processing is needed here.
106
+ pass
105
107
 
106
- if (isinstance(validated_message, WebSocketUserInteractionResponseMessage)):
108
+ elif (isinstance(validated_message, WebSocketUserInteractionResponseMessage)):
107
109
  user_content = await self.process_user_message_content(validated_message)
108
110
  self._user_interaction_response.set_result(user_content)
109
- except (asyncio.CancelledError):
111
+ except (asyncio.CancelledError, WebSocketDisconnect):
112
+ # TODO: Handle the disconnect
110
113
  break
111
114
 
112
115
  return None
@@ -130,29 +133,32 @@ class MessageHandler:
130
133
 
131
134
  return None
132
135
 
133
- async def process_user_message(self, message_as_validated_type: WebSocketUserMessage) -> None:
136
+ async def process_workflow_request(self, user_message_as_validated_type: WebSocketUserMessage) -> None:
134
137
  """
135
138
  Process user messages and routes them appropriately.
136
139
 
137
- :param message_as_validated_type: A WebSocketUserMessage Data Model instance.
140
+ :param user_message_as_validated_type: A WebSocketUserMessage Data Model instance.
138
141
  """
139
142
 
140
143
  try:
141
- self._message_parent_id = message_as_validated_type.id
142
- self._workflow_schema_type = message_as_validated_type.schema_type
144
+ self._message_parent_id = user_message_as_validated_type.id
145
+ self._workflow_schema_type = user_message_as_validated_type.schema_type
146
+ conversation_id: str = user_message_as_validated_type.conversation_id
143
147
 
144
- content: BaseModel | None = await self.process_user_message_content(message_as_validated_type)
148
+ content: BaseModel | None = await self.process_user_message_content(user_message_as_validated_type)
145
149
 
146
150
  if content is None:
147
- raise ValueError(f"User message content could not be found: {message_as_validated_type}")
151
+ raise ValueError(f"User message content could not be found: {user_message_as_validated_type}")
148
152
 
149
- if isinstance(content, TextContent) and (self._background_task is None):
153
+ if isinstance(content, TextContent) and (self._running_workflow_task is None):
150
154
 
151
- await self._process_response()
152
- self._background_task = asyncio.create_task(
153
- self._websocket_reference.workflow_schema_type.get(self._workflow_schema_type)(
154
- content.text)).add_done_callback(
155
- lambda task: asyncio.create_task(self._on_process_stream_task_done(task)))
155
+ def _done_callback(task: asyncio.Task):
156
+ self._running_workflow_task = None
157
+
158
+ # await self._process_response()
159
+ self._running_workflow_task = asyncio.create_task(
160
+ self._run_workflow(content.text, conversation_id,
161
+ result_type=AIQChatResponse)).add_done_callback(_done_callback)
156
162
 
157
163
  except ValueError as e:
158
164
  logger.error("User message content not found: %s", str(e), exc_info=True)
@@ -220,60 +226,73 @@ class MessageHandler:
220
226
  content=Error(code=ErrorTypes.UNKNOWN_ERROR, message="default", details=str(e)))
221
227
 
222
228
  finally:
223
- await self._messages_queue.put(message.model_dump())
224
-
225
- async def _on_process_stream_task_done(self, task: asyncio.Task) -> None:
226
- await self.create_websocket_message(data_model=SystemResponseContent(),
227
- message_type=WebSocketMessageType.RESPONSE_MESSAGE,
228
- status=WebSocketMessageStatus.COMPLETE)
229
+ if (message is not None):
230
+ await self._socket.send_json(message.model_dump())
229
231
 
230
- return None
231
-
232
- async def process_out_going_messages(self, websocket: WebSocket) -> None:
232
+ async def human_interaction_callback(self, prompt: InteractionPrompt) -> HumanResponse:
233
233
  """
234
- Spawns out going message processing task.
234
+ Registered human interaction callback that processes human interactions and returns
235
+ responses from websocket connection.
235
236
 
236
- :param websocket: Websocket instance.
237
+ :param prompt: Incoming interaction content data model.
238
+ :return: A Text Content Base Pydantic model.
237
239
  """
238
- while True:
239
- try:
240
- out_going_message = await self._out_going_messages_queue.get()
241
- await self._websocket_reference.on_send(websocket, out_going_message)
242
240
 
243
- except (asyncio.CancelledError, ValidationError):
244
- break
241
+ # First create a future from the loop for the human response
242
+ human_response_future: asyncio.Future[HumanResponse] = asyncio.get_running_loop().create_future()
245
243
 
246
- return None
244
+ # Then add the future to the outstanding human prompts dictionary
245
+ self._user_interaction_response = human_response_future
246
+
247
+ try:
247
248
 
248
- async def _process_response(self):
249
- self._websocket_reference.process_response_event.set()
249
+ await self.create_websocket_message(data_model=prompt.content,
250
+ message_type=WebSocketMessageType.SYSTEM_INTERACTION_MESSAGE,
251
+ status=WebSocketMessageStatus.IN_PROGRESS)
250
252
 
251
- async def _pause_response(self):
252
- self._websocket_reference.process_response_event.clear()
253
+ if (isinstance(prompt.content, HumanPromptNotification)):
253
254
 
254
- async def __reset_user_interaction_response(self):
255
- self._user_interaction_response = asyncio.Future()
255
+ return HumanResponseNotification()
256
256
 
257
- async def human_interaction(self, prompt: InteractionPrompt) -> HumanResponse:
258
- """
259
- Registered human interaction callback that processes human interactions and returns
260
- responses from websocket connection.
257
+ # Wait for the human response future to complete
258
+ interaction_response: HumanResponse = await human_response_future
261
259
 
262
- :param prompt: Incoming interaction content data model.
263
- :return: A Text Content Base Pydantic model.
264
- """
265
- await self.create_websocket_message(data_model=prompt.content,
266
- message_type=WebSocketMessageType.SYSTEM_INTERACTION_MESSAGE,
267
- status=WebSocketMessageStatus.IN_PROGRESS)
260
+ interaction_response: HumanResponse = await self._message_validator.convert_text_content_to_human_response(
261
+ interaction_response, prompt.content)
268
262
 
269
- if (isinstance(prompt.content, HumanPromptNotification)):
270
- return HumanResponseNotification()
263
+ return interaction_response
271
264
 
272
- user_message_repsonse_content: TextContent = await self._user_interaction_response
273
- interaction_response: HumanResponse = await self._message_validator.convert_text_content_to_human_response(
274
- user_message_repsonse_content, prompt.content)
265
+ finally:
266
+ # Delete the future from the outstanding human prompts dictionary
267
+ self._user_interaction_response = None
268
+
269
+ async def _run_workflow(self,
270
+ payload: typing.Any,
271
+ conversation_id: str | None = None,
272
+ result_type: type | None = None,
273
+ output_type: type | None = None) -> None:
274
+
275
+ try:
276
+ async with self._session_manager.session(
277
+ conversation_id=conversation_id,
278
+ request=self._socket,
279
+ user_input_callback=self.human_interaction_callback,
280
+ user_authentication_callback=(self._flow_handler.authenticate
281
+ if self._flow_handler else None)) as session:
275
282
 
276
- await self.__reset_user_interaction_response()
277
- await self._process_response()
283
+ async for value in generate_streaming_response(payload,
284
+ session_manager=session,
285
+ streaming=True,
286
+ step_adaptor=self._step_adaptor,
287
+ result_type=result_type,
288
+ output_type=output_type):
278
289
 
279
- return interaction_response
290
+ if not isinstance(value, AIQResponseSerializable):
291
+ value = AIQResponsePayloadOutput(payload=value)
292
+
293
+ await self.create_websocket_message(data_model=value, status=WebSocketMessageStatus.IN_PROGRESS)
294
+
295
+ finally:
296
+ await self.create_websocket_message(data_model=SystemResponseContent(),
297
+ message_type=WebSocketMessageType.RESPONSE_MESSAGE,
298
+ status=WebSocketMessageStatus.COMPLETE)
@@ -291,7 +291,8 @@ class StepAdaptor:
291
291
 
292
292
  return event
293
293
 
294
- def process(self, step: IntermediateStep) -> AIQResponseSerializable | None:
294
+ def process(self, step: IntermediateStep) -> AIQResponseSerializable | None: # pylint: disable=R1710
295
+
295
296
  # Track the chunk
296
297
  self._history.append(step)
297
298
  payload = step.payload
@@ -0,0 +1,57 @@
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 pydantic import AliasChoices
17
+ from pydantic import ConfigDict
18
+ from pydantic import Field
19
+
20
+ from aiq.builder.builder import Builder
21
+ from aiq.builder.llm import LLMProviderInfo
22
+ from aiq.cli.register_workflow import register_llm_provider
23
+ from aiq.data_models.llm import LLMBaseConfig
24
+ from aiq.data_models.retry_mixin import RetryMixin
25
+
26
+
27
+ class AWSBedrockModelConfig(LLMBaseConfig, RetryMixin, name="aws_bedrock"):
28
+ """An AWS Bedrock llm provider to be used with an LLM client."""
29
+
30
+ model_config = ConfigDict(protected_namespaces=())
31
+
32
+ # Completion parameters
33
+ model_name: str = Field(validation_alias=AliasChoices("model_name", "model"),
34
+ serialization_alias="model",
35
+ description="The model name for the hosted AWS Bedrock.")
36
+ temperature: float = Field(default=0.0, ge=0.0, le=1.0, description="Sampling temperature in [0, 1].")
37
+ max_tokens: int | None = Field(default=1024,
38
+ gt=0,
39
+ description="Maximum number of tokens to generate."
40
+ "This field is ONLY required when using AWS Bedrock with Langchain.")
41
+ context_size: int | None = Field(default=1024,
42
+ gt=0,
43
+ description="Maximum number of tokens to generate."
44
+ "This field is ONLY required when using AWS Bedrock with LlamaIndex.")
45
+
46
+ # Client parameters
47
+ region_name: str | None = Field(default="None", description="AWS region to use.")
48
+ base_url: str | None = Field(
49
+ default=None, description="Bedrock endpoint to use. Needed if you don't want to default to us-east-1 endpoint.")
50
+ credentials_profile_name: str | None = Field(
51
+ default=None, description="The name of the profile in the ~/.aws/credentials or ~/.aws/config files.")
52
+
53
+
54
+ @register_llm_provider(config_type=AWSBedrockModelConfig)
55
+ async def aws_bedrock_model(llm_config: AWSBedrockModelConfig, builder: Builder):
56
+
57
+ yield LLMProviderInfo(config=llm_config, description="A AWS Bedrock model for use with an LLM client.")
aiq/llm/nim_llm.py CHANGED
@@ -22,9 +22,10 @@ from aiq.builder.builder import Builder
22
22
  from aiq.builder.llm import LLMProviderInfo
23
23
  from aiq.cli.register_workflow import register_llm_provider
24
24
  from aiq.data_models.llm import LLMBaseConfig
25
+ from aiq.data_models.retry_mixin import RetryMixin
25
26
 
26
27
 
27
- class NIMModelConfig(LLMBaseConfig, name="nim"):
28
+ class NIMModelConfig(LLMBaseConfig, RetryMixin, name="nim"):
28
29
  """An NVIDIA Inference Microservice (NIM) llm provider to be used with an LLM client."""
29
30
 
30
31
  model_config = ConfigDict(protected_namespaces=())
aiq/llm/openai_llm.py CHANGED
@@ -21,12 +21,13 @@ from aiq.builder.builder import Builder
21
21
  from aiq.builder.llm import LLMProviderInfo
22
22
  from aiq.cli.register_workflow import register_llm_provider
23
23
  from aiq.data_models.llm import LLMBaseConfig
24
+ from aiq.data_models.retry_mixin import RetryMixin
24
25
 
25
26
 
26
- class OpenAIModelConfig(LLMBaseConfig, name="openai"):
27
+ class OpenAIModelConfig(LLMBaseConfig, RetryMixin, name="openai"):
27
28
  """An OpenAI LLM provider to be used with an LLM client."""
28
29
 
29
- model_config = ConfigDict(protected_namespaces=())
30
+ model_config = ConfigDict(protected_namespaces=(), extra="allow")
30
31
 
31
32
  api_key: str | None = Field(default=None, description="OpenAI API key to interact with hosted model.")
32
33
  base_url: str | None = Field(default=None, description="Base url to the hosted model.")
aiq/llm/register.py CHANGED
@@ -18,5 +18,6 @@
18
18
  # isort:skip_file
19
19
 
20
20
  # Import any providers which need to be automatically registered here
21
+ from . import aws_bedrock_llm
21
22
  from . import nim_llm
22
23
  from . import openai_llm
aiq/meta/pypi.md CHANGED
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
15
15
  limitations under the License.
16
16
  -->
17
17
 
18
- ![NVIDIA Agent Intelligence Toolkit](https://media.githubusercontent.com/media/NVIDIA/AIQToolkit/refs/heads/main/docs/source/_static/aiqtoolkit_banner.png "AIQ toolkit banner image")
18
+ ![NVIDIA Agent Intelligence Toolkit](https://media.githubusercontent.com/media/NVIDIA/NeMo-Agent-Toolkit/refs/heads/main/docs/source/_static/aiqtoolkit_banner.png "AIQ toolkit banner image")
19
19
 
20
20
  # NVIDIA Agent Intelligence Toolkit
21
21
 
@@ -23,26 +23,26 @@ AIQ toolkit is a flexible library designed to seamlessly integrate your enterpri
23
23
 
24
24
  ## Key Features
25
25
 
26
- - [**Framework Agnostic:**](https://docs.nvidia.com/aiqtoolkit/v1.2.0-dev/extend/plugins.html) Works with any agentic framework, so you can use your current technology stack without replatforming.
27
- - [**Reusability:**](https://docs.nvidia.com/aiqtoolkit/v1.2.0-dev/extend/sharing-components.html) Every agent, tool, or workflow can be combined and repurposed, allowing developers to leverage existing work in new scenarios.
28
- - [**Rapid Development:**](https://docs.nvidia.com/aiqtoolkit/v1.2.0-dev/tutorials/index.html) Start with a pre-built agent, tool, or workflow, and customize it to your needs.
29
- - [**Profiling:**](https://docs.nvidia.com/aiqtoolkit/v1.2.0-dev/workflows/profiler.html) Profile entire workflows down to the tool and agent level, track input/output tokens and timings, and identify bottlenecks.
30
- - [**Observability:**](https://docs.nvidia.com/aiqtoolkit/v1.2.0-dev/workflows/observe/observe-workflow-with-phoenix.html) Monitor and debug your workflows with any OpenTelemetry-compatible observability tool, with examples using [Phoenix](https://docs.nvidia.com/aiqtoolkit/v1.2.0-dev/workflows/observe/observe-workflow-with-phoenix.html) and [W&B Weave](https://docs.nvidia.com/aiqtoolkit/v1.2.0-dev/workflows/observe/observe-workflow-with-weave.html).
31
- - [**Evaluation System:**](https://docs.nvidia.com/aiqtoolkit/v1.2.0-dev/workflows/evaluate.html) Validate and maintain accuracy of agentic workflows with built-in evaluation tools.
32
- - [**User Interface:**](https://docs.nvidia.com/aiqtoolkit/v1.2.0-dev/quick-start/launching-ui.html) Use the AIQ toolkit UI chat interface to interact with your agents, visualize output, and debug workflows.
33
- - [**MCP Compatibility**](https://docs.nvidia.com/aiqtoolkit/v1.2.0-dev/workflows/mcp/mcp-client.html) Compatible with Model Context Protocol (MCP), allowing tools served by MCP Servers to be used as AIQ toolkit functions.
26
+ - [**Framework Agnostic:**](https://docs.nvidia.com/aiqtoolkit/1.2.0/extend/plugins.html) Works with any agentic framework, so you can use your current technology stack without replatforming.
27
+ - [**Reusability:**](https://docs.nvidia.com/aiqtoolkit/1.2.0/extend/sharing-components.html) Every agent, tool, or workflow can be combined and repurposed, allowing developers to leverage existing work in new scenarios.
28
+ - [**Rapid Development:**](https://docs.nvidia.com/aiqtoolkit/1.2.0/tutorials/index.html) Start with a pre-built agent, tool, or workflow, and customize it to your needs.
29
+ - [**Profiling:**](https://docs.nvidia.com/aiqtoolkit/1.2.0/workflows/profiler.html) Profile entire workflows down to the tool and agent level, track input/output tokens and timings, and identify bottlenecks.
30
+ - [**Observability:**](https://docs.nvidia.com/aiqtoolkit/1.2.0/workflows/observe/observe-workflow-with-phoenix.html) Monitor and debug your workflows with any OpenTelemetry-compatible observability tool, with examples using [Phoenix](https://docs.nvidia.com/aiqtoolkit/1.2.0/workflows/observe/observe-workflow-with-phoenix.html) and [W&B Weave](https://docs.nvidia.com/aiqtoolkit/1.2.0/workflows/observe/observe-workflow-with-weave.html).
31
+ - [**Evaluation System:**](https://docs.nvidia.com/aiqtoolkit/1.2.0/workflows/evaluate.html) Validate and maintain accuracy of agentic workflows with built-in evaluation tools.
32
+ - [**User Interface:**](https://docs.nvidia.com/aiqtoolkit/1.2.0/quick-start/launching-ui.html) Use the AIQ toolkit UI chat interface to interact with your agents, visualize output, and debug workflows.
33
+ - [**MCP Compatibility**](https://docs.nvidia.com/aiqtoolkit/1.2.0/workflows/mcp/mcp-client.html) Compatible with Model Context Protocol (MCP), allowing tools served by MCP Servers to be used as AIQ toolkit functions.
34
34
 
35
35
  With AIQ toolkit, you can move quickly, experiment freely, and ensure reliability across all your agent-driven projects.
36
36
 
37
37
  ## Links
38
- * [Documentation](https://docs.nvidia.com/aiqtoolkit/v1.2.0-dev/index.html): Explore the full documentation for AIQ toolkit.
38
+ * [Documentation](https://docs.nvidia.com/aiqtoolkit/1.2.0/index.html): Explore the full documentation for AIQ toolkit.
39
39
 
40
40
  ## First time user?
41
- If this is your first time using AIQ toolkit, it is recommended to install the latest version from the [source repository](https://github.com/NVIDIA/AIQToolkit?tab=readme-ov-file#quick-start) on GitHub. This package is intended for users who are familiar with AIQ toolkit applications and need to add AIQ toolkit as a dependency to their project.
41
+ If this is your first time using AIQ toolkit, it is recommended to install the latest version from the [source repository](https://github.com/NVIDIA/NeMo-Agent-Toolkit?tab=readme-ov-file#quick-start) on GitHub. This package is intended for users who are familiar with AIQ toolkit applications and need to add AIQ toolkit as a dependency to their project.
42
42
 
43
43
  ## Feedback
44
44
 
45
- We would love to hear from you! Please file an issue on [GitHub](https://github.com/NVIDIA/AIQToolkit/issues) if you have any feedback or feature requests.
45
+ We would love to hear from you! Please file an issue on [GitHub](https://github.com/NVIDIA/NeMo-Agent-Toolkit/issues) if you have any feedback or feature requests.
46
46
 
47
47
  ## Acknowledgements
48
48