aiqtoolkit 1.2.0a20250707__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.0a20250707.dist-info → aiqtoolkit-1.2.0a20250730.dist-info}/METADATA +37 -9
  190. {aiqtoolkit-1.2.0a20250707.dist-info → aiqtoolkit-1.2.0a20250730.dist-info}/RECORD +195 -80
  191. {aiqtoolkit-1.2.0a20250707.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.0a20250707.dist-info → aiqtoolkit-1.2.0a20250730.dist-info}/WHEEL +0 -0
  195. {aiqtoolkit-1.2.0a20250707.dist-info → aiqtoolkit-1.2.0a20250730.dist-info}/licenses/LICENSE-3rd-party.txt +0 -0
  196. {aiqtoolkit-1.2.0a20250707.dist-info → aiqtoolkit-1.2.0a20250730.dist-info}/licenses/LICENSE.md +0 -0
  197. {aiqtoolkit-1.2.0a20250707.dist-info → aiqtoolkit-1.2.0a20250730.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,414 @@
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
+ Test suite for Code Execution Sandbox using pytest.
17
+
18
+ This module provides comprehensive testing for the code execution sandbox service,
19
+ replacing the original bash script with a more maintainable Python implementation.
20
+ """
21
+
22
+ import os
23
+ from typing import Any
24
+
25
+ import pytest
26
+ import requests
27
+ from requests.exceptions import ConnectionError
28
+ from requests.exceptions import RequestException
29
+ from requests.exceptions import Timeout
30
+
31
+
32
+ class TestCodeExecutionSandbox:
33
+ """Test suite for the Code Execution Sandbox service."""
34
+
35
+ @pytest.fixture(scope="class")
36
+ def sandbox_config(self):
37
+ """Configuration for sandbox testing."""
38
+ return {
39
+ "url": os.environ.get("SANDBOX_URL", "http://127.0.0.1:6000/execute"),
40
+ "timeout": int(os.environ.get("SANDBOX_TIMEOUT", "30")),
41
+ "connection_timeout": 5
42
+ }
43
+
44
+ @pytest.fixture(scope="class", autouse=True)
45
+ def check_sandbox_running(self, sandbox_config):
46
+ """Check if sandbox server is running before running tests."""
47
+ try:
48
+ _ = requests.get(sandbox_config["url"], timeout=sandbox_config["connection_timeout"])
49
+ print(f"✓ Sandbox server is running at {sandbox_config['url']}")
50
+ except (ConnectionError, Timeout, RequestException):
51
+ pytest.skip(
52
+ f"Sandbox server is not running at {sandbox_config['url']}. "
53
+ "Please start it with: cd src/aiq/tool/code_execution/local_sandbox && ./start_local_sandbox.sh")
54
+
55
+ def execute_code(self, sandbox_config: dict[str, Any], code: str, language: str = "python") -> dict[str, Any]:
56
+ """
57
+ Execute code in the sandbox and return the response.
58
+
59
+ Args:
60
+ sandbox_config: Configuration dictionary
61
+ code: Code to execute
62
+ language: Programming language (default: python)
63
+
64
+ Returns:
65
+ dictionary containing the response from the sandbox
66
+ """
67
+ payload = {"generated_code": code, "timeout": sandbox_config["timeout"], "language": language}
68
+
69
+ response = requests.post(
70
+ sandbox_config["url"],
71
+ json=payload,
72
+ timeout=sandbox_config["timeout"] + 5 # Add buffer to request timeout
73
+ )
74
+
75
+ # Ensure we got a response
76
+ response.raise_for_status()
77
+ return response.json()
78
+
79
+ def test_simple_print(self, sandbox_config):
80
+ """Test simple print statement execution."""
81
+ code = "print('Hello, World!')"
82
+ result = self.execute_code(sandbox_config, code)
83
+
84
+ assert result["process_status"] == "completed"
85
+ assert "Hello, World!" in result["stdout"]
86
+ assert result["stderr"] == ""
87
+
88
+ def test_basic_arithmetic(self, sandbox_config):
89
+ """Test basic arithmetic operations."""
90
+ code = """
91
+ result = 2 + 3
92
+ print(f'Result: {result}')
93
+ """
94
+ result = self.execute_code(sandbox_config, code)
95
+
96
+ assert result["process_status"] == "completed"
97
+ assert "Result: 5" in result["stdout"]
98
+ assert result["stderr"] == ""
99
+
100
+ def test_numpy_operations(self, sandbox_config):
101
+ """Test numpy dependency availability and operations."""
102
+ code = """
103
+ import numpy as np
104
+ arr = np.array([1, 2, 3, 4, 5])
105
+ print(f'Array: {arr}')
106
+ print(f'Mean: {np.mean(arr)}')
107
+ """
108
+ result = self.execute_code(sandbox_config, code)
109
+
110
+ assert result["process_status"] == "completed"
111
+ assert "Array: [1 2 3 4 5]" in result["stdout"]
112
+ assert "Mean: 3.0" in result["stdout"]
113
+ assert result["stderr"] == ""
114
+
115
+ def test_pandas_operations(self, sandbox_config):
116
+ """Test pandas dependency availability and operations."""
117
+ code = """
118
+ import pandas as pd
119
+ df = pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6]})
120
+ print(df)
121
+ print(f'Sum of column A: {df["A"].sum()}')
122
+ """
123
+ result = self.execute_code(sandbox_config, code)
124
+
125
+ assert result["process_status"] == "completed"
126
+ assert "Sum of column A: 6" in result["stdout"]
127
+ assert result["stderr"] == ""
128
+
129
+ def test_plotly_import(self, sandbox_config):
130
+ """Test plotly dependency availability."""
131
+ code = """
132
+ import plotly.graph_objects as go
133
+ print('Plotly imported successfully')
134
+ fig = go.Figure()
135
+ fig.add_trace(go.Scatter(x=[1, 2, 3], y=[4, 5, 6]))
136
+ print('Plot created successfully')
137
+ """
138
+ result = self.execute_code(sandbox_config, code)
139
+
140
+ assert result["process_status"] == "completed"
141
+ assert "Plotly imported successfully" in result["stdout"]
142
+ assert "Plot created successfully" in result["stdout"]
143
+ assert result["stderr"] == ""
144
+
145
+ def test_syntax_error_handling(self, sandbox_config):
146
+ """Test handling of syntax errors."""
147
+ code = """
148
+ print('Hello World'
149
+ # Missing closing parenthesis
150
+ """
151
+ result = self.execute_code(sandbox_config, code)
152
+
153
+ assert result["process_status"] == "error"
154
+ assert "SyntaxError" in result["stderr"] or "SyntaxError" in result["stdout"]
155
+
156
+ def test_runtime_error_handling(self, sandbox_config):
157
+ """Test handling of runtime errors."""
158
+ code = """
159
+ x = 1 / 0
160
+ print('This should not print')
161
+ """
162
+ result = self.execute_code(sandbox_config, code)
163
+
164
+ assert result["process_status"] == "error"
165
+ assert "ZeroDivisionError" in result["stderr"] or "ZeroDivisionError" in result["stdout"]
166
+
167
+ def test_import_error_handling(self, sandbox_config):
168
+ """Test handling of import errors."""
169
+ code = """
170
+ import nonexistent_module
171
+ print('This should not print')
172
+ """
173
+ result = self.execute_code(sandbox_config, code)
174
+
175
+ assert result["process_status"] == "error"
176
+ assert "ModuleNotFoundError" in result["stderr"] or "ImportError" in result["stderr"]
177
+
178
+ def test_mixed_output(self, sandbox_config):
179
+ """Test code that produces both stdout and stderr output."""
180
+ code = """
181
+ import sys
182
+ print('This goes to stdout')
183
+ print('This goes to stderr', file=sys.stderr)
184
+ print('Back to stdout')
185
+ """
186
+ result = self.execute_code(sandbox_config, code)
187
+
188
+ assert result["process_status"] == "completed"
189
+ assert "This goes to stdout" in result["stdout"]
190
+ assert "Back to stdout" in result["stdout"]
191
+ assert "This goes to stderr" in result["stderr"]
192
+
193
+ def test_long_running_code(self, sandbox_config):
194
+ """Test code that takes some time to execute but completes within timeout."""
195
+ code = """
196
+ import time
197
+ for i in range(3):
198
+ print(f'Iteration {i}')
199
+ time.sleep(0.5)
200
+ print('Completed')
201
+ """
202
+ result = self.execute_code(sandbox_config, code)
203
+
204
+ assert result["process_status"] == "completed"
205
+ assert "Iteration 0" in result["stdout"]
206
+ assert "Iteration 1" in result["stdout"]
207
+ assert "Iteration 2" in result["stdout"]
208
+ assert "Completed" in result["stdout"]
209
+ assert result["stderr"] == ""
210
+
211
+ def test_file_operations(self, sandbox_config):
212
+ """Test basic file operations in the sandbox."""
213
+ code = """
214
+ import os
215
+ print(f'Current directory: {os.getcwd()}')
216
+ with open('test_file.txt', 'w') as f:
217
+ f.write('Hello, World!')
218
+ with open('test_file.txt', 'r') as f:
219
+ content = f.read()
220
+ print(f'File content: {content}')
221
+ os.remove('test_file.txt')
222
+ print('File operations completed')
223
+ """
224
+ result = self.execute_code(sandbox_config, code)
225
+
226
+ assert result["process_status"] == "completed"
227
+ assert "File content: Hello, World!" in result["stdout"]
228
+ assert "File operations completed" in result["stdout"]
229
+ assert result["stderr"] == ""
230
+
231
+ def test_file_persistence_create(self, sandbox_config):
232
+ """Test file persistence - create various file types."""
233
+ code = """
234
+ import os
235
+ import pandas as pd
236
+ import numpy as np
237
+ print('Current directory:', os.getcwd())
238
+ print('Directory contents:', os.listdir('.'))
239
+
240
+ # Create a test file
241
+ with open('persistence_test.txt', 'w') as f:
242
+ f.write('Hello from sandbox persistence test!')
243
+
244
+ # Create a CSV file
245
+ df = pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6]})
246
+ df.to_csv('persistence_test.csv', index=False)
247
+
248
+ # Create a numpy array file
249
+ arr = np.array([1, 2, 3, 4, 5])
250
+ np.save('persistence_test.npy', arr)
251
+
252
+ print('Files created:')
253
+ for file in os.listdir('.'):
254
+ if 'persistence_test' in file:
255
+ print(' -', file)
256
+ """
257
+ result = self.execute_code(sandbox_config, code)
258
+
259
+ assert result["process_status"] == "completed"
260
+ assert "persistence_test.txt" in result["stdout"]
261
+ assert "persistence_test.csv" in result["stdout"]
262
+ assert "persistence_test.npy" in result["stdout"]
263
+ assert result["stderr"] == ""
264
+
265
+ def test_file_persistence_read(self, sandbox_config):
266
+ """Test file persistence - read back created files."""
267
+ code = """
268
+ import pandas as pd
269
+ import numpy as np
270
+
271
+ # Read back the files we created
272
+ print('=== Reading persistence_test.txt ===')
273
+ with open('persistence_test.txt', 'r') as f:
274
+ content = f.read()
275
+ print(f'Content: {content}')
276
+
277
+ print('\\n=== Reading persistence_test.csv ===')
278
+ df = pd.read_csv('persistence_test.csv')
279
+ print(df)
280
+ print(f'DataFrame shape: {df.shape}')
281
+
282
+ print('\\n=== Reading persistence_test.npy ===')
283
+ arr = np.load('persistence_test.npy')
284
+ print(f'Array: {arr}')
285
+ print(f'Array sum: {np.sum(arr)}')
286
+
287
+ print('\\n=== File persistence test PASSED! ===')
288
+ """
289
+ result = self.execute_code(sandbox_config, code)
290
+
291
+ assert result["process_status"] == "completed"
292
+ assert "Content: Hello from sandbox persistence test!" in result["stdout"]
293
+ assert "DataFrame shape: (3, 2)" in result["stdout"]
294
+ assert "Array: [1 2 3 4 5]" in result["stdout"]
295
+ assert "Array sum: 15" in result["stdout"]
296
+ assert "File persistence test PASSED!" in result["stdout"]
297
+ assert result["stderr"] == ""
298
+
299
+ def test_json_operations(self, sandbox_config):
300
+ """Test JSON file operations for persistence."""
301
+ code = """
302
+ import json
303
+ import os
304
+
305
+ # Create a complex JSON file
306
+ data = {
307
+ 'test_name': 'sandbox_persistence',
308
+ 'timestamp': '2024-07-03',
309
+ 'results': {
310
+ 'numpy_test': True,
311
+ 'pandas_test': True,
312
+ 'file_operations': True
313
+ },
314
+ 'metrics': [1.5, 2.3, 3.7, 4.1],
315
+ 'metadata': {
316
+ 'working_dir': os.getcwd(),
317
+ 'python_version': '3.x'
318
+ }
319
+ }
320
+
321
+ # Save JSON file
322
+ with open('persistence_test.json', 'w') as f:
323
+ json.dump(data, f, indent=2)
324
+
325
+ # Read it back
326
+ with open('persistence_test.json', 'r') as f:
327
+ loaded_data = json.load(f)
328
+
329
+ print('JSON file created and loaded successfully')
330
+ print(f'Test name: {loaded_data["test_name"]}')
331
+ print(f'Results count: {len(loaded_data["results"])}')
332
+ print(f'Metrics: {loaded_data["metrics"]}')
333
+ print('JSON persistence test completed!')
334
+ """
335
+ result = self.execute_code(sandbox_config, code)
336
+
337
+ assert result["process_status"] == "completed"
338
+ assert "JSON file created and loaded successfully" in result["stdout"]
339
+ assert "Test name: sandbox_persistence" in result["stdout"]
340
+ assert "Results count: 3" in result["stdout"]
341
+ assert "JSON persistence test completed!" in result["stdout"]
342
+ assert result["stderr"] == ""
343
+
344
+ def test_missing_generated_code_field(self, sandbox_config):
345
+ """Test request missing the generated_code field."""
346
+ payload = {"timeout": 10, "language": "python"}
347
+
348
+ response = requests.post(sandbox_config["url"], json=payload)
349
+
350
+ # Should return an error status code or error in response
351
+ assert response.status_code != 200 or "error" in response.json()
352
+
353
+ def test_missing_timeout_field(self, sandbox_config):
354
+ """Test request missing the timeout field."""
355
+ payload = {"generated_code": "print('test')", "language": "python"}
356
+
357
+ response = requests.post(sandbox_config["url"], json=payload)
358
+
359
+ # Should return error for missing timeout field
360
+ result = response.json()
361
+ assert response.status_code == 400 and result["process_status"] == "error"
362
+
363
+ def test_invalid_json(self, sandbox_config):
364
+ """Test request with invalid JSON."""
365
+ invalid_json = '{"generated_code": "print("test")", "timeout": 10}'
366
+
367
+ response = requests.post(sandbox_config["url"], data=invalid_json, headers={"Content-Type": "application/json"})
368
+
369
+ # Should return error for invalid JSON
370
+ assert response.status_code != 200
371
+
372
+ def test_non_json_request(self, sandbox_config):
373
+ """Test request with non-JSON content."""
374
+ response = requests.post(sandbox_config["url"], data="This is not JSON", headers={"Content-Type": "text/plain"})
375
+
376
+ # Should return error for non-JSON content
377
+ assert response.status_code != 200
378
+
379
+ def test_timeout_too_low(self, sandbox_config):
380
+ """Test request with timeout too low."""
381
+ code = """
382
+ import time
383
+ time.sleep(2.0)
384
+ """
385
+ payload = {"generated_code": code, "timeout": 1, "language": "python"}
386
+ response = requests.post(sandbox_config["url"], json=payload)
387
+ assert response.json()["process_status"] == "timeout"
388
+ assert response.status_code == 200
389
+
390
+
391
+ # Pytest configuration and fixtures for command-line options
392
+ def pytest_addoption(parser):
393
+ """Add custom command-line options for pytest."""
394
+ parser.addoption("--sandbox-url",
395
+ action="store",
396
+ default="http://127.0.0.1:6000/execute",
397
+ help="Sandbox URL for testing")
398
+ parser.addoption("--sandbox-timeout",
399
+ action="store",
400
+ type=int,
401
+ default=30,
402
+ help="Timeout in seconds for sandbox operations")
403
+
404
+
405
+ @pytest.fixture(scope="session", autouse=True)
406
+ def setup_environment(request):
407
+ """Setup environment variables from command-line options."""
408
+ os.environ["SANDBOX_URL"] = request.config.getoption("--sandbox-url", "http://127.0.0.1:6000/execute")
409
+ os.environ["SANDBOX_TIMEOUT"] = str(request.config.getoption("--sandbox-timeout", 30))
410
+
411
+
412
+ if __name__ == "__main__":
413
+ # Allow running as a script
414
+ pytest.main([__file__, "-v"])
@@ -0,0 +1,142 @@
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
+ from enum import Enum
17
+
18
+
19
+ class MCPErrorCategory(str, Enum):
20
+ """Categories of MCP errors for structured handling."""
21
+ CONNECTION = "connection"
22
+ TIMEOUT = "timeout"
23
+ SSL = "ssl"
24
+ AUTHENTICATION = "authentication"
25
+ TOOL_NOT_FOUND = "tool_not_found"
26
+ PROTOCOL = "protocol"
27
+ UNKNOWN = "unknown"
28
+
29
+
30
+ class MCPError(Exception):
31
+ """Base exception for MCP-related errors."""
32
+
33
+ def __init__(self,
34
+ message: str,
35
+ url: str,
36
+ category: MCPErrorCategory = MCPErrorCategory.UNKNOWN,
37
+ suggestions: list[str] | None = None,
38
+ original_exception: Exception | None = None):
39
+ super().__init__(message)
40
+ self.url = url
41
+ self.category = category
42
+ self.suggestions = suggestions or []
43
+ self.original_exception = original_exception
44
+
45
+
46
+ class MCPConnectionError(MCPError):
47
+ """Exception for MCP connection failures."""
48
+
49
+ def __init__(self, url: str, original_exception: Exception | None = None):
50
+ super().__init__(f"Unable to connect to MCP server at {url}",
51
+ url=url,
52
+ category=MCPErrorCategory.CONNECTION,
53
+ suggestions=[
54
+ "Please ensure the MCP server is running and accessible",
55
+ "Check if the URL and port are correct"
56
+ ],
57
+ original_exception=original_exception)
58
+
59
+
60
+ class MCPTimeoutError(MCPError):
61
+ """Exception for MCP timeout errors."""
62
+
63
+ def __init__(self, url: str, original_exception: Exception | None = None):
64
+ super().__init__(f"Connection timed out to MCP server at {url}",
65
+ url=url,
66
+ category=MCPErrorCategory.TIMEOUT,
67
+ suggestions=[
68
+ "The server may be overloaded or network is slow",
69
+ "Try again in a moment or check network connectivity"
70
+ ],
71
+ original_exception=original_exception)
72
+
73
+
74
+ class MCPSSLError(MCPError):
75
+ """Exception for MCP SSL/TLS errors."""
76
+
77
+ def __init__(self, url: str, original_exception: Exception | None = None):
78
+ super().__init__(f"SSL/TLS error connecting to {url}",
79
+ url=url,
80
+ category=MCPErrorCategory.SSL,
81
+ suggestions=[
82
+ "Check if the server requires HTTPS or has valid certificates",
83
+ "Try using HTTP instead of HTTPS if appropriate"
84
+ ],
85
+ original_exception=original_exception)
86
+
87
+
88
+ class MCPRequestError(MCPError):
89
+ """Exception for MCP request errors."""
90
+
91
+ def __init__(self, url: str, original_exception: Exception | None = None):
92
+ message = f"Request failed to MCP server at {url}"
93
+ if original_exception:
94
+ message += f": {original_exception}"
95
+
96
+ super().__init__(message,
97
+ url=url,
98
+ category=MCPErrorCategory.PROTOCOL,
99
+ suggestions=["Check the server URL format and network settings"],
100
+ original_exception=original_exception)
101
+
102
+
103
+ class MCPToolNotFoundError(MCPError):
104
+ """Exception for when a specific MCP tool is not found."""
105
+
106
+ def __init__(self, tool_name: str, url: str, original_exception: Exception | None = None):
107
+ super().__init__(f"Tool '{tool_name}' not available at {url}",
108
+ url=url,
109
+ category=MCPErrorCategory.TOOL_NOT_FOUND,
110
+ suggestions=[
111
+ "Use 'aiq info mcp --detail' to see available tools",
112
+ "Check that the tool name is spelled correctly"
113
+ ],
114
+ original_exception=original_exception)
115
+
116
+
117
+ class MCPAuthenticationError(MCPError):
118
+ """Exception for MCP authentication failures."""
119
+
120
+ def __init__(self, url: str, original_exception: Exception | None = None):
121
+ super().__init__(f"Authentication failed when connecting to MCP server at {url}",
122
+ url=url,
123
+ category=MCPErrorCategory.AUTHENTICATION,
124
+ suggestions=[
125
+ "Check if the server requires authentication credentials",
126
+ "Verify that your credentials are correct and not expired"
127
+ ],
128
+ original_exception=original_exception)
129
+
130
+
131
+ class MCPProtocolError(MCPError):
132
+ """Exception for MCP protocol-related errors."""
133
+
134
+ def __init__(self, url: str, message: str = "Protocol error", original_exception: Exception | None = None):
135
+ super().__init__(f"{message} (MCP server at {url})",
136
+ url=url,
137
+ category=MCPErrorCategory.PROTOCOL,
138
+ suggestions=[
139
+ "Check that the MCP server is running and accessible at this URL",
140
+ "Verify the server supports the expected MCP protocol version"
141
+ ],
142
+ original_exception=original_exception)
@@ -27,6 +27,9 @@ from pydantic import BaseModel
27
27
  from pydantic import Field
28
28
  from pydantic import create_model
29
29
 
30
+ from aiq.tool.mcp.exceptions import MCPToolNotFoundError
31
+ from aiq.utils.exception_handlers.mcp import mcp_exception_handler
32
+
30
33
  logger = logging.getLogger(__name__)
31
34
 
32
35
 
@@ -138,9 +141,16 @@ class MCPBuilder(MCPSSEClient):
138
141
  super().__init__(url)
139
142
  self._tools = None
140
143
 
144
+ @mcp_exception_handler
141
145
  async def get_tools(self):
142
146
  """
