aiqtoolkit 1.1.0a20250429__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 (309) hide show
  1. aiq/agent/__init__.py +0 -0
  2. aiq/agent/base.py +76 -0
  3. aiq/agent/dual_node.py +67 -0
  4. aiq/agent/react_agent/__init__.py +0 -0
  5. aiq/agent/react_agent/agent.py +322 -0
  6. aiq/agent/react_agent/output_parser.py +104 -0
  7. aiq/agent/react_agent/prompt.py +46 -0
  8. aiq/agent/react_agent/register.py +148 -0
  9. aiq/agent/reasoning_agent/__init__.py +0 -0
  10. aiq/agent/reasoning_agent/reasoning_agent.py +224 -0
  11. aiq/agent/register.py +23 -0
  12. aiq/agent/rewoo_agent/__init__.py +0 -0
  13. aiq/agent/rewoo_agent/agent.py +410 -0
  14. aiq/agent/rewoo_agent/prompt.py +108 -0
  15. aiq/agent/rewoo_agent/register.py +158 -0
  16. aiq/agent/tool_calling_agent/__init__.py +0 -0
  17. aiq/agent/tool_calling_agent/agent.py +123 -0
  18. aiq/agent/tool_calling_agent/register.py +105 -0
  19. aiq/builder/__init__.py +0 -0
  20. aiq/builder/builder.py +223 -0
  21. aiq/builder/component_utils.py +303 -0
  22. aiq/builder/context.py +198 -0
  23. aiq/builder/embedder.py +24 -0
  24. aiq/builder/eval_builder.py +116 -0
  25. aiq/builder/evaluator.py +29 -0
  26. aiq/builder/framework_enum.py +24 -0
  27. aiq/builder/front_end.py +73 -0
  28. aiq/builder/function.py +297 -0
  29. aiq/builder/function_base.py +372 -0
  30. aiq/builder/function_info.py +627 -0
  31. aiq/builder/intermediate_step_manager.py +125 -0
  32. aiq/builder/llm.py +25 -0
  33. aiq/builder/retriever.py +25 -0
  34. aiq/builder/user_interaction_manager.py +71 -0
  35. aiq/builder/workflow.py +134 -0
  36. aiq/builder/workflow_builder.py +733 -0
  37. aiq/cli/__init__.py +14 -0
  38. aiq/cli/cli_utils/__init__.py +0 -0
  39. aiq/cli/cli_utils/config_override.py +233 -0
  40. aiq/cli/cli_utils/validation.py +37 -0
  41. aiq/cli/commands/__init__.py +0 -0
  42. aiq/cli/commands/configure/__init__.py +0 -0
  43. aiq/cli/commands/configure/channel/__init__.py +0 -0
  44. aiq/cli/commands/configure/channel/add.py +28 -0
  45. aiq/cli/commands/configure/channel/channel.py +34 -0
  46. aiq/cli/commands/configure/channel/remove.py +30 -0
  47. aiq/cli/commands/configure/channel/update.py +30 -0
  48. aiq/cli/commands/configure/configure.py +33 -0
  49. aiq/cli/commands/evaluate.py +139 -0
  50. aiq/cli/commands/info/__init__.py +14 -0
  51. aiq/cli/commands/info/info.py +37 -0
  52. aiq/cli/commands/info/list_channels.py +32 -0
  53. aiq/cli/commands/info/list_components.py +129 -0
  54. aiq/cli/commands/registry/__init__.py +14 -0
  55. aiq/cli/commands/registry/publish.py +88 -0
  56. aiq/cli/commands/registry/pull.py +118 -0
  57. aiq/cli/commands/registry/registry.py +36 -0
  58. aiq/cli/commands/registry/remove.py +108 -0
  59. aiq/cli/commands/registry/search.py +155 -0
  60. aiq/cli/commands/start.py +250 -0
  61. aiq/cli/commands/uninstall.py +83 -0
  62. aiq/cli/commands/validate.py +47 -0
  63. aiq/cli/commands/workflow/__init__.py +14 -0
  64. aiq/cli/commands/workflow/templates/__init__.py.j2 +0 -0
  65. aiq/cli/commands/workflow/templates/config.yml.j2 +16 -0
  66. aiq/cli/commands/workflow/templates/pyproject.toml.j2 +22 -0
  67. aiq/cli/commands/workflow/templates/register.py.j2 +5 -0
  68. aiq/cli/commands/workflow/templates/workflow.py.j2 +36 -0
  69. aiq/cli/commands/workflow/workflow.py +37 -0
  70. aiq/cli/commands/workflow/workflow_commands.py +307 -0
  71. aiq/cli/entrypoint.py +133 -0
  72. aiq/cli/main.py +44 -0
  73. aiq/cli/register_workflow.py +408 -0
  74. aiq/cli/type_registry.py +869 -0
  75. aiq/data_models/__init__.py +14 -0
  76. aiq/data_models/api_server.py +550 -0
  77. aiq/data_models/common.py +143 -0
  78. aiq/data_models/component.py +46 -0
  79. aiq/data_models/component_ref.py +135 -0
  80. aiq/data_models/config.py +349 -0
  81. aiq/data_models/dataset_handler.py +122 -0
  82. aiq/data_models/discovery_metadata.py +269 -0
  83. aiq/data_models/embedder.py +26 -0
  84. aiq/data_models/evaluate.py +101 -0
  85. aiq/data_models/evaluator.py +26 -0
  86. aiq/data_models/front_end.py +26 -0
  87. aiq/data_models/function.py +30 -0
  88. aiq/data_models/function_dependencies.py +64 -0
  89. aiq/data_models/interactive.py +237 -0
  90. aiq/data_models/intermediate_step.py +269 -0
  91. aiq/data_models/invocation_node.py +38 -0
  92. aiq/data_models/llm.py +26 -0
  93. aiq/data_models/logging.py +26 -0
  94. aiq/data_models/memory.py +26 -0
  95. aiq/data_models/profiler.py +53 -0
  96. aiq/data_models/registry_handler.py +26 -0
  97. aiq/data_models/retriever.py +30 -0
  98. aiq/data_models/step_adaptor.py +64 -0
  99. aiq/data_models/streaming.py +33 -0
  100. aiq/data_models/swe_bench_model.py +54 -0
  101. aiq/data_models/telemetry_exporter.py +26 -0
  102. aiq/embedder/__init__.py +0 -0
  103. aiq/embedder/langchain_client.py +41 -0
  104. aiq/embedder/nim_embedder.py +58 -0
  105. aiq/embedder/openai_embedder.py +42 -0
  106. aiq/embedder/register.py +24 -0
  107. aiq/eval/__init__.py +14 -0
  108. aiq/eval/config.py +42 -0
  109. aiq/eval/dataset_handler/__init__.py +0 -0
  110. aiq/eval/dataset_handler/dataset_downloader.py +106 -0
  111. aiq/eval/dataset_handler/dataset_filter.py +52 -0
  112. aiq/eval/dataset_handler/dataset_handler.py +164 -0
  113. aiq/eval/evaluate.py +322 -0
  114. aiq/eval/evaluator/__init__.py +14 -0
  115. aiq/eval/evaluator/evaluator_model.py +44 -0
  116. aiq/eval/intermediate_step_adapter.py +93 -0
  117. aiq/eval/rag_evaluator/__init__.py +0 -0
  118. aiq/eval/rag_evaluator/evaluate.py +138 -0
  119. aiq/eval/rag_evaluator/register.py +138 -0
  120. aiq/eval/register.py +22 -0
  121. aiq/eval/remote_workflow.py +128 -0
  122. aiq/eval/runtime_event_subscriber.py +52 -0
  123. aiq/eval/swe_bench_evaluator/__init__.py +0 -0
  124. aiq/eval/swe_bench_evaluator/evaluate.py +215 -0
  125. aiq/eval/swe_bench_evaluator/register.py +36 -0
  126. aiq/eval/trajectory_evaluator/__init__.py +0 -0
  127. aiq/eval/trajectory_evaluator/evaluate.py +118 -0
  128. aiq/eval/trajectory_evaluator/register.py +40 -0
  129. aiq/eval/utils/__init__.py +0 -0
  130. aiq/eval/utils/output_uploader.py +131 -0
  131. aiq/eval/utils/tqdm_position_registry.py +40 -0
  132. aiq/front_ends/__init__.py +14 -0
  133. aiq/front_ends/console/__init__.py +14 -0
  134. aiq/front_ends/console/console_front_end_config.py +32 -0
  135. aiq/front_ends/console/console_front_end_plugin.py +107 -0
  136. aiq/front_ends/console/register.py +25 -0
  137. aiq/front_ends/cron/__init__.py +14 -0
  138. aiq/front_ends/fastapi/__init__.py +14 -0
  139. aiq/front_ends/fastapi/fastapi_front_end_config.py +150 -0
  140. aiq/front_ends/fastapi/fastapi_front_end_plugin.py +103 -0
  141. aiq/front_ends/fastapi/fastapi_front_end_plugin_worker.py +574 -0
  142. aiq/front_ends/fastapi/intermediate_steps_subscriber.py +80 -0
  143. aiq/front_ends/fastapi/job_store.py +161 -0
  144. aiq/front_ends/fastapi/main.py +70 -0
  145. aiq/front_ends/fastapi/message_handler.py +279 -0
  146. aiq/front_ends/fastapi/message_validator.py +345 -0
  147. aiq/front_ends/fastapi/register.py +25 -0
  148. aiq/front_ends/fastapi/response_helpers.py +181 -0
  149. aiq/front_ends/fastapi/step_adaptor.py +315 -0
  150. aiq/front_ends/fastapi/websocket.py +148 -0
  151. aiq/front_ends/mcp/__init__.py +14 -0
  152. aiq/front_ends/mcp/mcp_front_end_config.py +32 -0
  153. aiq/front_ends/mcp/mcp_front_end_plugin.py +93 -0
  154. aiq/front_ends/mcp/register.py +27 -0
  155. aiq/front_ends/mcp/tool_converter.py +242 -0
  156. aiq/front_ends/register.py +22 -0
  157. aiq/front_ends/simple_base/__init__.py +14 -0
  158. aiq/front_ends/simple_base/simple_front_end_plugin_base.py +52 -0
  159. aiq/llm/__init__.py +0 -0
  160. aiq/llm/nim_llm.py +45 -0
  161. aiq/llm/openai_llm.py +45 -0
  162. aiq/llm/register.py +22 -0
  163. aiq/llm/utils/__init__.py +14 -0
  164. aiq/llm/utils/env_config_value.py +94 -0
  165. aiq/llm/utils/error.py +17 -0
  166. aiq/memory/__init__.py +20 -0
  167. aiq/memory/interfaces.py +183 -0
  168. aiq/memory/models.py +102 -0
  169. aiq/meta/module_to_distro.json +3 -0
  170. aiq/meta/pypi.md +59 -0
  171. aiq/observability/__init__.py +0 -0
  172. aiq/observability/async_otel_listener.py +270 -0
  173. aiq/observability/register.py +97 -0
  174. aiq/plugins/.namespace +1 -0
  175. aiq/profiler/__init__.py +0 -0
  176. aiq/profiler/callbacks/__init__.py +0 -0
  177. aiq/profiler/callbacks/agno_callback_handler.py +295 -0
  178. aiq/profiler/callbacks/base_callback_class.py +20 -0
  179. aiq/profiler/callbacks/langchain_callback_handler.py +278 -0
  180. aiq/profiler/callbacks/llama_index_callback_handler.py +205 -0
  181. aiq/profiler/callbacks/semantic_kernel_callback_handler.py +238 -0
  182. aiq/profiler/callbacks/token_usage_base_model.py +27 -0
  183. aiq/profiler/data_frame_row.py +51 -0
  184. aiq/profiler/decorators/__init__.py +0 -0
  185. aiq/profiler/decorators/framework_wrapper.py +131 -0
  186. aiq/profiler/decorators/function_tracking.py +254 -0
  187. aiq/profiler/forecasting/__init__.py +0 -0
  188. aiq/profiler/forecasting/config.py +18 -0
  189. aiq/profiler/forecasting/model_trainer.py +75 -0
  190. aiq/profiler/forecasting/models/__init__.py +22 -0
  191. aiq/profiler/forecasting/models/forecasting_base_model.py +40 -0
  192. aiq/profiler/forecasting/models/linear_model.py +196 -0
  193. aiq/profiler/forecasting/models/random_forest_regressor.py +268 -0
  194. aiq/profiler/inference_metrics_model.py +25 -0
  195. aiq/profiler/inference_optimization/__init__.py +0 -0
  196. aiq/profiler/inference_optimization/bottleneck_analysis/__init__.py +0 -0
  197. aiq/profiler/inference_optimization/bottleneck_analysis/nested_stack_analysis.py +452 -0
  198. aiq/profiler/inference_optimization/bottleneck_analysis/simple_stack_analysis.py +258 -0
  199. aiq/profiler/inference_optimization/data_models.py +386 -0
  200. aiq/profiler/inference_optimization/experimental/__init__.py +0 -0
  201. aiq/profiler/inference_optimization/experimental/concurrency_spike_analysis.py +468 -0
  202. aiq/profiler/inference_optimization/experimental/prefix_span_analysis.py +405 -0
  203. aiq/profiler/inference_optimization/llm_metrics.py +212 -0
  204. aiq/profiler/inference_optimization/prompt_caching.py +163 -0
  205. aiq/profiler/inference_optimization/token_uniqueness.py +107 -0
  206. aiq/profiler/inference_optimization/workflow_runtimes.py +72 -0
  207. aiq/profiler/intermediate_property_adapter.py +102 -0
  208. aiq/profiler/profile_runner.py +433 -0
  209. aiq/profiler/utils.py +184 -0
  210. aiq/registry_handlers/__init__.py +0 -0
  211. aiq/registry_handlers/local/__init__.py +0 -0
  212. aiq/registry_handlers/local/local_handler.py +176 -0
  213. aiq/registry_handlers/local/register_local.py +37 -0
  214. aiq/registry_handlers/metadata_factory.py +60 -0
  215. aiq/registry_handlers/package_utils.py +198 -0
  216. aiq/registry_handlers/pypi/__init__.py +0 -0
  217. aiq/registry_handlers/pypi/pypi_handler.py +251 -0
  218. aiq/registry_handlers/pypi/register_pypi.py +40 -0
  219. aiq/registry_handlers/register.py +21 -0
  220. aiq/registry_handlers/registry_handler_base.py +157 -0
  221. aiq/registry_handlers/rest/__init__.py +0 -0
  222. aiq/registry_handlers/rest/register_rest.py +56 -0
  223. aiq/registry_handlers/rest/rest_handler.py +237 -0
  224. aiq/registry_handlers/schemas/__init__.py +0 -0
  225. aiq/registry_handlers/schemas/headers.py +42 -0
  226. aiq/registry_handlers/schemas/package.py +68 -0
  227. aiq/registry_handlers/schemas/publish.py +63 -0
  228. aiq/registry_handlers/schemas/pull.py +81 -0
  229. aiq/registry_handlers/schemas/remove.py +36 -0
  230. aiq/registry_handlers/schemas/search.py +91 -0
  231. aiq/registry_handlers/schemas/status.py +47 -0
  232. aiq/retriever/__init__.py +0 -0
  233. aiq/retriever/interface.py +37 -0
  234. aiq/retriever/milvus/__init__.py +14 -0
  235. aiq/retriever/milvus/register.py +81 -0
  236. aiq/retriever/milvus/retriever.py +228 -0
  237. aiq/retriever/models.py +74 -0
  238. aiq/retriever/nemo_retriever/__init__.py +14 -0
  239. aiq/retriever/nemo_retriever/register.py +60 -0
  240. aiq/retriever/nemo_retriever/retriever.py +190 -0
  241. aiq/retriever/register.py +22 -0
  242. aiq/runtime/__init__.py +14 -0
  243. aiq/runtime/loader.py +188 -0
  244. aiq/runtime/runner.py +176 -0
  245. aiq/runtime/session.py +116 -0
  246. aiq/settings/__init__.py +0 -0
  247. aiq/settings/global_settings.py +318 -0
  248. aiq/test/.namespace +1 -0
  249. aiq/tool/__init__.py +0 -0
  250. aiq/tool/code_execution/__init__.py +0 -0
  251. aiq/tool/code_execution/code_sandbox.py +188 -0
  252. aiq/tool/code_execution/local_sandbox/Dockerfile.sandbox +60 -0
  253. aiq/tool/code_execution/local_sandbox/__init__.py +13 -0
  254. aiq/tool/code_execution/local_sandbox/local_sandbox_server.py +79 -0
  255. aiq/tool/code_execution/local_sandbox/sandbox.requirements.txt +4 -0
  256. aiq/tool/code_execution/local_sandbox/start_local_sandbox.sh +25 -0
  257. aiq/tool/code_execution/register.py +70 -0
  258. aiq/tool/code_execution/utils.py +100 -0
  259. aiq/tool/datetime_tools.py +42 -0
  260. aiq/tool/document_search.py +141 -0
  261. aiq/tool/github_tools/__init__.py +0 -0
  262. aiq/tool/github_tools/create_github_commit.py +133 -0
  263. aiq/tool/github_tools/create_github_issue.py +87 -0
  264. aiq/tool/github_tools/create_github_pr.py +106 -0
  265. aiq/tool/github_tools/get_github_file.py +106 -0
  266. aiq/tool/github_tools/get_github_issue.py +166 -0
  267. aiq/tool/github_tools/get_github_pr.py +256 -0
  268. aiq/tool/github_tools/update_github_issue.py +100 -0
  269. aiq/tool/mcp/__init__.py +14 -0
  270. aiq/tool/mcp/mcp_client.py +220 -0
  271. aiq/tool/mcp/mcp_tool.py +75 -0
  272. aiq/tool/memory_tools/__init__.py +0 -0
  273. aiq/tool/memory_tools/add_memory_tool.py +67 -0
  274. aiq/tool/memory_tools/delete_memory_tool.py +67 -0
  275. aiq/tool/memory_tools/get_memory_tool.py +72 -0
  276. aiq/tool/nvidia_rag.py +95 -0
  277. aiq/tool/register.py +36 -0
  278. aiq/tool/retriever.py +89 -0
  279. aiq/utils/__init__.py +0 -0
  280. aiq/utils/data_models/__init__.py +0 -0
  281. aiq/utils/data_models/schema_validator.py +58 -0
  282. aiq/utils/debugging_utils.py +43 -0
  283. aiq/utils/exception_handlers/__init__.py +0 -0
  284. aiq/utils/exception_handlers/schemas.py +114 -0
  285. aiq/utils/io/__init__.py +0 -0
  286. aiq/utils/io/yaml_tools.py +50 -0
  287. aiq/utils/metadata_utils.py +74 -0
  288. aiq/utils/producer_consumer_queue.py +178 -0
  289. aiq/utils/reactive/__init__.py +0 -0
  290. aiq/utils/reactive/base/__init__.py +0 -0
  291. aiq/utils/reactive/base/observable_base.py +65 -0
  292. aiq/utils/reactive/base/observer_base.py +55 -0
  293. aiq/utils/reactive/base/subject_base.py +79 -0
  294. aiq/utils/reactive/observable.py +59 -0
  295. aiq/utils/reactive/observer.py +76 -0
  296. aiq/utils/reactive/subject.py +131 -0
  297. aiq/utils/reactive/subscription.py +49 -0
  298. aiq/utils/settings/__init__.py +0 -0
  299. aiq/utils/settings/global_settings.py +197 -0
  300. aiq/utils/type_converter.py +232 -0
  301. aiq/utils/type_utils.py +397 -0
  302. aiq/utils/url_utils.py +27 -0
  303. aiqtoolkit-1.1.0a20250429.dist-info/METADATA +326 -0
  304. aiqtoolkit-1.1.0a20250429.dist-info/RECORD +309 -0
  305. aiqtoolkit-1.1.0a20250429.dist-info/WHEEL +5 -0
  306. aiqtoolkit-1.1.0a20250429.dist-info/entry_points.txt +17 -0
  307. aiqtoolkit-1.1.0a20250429.dist-info/licenses/LICENSE-3rd-party.txt +3686 -0
  308. aiqtoolkit-1.1.0a20250429.dist-info/licenses/LICENSE.md +201 -0
  309. aiqtoolkit-1.1.0a20250429.dist-info/top_level.txt +1 -0