143
147
  Retrieve a dictionary of all tools served by the MCP server.
148
+
149
+ Returns:
150
+ Dict of tool name to MCPToolClient
151
+
152
+ Raises:
153
+ MCPError: If connection or tool retrieval fails
144
154
  """
145
155
  async with self.connect_to_sse_server() as session:
146
156
  response = await session.list_tools()
@@ -150,6 +160,7 @@ class MCPBuilder(MCPSSEClient):
150
160
  for tool in response.tools
151
161
  }
152
162
 
163
+ @mcp_exception_handler
153
164
  async def get_tool(self, tool_name: str) -> MCPToolClient:
154
165
  """
155
166
  Get an MCP Tool by name.
@@ -160,17 +171,19 @@ class MCPBuilder(MCPSSEClient):
160
171
  Returns:
161
172
  MCPToolClient for the configured tool.
162
173
 
163
- Raise:
164
- ValueError if no tool is available with that name.
174
+ Raises:
175
+ MCPToolNotFoundError: If no tool is available with that name
176
+ MCPError: If connection fails
165
177
  """
166
178
  if not self._tools:
167
179
  self._tools = await self.get_tools()
168
180
 
169
181
  tool = self._tools.get(tool_name)
170
182
  if not tool:
171
- raise ValueError(f"Tool {tool_name} not available at {self.url}")
183
+ raise MCPToolNotFoundError(tool_name, self.url)
172
184
  return tool
173
185
 
186
+ @mcp_exception_handler
174
187
  async def call_tool(self, tool_name: str, tool_args: dict | None):
175
188
  async with self.connect_to_sse_server() as session:
176
189
  result = await session.call_tool(tool_name, tool_args)
@@ -221,6 +234,7 @@ class MCPToolClient(MCPSSEClient):
221
234
  """