@@ -0,0 +1,131 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ import threading
17
+ from collections.abc import Callable
18
+ from typing import TypeVar
19
+
20
+ from aiq.utils.reactive.base.subject_base import SubjectBase
21
+ from aiq.utils.reactive.observable import Observable
22
+ from aiq.utils.reactive.observer import Observer
23
+ from aiq.utils.reactive.subscription import Subscription
24
+
25
+ T = TypeVar("T")
26
+
27
+ OnNext = Callable[[T], None]
28
+ OnError = Callable[[Exception], None]
29
+ OnComplete = Callable[[], None]
30
+
31
+
32
+ class Subject(Observable[T], Observer[T], SubjectBase[T]):
33
+ """
34
+ A Subject is both an Observer (receives events) and an Observable (sends events).
35
+ - Maintains a list of ObserverBase[T].
36
+ - No internal buffering or replay; events are only delivered to current subscribers.
37
+ - Thread-safe via a lock.
38
+
39
+ Once on_error or on_complete is called, the Subject is closed.
40
+ """
41
+
42
+ def __init__(self) -> None:
43
+ super().__init__()
44
+ self._lock = threading.RLock()
45
+ self._closed = False
46
+ self._error: Exception | None = None
47
+ self._observers: list[Observer[T]] = []
48
+ self._disposed = False
49
+
50
+ # ==========================================================================
51
+ # Observable[T] - for consumers
52
+ # ==========================================================================
53
+ def _subscribe_core(self, observer: Observer[T]) -> Subscription:
54
+ """
55
+ Subscribe to this subject. If disposed, returns a dummy subscription.
56
+ Otherwise, registers the given observer.
57
+ """
58
+ with self._lock:
59
+ if self._disposed:
60
+ # Already disposed => no subscription
61
+ return Subscription(self, None)
62
+
63
+ self._observers.append(observer)
64
+ return Subscription(self, observer)
65
+
66
+ # ==========================================================================
67
+ # ObserverBase[T] - for producers
68
+ # ==========================================================================
69
+ def on_next(self, value: T) -> None:
70
+ """
71
+ Called by producers to emit an item. Delivers synchronously to each observer.
72
+ If closed or disposed, do nothing.
73
+ """
74
+ with self._lock:
75
+ if self._closed or self._disposed:
76
+ return
77
+ # Copy the current observers to avoid mutation issues
78
+ current_observers = list(self._observers)
79
+
80
+ # Deliver outside the lock
81
+ for obs in current_observers:
82
+ obs.on_next(value)
83
+
84
+ def on_error(self, exc: Exception) -> None:
85
+ """
86
+ Called by producers to signal an error. Notifies all observers.
87
+ """
88
+ with self._lock:
89
+ if self._closed or self._disposed:
90
+ return
91
+ current_obs = list(self._observers)
92
+
93
+ for obs in current_obs:
94
+ obs.on_error(exc)
95
+
96
+ def on_complete(self) -> None:
97
+ """
98
+ Called by producers to signal completion. Notifies all observers, then
99
+ clears them. Subject is closed.
100
+ """
101
+ with self._lock:
102
+ if self._closed or self._disposed:
103
+ return
104
+ current_observers = list(self._observers)
105
+ self.dispose()
106
+
107
+ for obs in current_observers:
108
+ obs.on_complete()
109
+
110
+ # ==========================================================================
111
+ # SubjectBase - internal unsubscribing
112
+ # ==========================================================================
113
+ def _unsubscribe_observer(self, observer: Observer[T]) -> None:
114
+ with self._lock:
115
+ if not self._disposed and observer in self._observers:
116
+ self._observers.remove(observer)
117
+
118
+ # ==========================================================================
119
+ # Disposal
120
+ # ==========================================================================
121
+ def dispose(self) -> None:
122
+ """
123
+ Immediately close the Subject. No future on_next, on_error, or on_complete.
124
+ Clears all observers.
125
+ """
126
+ with self._lock:
127
+ if not self._disposed:
128
+ self._disposed = True
129
+ self._observers.clear()
130
+ self._closed = True
131
+ self._error = None
@@ -0,0 +1,49 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ import typing
17
+ from collections.abc import Callable
18
+ from typing import Generic
19
+ from typing import TypeVar
20
+
21
+ if typing.TYPE_CHECKING:
22
+ from aiq.utils.reactive.base.subject_base import SubjectBase
23
+
24
+ _T = TypeVar("_T") # pylint: disable=invalid-name
25
+
26
+ OnNext = Callable[[_T], None]
27
+ OnError = Callable[[Exception], None]
28
+ OnComplete = Callable[[], None]
29
+
30
+
31
+ class Subscription(Generic[_T]):
32
+ """
33
+ Represents a subscription to a Subject.
34
+ Unsubscribing removes the associated observer from the Subject's subscriber list.
35
+ """
36
+
37
+ def __init__(self, subject: "SubjectBase", observer: object | None): # noqa: F821
38
+ self._subject = subject
39
+ self._observer = observer
40
+ self._unsubscribed = False
41
+
42
+ def unsubscribe(self) -> None:
43
+ """
44
+ Stop receiving further events.
45
+ """
46
+ if not self._unsubscribed and self._observer is not None:
47
+ self._subject._unsubscribe_observer(self._observer)
48
+ self._observer = None
49
+ self._unsubscribed = True
File without changes
@@ -0,0 +1,197 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ import logging
17
+
18
+ from pydantic import create_model
19
+
20
+ from aiq.cli.type_registry import GlobalTypeRegistry
21
+ from aiq.data_models.registry_handler import RegistryHandlerBaseConfig
22
+ from aiq.settings.global_settings import GlobalSettings
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ def configure_registry_channel(config_type: RegistryHandlerBaseConfig, channel_name: str) -> None:
28
+ """Perform channel updates, gathering input from user and validatinig against the global settings data model.
29
+
30
+ Args:
31
+ config_type (RegistryHandlerBaseConfig): The registry handler configuration object to ensure valid channel
32
+ settings
33
+ channel_name (str): The name to use to reference the remote registry channel.
34
+ """
35
+
36
+ settings = GlobalSettings.get()
37
+
38
+ channel_registry_pre = {}
39
+
40
+ for field, info in config_type.model_fields.items():
41
+
42
+ if (field == "type"):
43
+ continue
44
+
45
+ while (True):
46
+ human_prompt = " ".join(field.title().split("_"))
47
+ user_input = input(f"{human_prompt}: ")
48
+ model_fields = {}
49
+ model_fields[field] = (info.annotation, ...)
50
+ DynamicFieldModel = create_model("DynamicFieldModel", **model_fields) # pylint: disable=C0103
51
+ dynamic_inputs = {field: user_input}
52
+
53
+ try:
54
+ validated_field_model = DynamicFieldModel(**dynamic_inputs)
55
+ channel_registry_pre[field] = getattr(validated_field_model, field)
56
+ break
57
+ except Exception as e:
58
+ logger.exception(e, exc_info=True)
59
+ logger.warning("Invalid '%s' input, input must be of type %s.", field, info.annotation)
60
+
61
+ validated_model = config_type(**channel_registry_pre)
62
+ settings_dict = settings.model_dump(serialize_as_any=True, by_alias=True)
63
+ settings_dict["channels"] = {**settings_dict["channels"], **{channel_name: validated_model}}
64
+
65
+ settings.update_settings(config_obj=settings_dict)
66
+
67
+
68
+ def add_channel_interative(channel_type: str) -> None:
69
+ """Add a remote registry channel to publish/search/pull AgentIQ plugin packages.
70
+
71
+ Args:
72
+ channel_type (str): They type of channel to configure.
73
+ """
74
+
75
+ settings = GlobalSettings.get()
76
+ registry = GlobalTypeRegistry.get()
77
+
78
+ try:
79
+ ChannelConfigType = registry.get_registered_channel_info_by_channel_type( # pylint: disable=C0103
80
+ channel_type=channel_type).config_type
81
+ except Exception as e:
82
+ logger.exception("Invalid channel type: %s", e, exc_info=True)
83
+ return
84
+
85
+ while (True):
86
+ channel_name = input("Channel Name: ").strip()
87
+ if len(channel_name) < 1:
88
+ logger.warning("Invalid channel name, cannot be empty or whitespace.")
89
+ if (channel_name in settings.channels):
90
+ logger.warning("Channel name '%s' already exists, choose a different name.", channel_name)
91
+ else:
92
+ settings.channels[channel_name] = {}
93
+ break
94
+
95
+ ChannelConfigType = registry.get_registered_channel_info_by_channel_type( # pylint: disable=C0103
96
+ channel_type=channel_type).config_type
97
+
98
+ configure_registry_channel(config_type=ChannelConfigType, channel_name=channel_name)
99
+
100
+
101
+ def get_existing_channel_interactive(channel_name: str) -> tuple[str, bool]:
102
+ """Retrieve an existing channel by configured name.
103
+
104
+ Args:
105
+ channel_name (str): The name to use to reference the remote registry channel.
106
+
107
+ Returns:
108
+ tuple[str, bool]: A tuple containing the retrieved channel name and a boolean representing a
109
+ valid match was or was not successful.
110
+ """
111
+
112
+ settings = GlobalSettings.get()
113
+ valid_channel = False
114
+ remote_channels = settings.channels
115
+
116
+ if (len(remote_channels) == 0):
117
+ logger.warning("No are configured channels to remove.")
118
+ return channel_name, valid_channel
119
+
120
+ while (not valid_channel):
121
+
122
+ if (channel_name not in remote_channels):
123
+ logger.warning("Channel name '%s' does not exist, choose a name from %s",
124
+ channel_name,
125
+ settings.channel_names)
126
+ channel_name = input("Channel Name: ").strip()
127
+ continue
128
+
129
+ valid_channel = True
130
+
131
+ return channel_name, valid_channel
132
+
133
+
134
+ def remove_channel(channel_name: str) -> None:
135
+ """Remove a configured registry channel from the global settings.
136
+
137
+ Args:
138
+ channel_name (str): The name to use to reference the remote registry channel.
139
+ """
140
+
141
+ settings = GlobalSettings.get()
142
+ settings_dict = settings.model_dump(serialize_as_any=True, by_alias=True).copy()
143
+ settings_dict["channels"].pop(channel_name)
144
+ settings.update_settings(config_obj=settings_dict)
145
+
146
+
147
+ def remove_channel_interactive(channel_name: str) -> None:
148
+ channel_name, valid_channel = get_existing_channel_interactive(channel_name=channel_name)
149
+ if (not valid_channel):
150
+ return
151
+ remove_channel(channel_name=channel_name)
152
+
153
+
154
+ def match_valid_channel(channel_name: str) -> None:
155
+ """Performs a match by registry channel to perform a channel configuration update.
156
+
157
+ Args:
158
+ channel_name (str): The name to use to reference the remote registry channel.
159
+ """
160
+
161
+ settings = GlobalSettings.get()
162
+ registry = GlobalTypeRegistry.get()
163
+
164
+ if len(settings.channel_names) == 0:
165
+ logger.warning("No channels have been configured, first add a channel.")
166
+ return
167
+
168
+ if (channel_name not in settings.channel_names):
169
+ logger.warning("Provided channel has not yet been configured, choose a different name "
170
+ "from %s .",
171
+ settings.channel_names)
172
+ while (True):
173
+ channel_name = input("Channel Name: ").strip()
174
+ if len(channel_name) < 1:
175
+ logger.warning("Invalid channel name, cannot be empty or whitespace.")
176
+ if (channel_name in settings.channel_names):
177
+ logger.warning("Channel name '%s' already exists, choose a different name.", channel_name)
178
+ else:
179
+ settings.channels[channel_name] = {}
180
+ break
181
+
182
+ channals_settings = settings.channels
183
+ channel_settings = channals_settings.get(channel_name)
184
+ ChannelConfigType = registry.get_registered_channel_info_by_channel_type( # pylint: disable=C0103
185
+ channel_type=channel_settings.static_type()).config_type
186
+
187
+ configure_registry_channel(config_type=ChannelConfigType, channel_name=channel_name)
188
+
189
+
190
+ def update_channel_interactive(channel_name: str):
191
+ """Launch an interactive session to update a configured channels settings.
192
+
193
+ Args:
194
+ channel_name (str): The name to use to reference the remote registry channel.
195
+ """
196
+
197
+ match_valid_channel(channel_name=channel_name)
@@ -0,0 +1,232 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ import logging
17
+ import typing
18
+ from collections import OrderedDict
19
+ from collections.abc import Callable
20
+ from io import TextIOWrapper
21
+
22
+ from aiq.utils.type_utils import DecomposedType
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ _T = typing.TypeVar("_T")
27
+
28
+
29
+ class ConvertException(Exception):
30
+ pass
31
+
32
+
33
+ class TypeConverter:
34
+ _global_initialized = False
35
+
36
+ def __init__(self, converters: list[Callable[[typing.Any], typing.Any]], parent: "TypeConverter | None" = None):
37
+ """
38
+ :param converters: A list of single-argument converter callables
39
+ annotated with their input param and return type.
40
+ :param parent: An optional parent TypeConverter for fallback.
41
+ """
42
+ # dict[to_type, dict[from_type, converter]]
43
+ self._converters: OrderedDict[type, OrderedDict[type, Callable]] = OrderedDict()
44
+ self._indirect_warnings_shown: set[tuple[type, type]] = set()
45
+
46
+ for converter in converters:
47
+ self.add_converter(converter)
48
+
49
+ if parent is None and TypeConverter._global_initialized:
50
+ parent = GlobalTypeConverter.get()
51
+ self._parent = parent
52
+
53
+ def add_converter(self, converter: Callable) -> None:
54
+ """
55
+ Registers a converter. Must have exactly one parameter
56
+ and an annotated return type.
57
+ """
58
+ sig = typing.get_type_hints(converter)
59
+ to_type = sig.pop("return", None)
60
+ if to_type is None:
61
+ raise ValueError("Converter must have a return type.")
62
+
63
+ if len(sig) != 1:
64
+ raise ValueError("Converter must have exactly one argument.")
65
+
66
+ from_type = next(iter(sig.values()))
67
+ if from_type is None:
68
+ raise ValueError("Converter's argument must have a data type.")
69
+
70
+ self._converters.setdefault(to_type, OrderedDict())[from_type] = converter
71
+ # to do(MDD): If needed, sort by specificity here.
72
+
73
+ def try_convert(self, data, to_type: type[_T]) -> _T | None:
74
+ """
75
+ Attempts to convert `data` into `to_type`. Returns None if no path is found.
76
+ """
77
+ decomposed = DecomposedType(to_type)
78
+
79
+ # 1) If data is already correct type, return it
80
+ if to_type is None or decomposed.is_instance((data, to_type)):
81
+ return data
82
+
83
+ root = decomposed.root
84
+
85
+ # 2) Attempt direct in *this* converter
86
+ direct_result = self._try_direct_conversion(data, root)
87
+ if direct_result is not None:
88
+ return direct_result
89
+
90
+ # 3) If direct fails entirely, do indirect in *this* converter
91
+ indirect_result = self._try_indirect_convert(data, to_type)
92
+ if indirect_result is not None:
93
+ return indirect_result
94
+
95
+ # 4) If we still haven't succeeded, return None
96
+ return None
97
+
98
+ def convert(self, data, to_type: type[_T]) -> _T:
99
+ """
100
+ Converts or raises ValueError if no path is found.
101
+ We also give the parent a chance if self fails.
102
+ """
103
+ result = self.try_convert(data, to_type)
104
+ if result is None and self._parent:
105
+ # fallback on parent entirely
106
+ return self._parent.convert(data, to_type)
107
+
108
+ if result is not None:
109
+ return result
110
+ raise ValueError(f"Cannot convert type {type(data)} to {to_type}. No match found.")
111
+
112
+ # -------------------------------------------------
113
+ # INTERNAL DIRECT CONVERSION (with parent fallback)
114
+ # -------------------------------------------------
115
+ def _try_direct_conversion(self, data, target_root_type: type) -> typing.Any | None:
116
+ """
117
+ Tries direct conversion in *this* converter's registry.
118
+ If no match here, we forward to parent's direct conversion
119
+ for recursion up the chain.
120
+ """
121
+ for convert_to_type, to_type_converters in self._converters.items():
122
+ # e.g. if Derived is a subclass of Base, this is valid
123
+ if issubclass(DecomposedType(convert_to_type).root, target_root_type):
124
+ for convert_from_type, from_type_converter in to_type_converters.items():
125
+ if isinstance(data, DecomposedType(convert_from_type).root):
126
+ try:
127
+ return from_type_converter(data)
128
+ except ConvertException:
129
+ pass
130
+
131
+ # If we can't convert directly here, try parent
132
+ if self._parent is not None:
133
+ return self._parent._try_direct_conversion(data, target_root_type)
134
+
135
+ return None
136
+
137
+ # -------------------------------------------------
138
+ # INTERNAL INDIRECT CONVERSION (with parent fallback)
139
+ # -------------------------------------------------
140
+ def _try_indirect_convert(self, data, to_type: type[_T]) -> _T | None:
141
+ """
142
+ Attempt indirect conversion (DFS) in *this* converter.
143
+ If no success, fallback to parent's indirect attempt.
144
+ """
145
+ visited = set()
146
+ final = self._try_indirect_conversion(data, to_type, visited)
147
+ if final is not None:
148
+ # Warn once if found a chain
149
+ self._maybe_warn_indirect(type(data), to_type)
150
+ return final
151
+
152
+ # If no success, try parent's indirect
153
+ if self._parent is not None:
154
+ parent_final = self._parent._try_indirect_convert(data, to_type)
155
+ if parent_final is not None:
156
+ self._maybe_warn_indirect(type(data), to_type)
157
+ return parent_final
158
+
159
+ return None
160
+
161
+ def _try_indirect_conversion(self, data: typing.Any, to_type: type[_T], visited: set[type]) -> _T | None:
162
+ """
163
+ DFS attempt to find a chain of conversions from type(data) to to_type,
164
+ ignoring parent. If not found, returns None.
165
+ """
166
+ # 1) If data is already correct type
167
+ if isinstance(data, to_type):
168
+ return data
169
+
170
+ current_type = type(data)
171
+ if current_type in visited:
172
+ return None
173
+
174
+ visited.add(current_type)
175
+
176
+ # 2) Attempt each known converter from current_type -> ???, then recurse
177
+ for _, to_type_converters in self._converters.items():
178
+ for convert_from_type, from_type_converter in to_type_converters.items():
179
+ if isinstance(data, convert_from_type):
180
+ try:
181
+ next_data = from_type_converter(data)
182
+ if isinstance(next_data, to_type):
183
+ return next_data
184
+ # else keep going
185
+ deeper = self._try_indirect_conversion(next_data, to_type, visited)
186
+ if deeper is not None:
187
+ return deeper
188
+ except ConvertException:
189
+ pass
190
+
191
+ return None
192
+
193
+ def _maybe_warn_indirect(self, source_type: type, to_type: type):
194
+ """
195
+ Warn once if an indirect path was used between these two types.
196
+ """
197
+ pair = (source_type, to_type)
198
+ if pair not in self._indirect_warnings_shown:
199
+ logger.warning(
200
+ "Indirect type conversion used to convert %s to %s, which may lead to unintended conversions. "
201
+ "Consider adding a direct converter from %s to %s to ensure correctness.",
202
+ source_type,
203
+ to_type,
204
+ source_type,
205
+ to_type)
206
+ self._indirect_warnings_shown.add(pair)
207
+
208
+
209
+ class GlobalTypeConverter:
210
+ _global_converter: TypeConverter = TypeConverter([])
211
+
212
+ @staticmethod
213
+ def get() -> TypeConverter:
214
+ return GlobalTypeConverter._global_converter
215
+
216
+ @staticmethod
217
+ def register_converter(converter: Callable) -> None:
218
+ GlobalTypeConverter._global_converter.add_converter(converter)
219
+
220
+ @staticmethod
221
+ def convert(data, to_type: type[_T]) -> _T:
222
+ return GlobalTypeConverter._global_converter.convert(data, to_type)
223
+
224
+
225
+ TypeConverter._global_initialized = True
226
+
227
+
228
+ def _text_io_wrapper_to_string(data: TextIOWrapper) -> str:
229
+ return data.read()
230
+
231
+
232
+ GlobalTypeConverter.register_converter(_text_io_wrapper_to_string)