222
235
  self._tool_description = description
223
236
 
237
+ @mcp_exception_handler
224
238
  async def acall(self, tool_args: dict) -> str:
225
239
  """
226
240
  Call the MCP tool with the provided arguments.
aiq/tool/mcp/mcp_tool.py CHANGED
@@ -29,7 +29,7 @@ logger = logging.getLogger(__name__)
29
29
 
30
30
  class MCPToolConfig(FunctionBaseConfig, name="mcp_tool_wrapper"):
31
31
  """
32
- Function which connects to a Model Context Protocol (MCP) server and wraps the selected tool as an AIQ Toolkit
32
+ Function which connects to a Model Context Protocol (MCP) server and wraps the selected tool as a NeMo Agent toolkit
33
33
  function.
34
34
  """
35
35
  # Add your custom configuration parameters here
aiq/tool/register.py CHANGED
@@ -17,6 +17,7 @@
17
17
  # flake8: noqa
18
18
 
19
19
  # Import any tools which need to be automatically registered here
20
+ from . import chat_completion
20
21
  from . import datetime_tools
21
22
  from . import document_search
22
23
  from . import github_tools
aiq/tool/server_tools.py CHANGED
@@ -23,8 +23,8 @@ class RequestAttributesTool(FunctionBaseConfig, name="current_request_attributes
23
23
  """
24
24
  A simple tool that demonstrates how to retrieve user-defined request attributes from HTTP requests
25
25
  within workflow tools. Please refer to the 'general' section of the configuration file located in the
26
- 'examples/simple_calculator/configs/config-metadata.yml' directory to see how to define a custom route using a
27
- YAML file and associate it with a corresponding function to acquire request attributes.
26
+ 'examples/getting_started/simple_web_query/configs/config-metadata.yml' directory to see how to define a
27
+ custom route using a YAML file and associate it with a corresponding function to acquire request attributes.
28
28
  """
29
29
  pass
30
30