aiqtoolkit 1.1.0a20250503__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 (314) 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 +212 -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 +376 -0
  30. aiq/builder/function_info.py +627 -0
  31. aiq/builder/intermediate_step_manager.py +127 -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 +143 -0
  36. aiq/builder/workflow_builder.py +749 -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 +36 -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 +38 -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 +313 -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 +879 -0
  75. aiq/data_models/__init__.py +14 -0
  76. aiq/data_models/api_server.py +588 -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 +104 -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 +169 -0
  113. aiq/eval/evaluate.py +323 -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 +23 -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/tunable_rag_evaluator/__init__.py +0 -0
  130. aiq/eval/tunable_rag_evaluator/evaluate.py +263 -0
  131. aiq/eval/tunable_rag_evaluator/register.py +50 -0
  132. aiq/eval/utils/__init__.py +0 -0
  133. aiq/eval/utils/output_uploader.py +131 -0
  134. aiq/eval/utils/tqdm_position_registry.py +40 -0
  135. aiq/front_ends/__init__.py +14 -0
  136. aiq/front_ends/console/__init__.py +14 -0
  137. aiq/front_ends/console/console_front_end_config.py +32 -0
  138. aiq/front_ends/console/console_front_end_plugin.py +107 -0
  139. aiq/front_ends/console/register.py +25 -0
  140. aiq/front_ends/cron/__init__.py +14 -0
  141. aiq/front_ends/fastapi/__init__.py +14 -0
  142. aiq/front_ends/fastapi/fastapi_front_end_config.py +150 -0
  143. aiq/front_ends/fastapi/fastapi_front_end_plugin.py +103 -0
  144. aiq/front_ends/fastapi/fastapi_front_end_plugin_worker.py +607 -0
  145. aiq/front_ends/fastapi/intermediate_steps_subscriber.py +80 -0
  146. aiq/front_ends/fastapi/job_store.py +161 -0
  147. aiq/front_ends/fastapi/main.py +70 -0
  148. aiq/front_ends/fastapi/message_handler.py +279 -0
  149. aiq/front_ends/fastapi/message_validator.py +345 -0
  150. aiq/front_ends/fastapi/register.py +25 -0
  151. aiq/front_ends/fastapi/response_helpers.py +195 -0
  152. aiq/front_ends/fastapi/step_adaptor.py +315 -0
  153. aiq/front_ends/fastapi/websocket.py +148 -0
  154. aiq/front_ends/mcp/__init__.py +14 -0
  155. aiq/front_ends/mcp/mcp_front_end_config.py +32 -0
  156. aiq/front_ends/mcp/mcp_front_end_plugin.py +93 -0
  157. aiq/front_ends/mcp/register.py +27 -0
  158. aiq/front_ends/mcp/tool_converter.py +242 -0
  159. aiq/front_ends/register.py +22 -0
  160. aiq/front_ends/simple_base/__init__.py +14 -0
  161. aiq/front_ends/simple_base/simple_front_end_plugin_base.py +52 -0
  162. aiq/llm/__init__.py +0 -0
  163. aiq/llm/nim_llm.py +45 -0
  164. aiq/llm/openai_llm.py +45 -0
  165. aiq/llm/register.py +22 -0
  166. aiq/llm/utils/__init__.py +14 -0
  167. aiq/llm/utils/env_config_value.py +94 -0
  168. aiq/llm/utils/error.py +17 -0
  169. aiq/memory/__init__.py +20 -0
  170. aiq/memory/interfaces.py +183 -0
  171. aiq/memory/models.py +102 -0
  172. aiq/meta/module_to_distro.json +3 -0
  173. aiq/meta/pypi.md +59 -0
  174. aiq/observability/__init__.py +0 -0
  175. aiq/observability/async_otel_listener.py +433 -0
  176. aiq/observability/register.py +99 -0
  177. aiq/plugins/.namespace +1 -0
  178. aiq/profiler/__init__.py +0 -0
  179. aiq/profiler/callbacks/__init__.py +0 -0
  180. aiq/profiler/callbacks/agno_callback_handler.py +295 -0
  181. aiq/profiler/callbacks/base_callback_class.py +20 -0
  182. aiq/profiler/callbacks/langchain_callback_handler.py +278 -0
  183. aiq/profiler/callbacks/llama_index_callback_handler.py +205 -0
  184. aiq/profiler/callbacks/semantic_kernel_callback_handler.py +238 -0
  185. aiq/profiler/callbacks/token_usage_base_model.py +27 -0
  186. aiq/profiler/data_frame_row.py +51 -0
  187. aiq/profiler/decorators/__init__.py +0 -0
  188. aiq/profiler/decorators/framework_wrapper.py +131 -0
  189. aiq/profiler/decorators/function_tracking.py +254 -0
  190. aiq/profiler/forecasting/__init__.py +0 -0
  191. aiq/profiler/forecasting/config.py +18 -0
  192. aiq/profiler/forecasting/model_trainer.py +75 -0
  193. aiq/profiler/forecasting/models/__init__.py +22 -0
  194. aiq/profiler/forecasting/models/forecasting_base_model.py +40 -0
  195. aiq/profiler/forecasting/models/linear_model.py +196 -0
  196. aiq/profiler/forecasting/models/random_forest_regressor.py +268 -0
  197. aiq/profiler/inference_metrics_model.py +25 -0
  198. aiq/profiler/inference_optimization/__init__.py +0 -0
  199. aiq/profiler/inference_optimization/bottleneck_analysis/__init__.py +0 -0
  200. aiq/profiler/inference_optimization/bottleneck_analysis/nested_stack_analysis.py +452 -0
  201. aiq/profiler/inference_optimization/bottleneck_analysis/simple_stack_analysis.py +258 -0
  202. aiq/profiler/inference_optimization/data_models.py +386 -0
  203. aiq/profiler/inference_optimization/experimental/__init__.py +0 -0
  204. aiq/profiler/inference_optimization/experimental/concurrency_spike_analysis.py +468 -0
  205. aiq/profiler/inference_optimization/experimental/prefix_span_analysis.py +405 -0
  206. aiq/profiler/inference_optimization/llm_metrics.py +212 -0
  207. aiq/profiler/inference_optimization/prompt_caching.py +163 -0
  208. aiq/profiler/inference_optimization/token_uniqueness.py +107 -0
  209. aiq/profiler/inference_optimization/workflow_runtimes.py +72 -0
  210. aiq/profiler/intermediate_property_adapter.py +102 -0
  211. aiq/profiler/profile_runner.py +433 -0
  212. aiq/profiler/utils.py +184 -0
  213. aiq/registry_handlers/__init__.py +0 -0
  214. aiq/registry_handlers/local/__init__.py +0 -0
  215. aiq/registry_handlers/local/local_handler.py +176 -0
  216. aiq/registry_handlers/local/register_local.py +37 -0
  217. aiq/registry_handlers/metadata_factory.py +60 -0
  218. aiq/registry_handlers/package_utils.py +198 -0
  219. aiq/registry_handlers/pypi/__init__.py +0 -0
  220. aiq/registry_handlers/pypi/pypi_handler.py +251 -0
  221. aiq/registry_handlers/pypi/register_pypi.py +40 -0
  222. aiq/registry_handlers/register.py +21 -0
  223. aiq/registry_handlers/registry_handler_base.py +157 -0
  224. aiq/registry_handlers/rest/__init__.py +0 -0
  225. aiq/registry_handlers/rest/register_rest.py +56 -0
  226. aiq/registry_handlers/rest/rest_handler.py +237 -0
  227. aiq/registry_handlers/schemas/__init__.py +0 -0
  228. aiq/registry_handlers/schemas/headers.py +42 -0
  229. aiq/registry_handlers/schemas/package.py +68 -0
  230. aiq/registry_handlers/schemas/publish.py +63 -0
  231. aiq/registry_handlers/schemas/pull.py +82 -0
  232. aiq/registry_handlers/schemas/remove.py +36 -0
  233. aiq/registry_handlers/schemas/search.py +91 -0
  234. aiq/registry_handlers/schemas/status.py +47 -0
  235. aiq/retriever/__init__.py +0 -0
  236. aiq/retriever/interface.py +37 -0
  237. aiq/retriever/milvus/__init__.py +14 -0
  238. aiq/retriever/milvus/register.py +81 -0
  239. aiq/retriever/milvus/retriever.py +228 -0
  240. aiq/retriever/models.py +74 -0
  241. aiq/retriever/nemo_retriever/__init__.py +14 -0
  242. aiq/retriever/nemo_retriever/register.py +60 -0
  243. aiq/retriever/nemo_retriever/retriever.py +190 -0
  244. aiq/retriever/register.py +22 -0
  245. aiq/runtime/__init__.py +14 -0
  246. aiq/runtime/loader.py +188 -0
  247. aiq/runtime/runner.py +176 -0
  248. aiq/runtime/session.py +136 -0
  249. aiq/runtime/user_metadata.py +131 -0
  250. aiq/settings/__init__.py +0 -0
  251. aiq/settings/global_settings.py +318 -0
  252. aiq/test/.namespace +1 -0
  253. aiq/tool/__init__.py +0 -0
  254. aiq/tool/code_execution/__init__.py +0 -0
  255. aiq/tool/code_execution/code_sandbox.py +188 -0
  256. aiq/tool/code_execution/local_sandbox/Dockerfile.sandbox +60 -0
  257. aiq/tool/code_execution/local_sandbox/__init__.py +13 -0
  258. aiq/tool/code_execution/local_sandbox/local_sandbox_server.py +79 -0
  259. aiq/tool/code_execution/local_sandbox/sandbox.requirements.txt +4 -0
  260. aiq/tool/code_execution/local_sandbox/start_local_sandbox.sh +25 -0
  261. aiq/tool/code_execution/register.py +70 -0
  262. aiq/tool/code_execution/utils.py +100 -0
  263. aiq/tool/datetime_tools.py +42 -0
  264. aiq/tool/document_search.py +141 -0
  265. aiq/tool/github_tools/__init__.py +0 -0
  266. aiq/tool/github_tools/create_github_commit.py +133 -0
  267. aiq/tool/github_tools/create_github_issue.py +87 -0
  268. aiq/tool/github_tools/create_github_pr.py +106 -0
  269. aiq/tool/github_tools/get_github_file.py +106 -0
  270. aiq/tool/github_tools/get_github_issue.py +166 -0
  271. aiq/tool/github_tools/get_github_pr.py +256 -0
  272. aiq/tool/github_tools/update_github_issue.py +100 -0
  273. aiq/tool/mcp/__init__.py +14 -0
  274. aiq/tool/mcp/mcp_client.py +220 -0
  275. aiq/tool/mcp/mcp_tool.py +76 -0
  276. aiq/tool/memory_tools/__init__.py +0 -0
  277. aiq/tool/memory_tools/add_memory_tool.py +67 -0
  278. aiq/tool/memory_tools/delete_memory_tool.py +67 -0
  279. aiq/tool/memory_tools/get_memory_tool.py +72 -0
  280. aiq/tool/nvidia_rag.py +95 -0
  281. aiq/tool/register.py +36 -0
  282. aiq/tool/retriever.py +89 -0
  283. aiq/utils/__init__.py +0 -0
  284. aiq/utils/data_models/__init__.py +0 -0
  285. aiq/utils/data_models/schema_validator.py +58 -0
  286. aiq/utils/debugging_utils.py +43 -0
  287. aiq/utils/exception_handlers/__init__.py +0 -0
  288. aiq/utils/exception_handlers/schemas.py +114 -0
  289. aiq/utils/io/__init__.py +0 -0
  290. aiq/utils/io/yaml_tools.py +119 -0
  291. aiq/utils/metadata_utils.py +74 -0
  292. aiq/utils/optional_imports.py +142 -0
  293. aiq/utils/producer_consumer_queue.py +178 -0
  294. aiq/utils/reactive/__init__.py +0 -0
  295. aiq/utils/reactive/base/__init__.py +0 -0
  296. aiq/utils/reactive/base/observable_base.py +65 -0
  297. aiq/utils/reactive/base/observer_base.py +55 -0
  298. aiq/utils/reactive/base/subject_base.py +79 -0
  299. aiq/utils/reactive/observable.py +59 -0
  300. aiq/utils/reactive/observer.py +76 -0
  301. aiq/utils/reactive/subject.py +131 -0
  302. aiq/utils/reactive/subscription.py +49 -0
  303. aiq/utils/settings/__init__.py +0 -0
  304. aiq/utils/settings/global_settings.py +197 -0
  305. aiq/utils/type_converter.py +232 -0
  306. aiq/utils/type_utils.py +397 -0
  307. aiq/utils/url_utils.py +27 -0
  308. aiqtoolkit-1.1.0a20250503.dist-info/METADATA +330 -0
  309. aiqtoolkit-1.1.0a20250503.dist-info/RECORD +314 -0
  310. aiqtoolkit-1.1.0a20250503.dist-info/WHEEL +5 -0
  311. aiqtoolkit-1.1.0a20250503.dist-info/entry_points.txt +17 -0
  312. aiqtoolkit-1.1.0a20250503.dist-info/licenses/LICENSE-3rd-party.txt +3686 -0
  313. aiqtoolkit-1.1.0a20250503.dist-info/licenses/LICENSE.md +201 -0
  314. aiqtoolkit-1.1.0a20250503.dist-info/top_level.txt +1 -0
@@ -0,0 +1,433 @@
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
+ import re
18
+ from contextlib import asynccontextmanager
19
+ from contextlib import contextmanager
20
+ from typing import Any
21
+
22
+ from openinference.semconv.trace import OpenInferenceSpanKindValues
23
+ from openinference.semconv.trace import SpanAttributes
24
+ from pydantic import TypeAdapter
25
+
26
+ from aiq.builder.context import AIQContextState
27
+ from aiq.data_models.intermediate_step import IntermediateStep
28
+ from aiq.data_models.intermediate_step import IntermediateStepState
29
+ from aiq.utils.optional_imports import TelemetryOptionalImportError
30
+ from aiq.utils.optional_imports import try_import_opentelemetry
31
+
32
+ try:
33
+ from weave.trace.context import weave_client_context
34
+ from weave.trace.context.call_context import get_current_call
35
+ from weave.trace.context.call_context import set_call_stack
36
+ from weave.trace.weave_client import Call
37
+ WEAVE_AVAILABLE = True
38
+ except ImportError:
39
+ WEAVE_AVAILABLE = False
40
+ # we simply don't do anything if weave is not available
41
+ pass
42
+
43
+ logger = logging.getLogger(__name__)
44
+
45
+ OPENINFERENCE_SPAN_KIND = SpanAttributes.OPENINFERENCE_SPAN_KIND
46
+
47
+ # Try to import OpenTelemetry modules
48
+ # If the dependencies are not installed, use dummy objects here
49
+ try:
50
+ opentelemetry = try_import_opentelemetry()
51
+ from opentelemetry import trace
52
+ from opentelemetry.sdk.trace import TracerProvider
53
+ from opentelemetry.trace import Span
54
+ from opentelemetry.trace.propagation import set_span_in_context
55
+ except TelemetryOptionalImportError:
56
+ from aiq.utils.optional_imports import DummySpan # pylint: disable=ungrouped-imports
57
+ from aiq.utils.optional_imports import DummyTrace # pylint: disable=ungrouped-imports
58
+ from aiq.utils.optional_imports import DummyTracerProvider # pylint: disable=ungrouped-imports
59
+ from aiq.utils.optional_imports import dummy_set_span_in_context # pylint: disable=ungrouped-imports
60
+ trace = DummyTrace
61
+ TracerProvider = DummyTracerProvider
62
+ Span = DummySpan
63
+ set_span_in_context = dummy_set_span_in_context
64
+
65
+
66
+ def _ns_timestamp(seconds_float: float) -> int:
67
+ """
68
+ Convert AIQ Toolkit's float `event_timestamp` (in seconds) into an integer number
69
+ of nanoseconds, as OpenTelemetry expects.
70
+ """
71
+ return int(seconds_float * 1e9)
72
+
73
+
74
+ class AsyncOtelSpanListener:
75
+ """
76
+ A separate, async class that listens to the AIQ Toolkit intermediate step
77
+ event stream and creates proper Otel spans:
78
+
79
+ - On FUNCTION_START => open a new top-level span
80
+ - On any other intermediate step => open a child subspan (immediate open/close)
81
+ - On FUNCTION_END => close the function's top-level span
82
+
83
+ This runs fully independently from the normal AIQ Toolkit workflow, so that
84
+ the workflow is not blocking or entangled by OTel calls.
85
+ """
86
+
87
+ def __init__(self, context_state: AIQContextState | None = None):
88
+ """
89
+ :param context_state: Optionally supply a specific AIQContextState.
90
+ If None, uses the global singleton.
91
+ """
92
+ self._context_state = context_state or AIQContextState.get()
93
+
94
+ # Maintain a subscription so we can unsubscribe on shutdown
95
+ self._subscription = None
96
+
97
+ # Outstanding spans which have been opened but not yet closed
98
+ self._outstanding_spans: dict[str, Span] = {}
99
+
100
+ # Stack of spans, for when we need to create a child span
101
+ self._span_stack: dict[str, Span] = {}
102
+
103
+ self._running = False
104
+
105
+ # Prepare the tracer (optionally you might already have done this)
106
+ if trace.get_tracer_provider() is None or not isinstance(trace.get_tracer_provider(), TracerProvider):
107
+ tracer_provider = TracerProvider()
108
+ trace.set_tracer_provider(tracer_provider)
109
+
110
+ # We'll optionally attach exporters if you want (out of scope to do it here).
111
+ # Example: tracer_provider.add_span_processor(BatchSpanProcessor(your_exporter))
112
+
113
+ self._tracer = trace.get_tracer("aiq-async-otel-listener")
114
+
115
+ # Initialize Weave-specific components if available
116
+ self.gc = None
117
+ self._weave_calls = {}
118
+ if WEAVE_AVAILABLE:
119
+ try:
120
+ # Try to get the weave client, but don't fail if Weave isn't initialized
121
+ self.gc = weave_client_context.require_weave_client()
122
+ except Exception:
123
+ # Weave is not initialized, so we don't do anything
124
+ pass
125
+
126
+ def _on_next(self, step: IntermediateStep) -> None:
127
+ """
128
+ The main logic that reacts to each IntermediateStep.
129
+ """
130
+ if (step.event_state == IntermediateStepState.START):
131
+
132
+ self._process_start_event(step)
133
+
134
+ elif (step.event_state == IntermediateStepState.END):
135
+
136
+ self._process_end_event(step)
137
+
138
+ def _on_error(self, exc: Exception) -> None:
139
+ logger.error("Error in intermediate step subscription: %s", exc, exc_info=True)
140
+
141
+ def _on_complete(self) -> None:
142
+ logger.debug("Intermediate step stream completed. No more events will arrive.")
143
+
144
+ @asynccontextmanager
145
+ async def start(self):
146
+ """
147
+ Usage::
148
+
149
+ otel_listener = AsyncOtelSpanListener()
150
+ async with otel_listener.start():
151
+ # run your AIQ Toolkit workflow
152
+ ...
153
+ # cleans up
154
+
155
+ This sets up the subscription to the AIQ Toolkit event stream and starts the background loop.
156
+ """
157
+ try:
158
+ # Subscribe to the event stream
159
+ subject = self._context_state.event_stream.get()
160
+ self._subscription = subject.subscribe(
161
+ on_next=self._on_next,
162
+ on_error=self._on_error,
163
+ on_complete=self._on_complete,
164
+ )
165
+
166
+ self._running = True
167
+
168
+ yield # let the caller do their workflow
169
+
170
+ finally:
171
+ # Cleanup
172
+ self._running = False
173
+ # Close out any running spans
174
+ await self._cleanup()
175
+
176
+ if self._subscription:
177
+ self._subscription.unsubscribe()
178
+ self._subscription = None
179
+
180
+ async def _cleanup(self):
181
+ """
182
+ Close any remaining open spans.
183
+ """
184
+ if self._outstanding_spans:
185
+ logger.warning(
186
+ "Not all spans were closed. Ensure all start events have a corresponding end event. Remaining: %s",
187
+ self._outstanding_spans)
188
+
189
+ for span_info in self._outstanding_spans.values():
190
+ span_info.end()
191
+
192
+ self._outstanding_spans.clear()
193
+
194
+ if len(self._span_stack) > 0:
195
+ logger.error(
196
+ "Not all spans were closed. Ensure all start events have a corresponding end event. Remaining: %s",
197
+ self._span_stack)
198
+
199
+ self._span_stack.clear()
200
+
201
+ # Clean up any lingering Weave calls if Weave is available and initialized
202
+ if self.gc is not None and self._weave_calls:
203
+ for _, call in list(self._weave_calls.items()):
204
+ self.gc.finish_call(call, {"status": "incomplete"})
205
+ self._weave_calls.clear()
206
+
207
+ def _serialize_payload(self, input_value: Any) -> tuple[str, bool]:
208
+ """
209
+ Serialize the input value to a string. Returns a tuple with the serialized value and a boolean indicating if the
210
+ serialization is JSON or a string
211
+ """
212
+ try:
213
+ return TypeAdapter(type(input_value)).dump_json(input_value).decode('utf-8'), True
214
+ except Exception:
215
+ # Fallback to string representation if we can't serialize using pydantic
216
+ return str(input_value), False
217
+
218
+ def _process_start_event(self, step: IntermediateStep):
219
+
220
+ parent_ctx = None
221
+
222
+ if (len(self._span_stack) > 0):
223
+ parent_span = self._span_stack.get(step.function_ancestry.parent_id, None)
224
+ if parent_span is None:
225
+ logger.warning("No parent span found for step %s", step.UUID)
226
+ return
227
+
228
+ parent_ctx = set_span_in_context(parent_span)
229
+
230
+ # Extract start/end times from the step
231
+ # By convention, `span_event_timestamp` is the time we started, `event_timestamp` is the time we ended.
232
+ # If span_event_timestamp is missing, we default to event_timestamp (meaning zero-length).
233
+ s_ts = step.payload.span_event_timestamp or step.payload.event_timestamp
234
+ start_ns = _ns_timestamp(s_ts)
235
+
236
+ # Optional: embed the LLM/tool name if present
237
+ if step.payload.name:
238
+ sub_span_name = f"{step.payload.name}"
239
+ else:
240
+ sub_span_name = f"{step.payload.event_type}"
241
+
242
+ # Start the subspan
243
+ sub_span = self._tracer.start_span(
244
+ name=sub_span_name,
245
+ context=parent_ctx,
246
+ attributes={
247
+ "aiq.event_type": step.payload.event_type.value,
248
+ "aiq.function.id": step.function_ancestry.function_id,
249
+ "aiq.function.name": step.function_ancestry.function_name,
250
+ "aiq.subspan.name": step.payload.name or "",
251
+ "aiq.event_timestamp": step.event_timestamp,
252
+ "aiq.framework": step.payload.framework.value if step.payload.framework else "unknown",
253
+ },
254
+ start_time=start_ns,
255
+ )
256
+
257
+ event_type_to_span_kind = {
258
+ "LLM_START": OpenInferenceSpanKindValues.LLM,
259
+ "LLM_END": OpenInferenceSpanKindValues.LLM,
260
+ "LLM_NEW_TOKEN": OpenInferenceSpanKindValues.LLM,
261
+ "TOOL_START": OpenInferenceSpanKindValues.TOOL,
262
+ "TOOL_END": OpenInferenceSpanKindValues.TOOL,
263
+ "FUNCTION_START": OpenInferenceSpanKindValues.CHAIN,
264
+ "FUNCTION_END": OpenInferenceSpanKindValues.CHAIN,
265
+ }
266
+
267
+ span_kind = event_type_to_span_kind.get(step.event_type, OpenInferenceSpanKindValues.UNKNOWN)
268
+ sub_span.set_attribute(SpanAttributes.OPENINFERENCE_SPAN_KIND, span_kind.value)
269
+
270
+ if step.payload.data and step.payload.data.input:
271
+ # optional parse
272
+ match = re.search(r"Human:\s*Question:\s*(.*)", str(step.payload.data.input))
273
+ if match:
274
+ human_question = match.group(1).strip()
275
+ sub_span.set_attribute(SpanAttributes.INPUT_VALUE, human_question)
276
+ else:
277
+ serialized_input, is_json = self._serialize_payload(step.payload.data.input)
278
+ sub_span.set_attribute(SpanAttributes.INPUT_VALUE, serialized_input)
279
+ sub_span.set_attribute(SpanAttributes.INPUT_MIME_TYPE, "application/json" if is_json else "text/plain")
280
+
281
+ self._span_stack[step.UUID] = sub_span
282
+
283
+ self._outstanding_spans[step.UUID] = sub_span
284
+
285
+ # Create corresponding Weave call if Weave is available and initialized
286
+ if self.gc is not None:
287
+ self._create_weave_call(step, sub_span)
288
+
289
+ def _process_end_event(self, step: IntermediateStep):
290
+
291
+ # Find the subspan that was created in the start event
292
+ sub_span = self._outstanding_spans.pop(step.UUID, None)
293
+
294
+ if sub_span is None:
295
+ logger.warning("No subspan found for step %s", step.UUID)
296
+ return
297
+
298
+ self._span_stack.pop(step.UUID, None)
299
+
300
+ # Optionally add more attributes from usage_info or data
301
+ usage_info = step.payload.usage_info
302
+ if usage_info:
303
+ sub_span.set_attribute("aiq.usage.num_llm_calls",
304
+ usage_info.num_llm_calls if usage_info.num_llm_calls else 0)
305
+ sub_span.set_attribute("aiq.usage.seconds_between_calls",
306
+ usage_info.seconds_between_calls if usage_info.seconds_between_calls else 0)
307
+ sub_span.set_attribute(SpanAttributes.LLM_TOKEN_COUNT_PROMPT,
308
+ usage_info.token_usage.prompt_tokens if usage_info.token_usage else 0)
309
+ sub_span.set_attribute(SpanAttributes.LLM_TOKEN_COUNT_COMPLETION,
310
+ usage_info.token_usage.completion_tokens if usage_info.token_usage else 0)
311
+ sub_span.set_attribute(SpanAttributes.LLM_TOKEN_COUNT_TOTAL,
312
+ usage_info.token_usage.total_tokens if usage_info.token_usage else 0)
313
+
314
+ if step.payload.data and step.payload.data.output is not None:
315
+ serialized_output, is_json = self._serialize_payload(step.payload.data.output)
316
+ sub_span.set_attribute(SpanAttributes.OUTPUT_VALUE, serialized_output)
317
+ sub_span.set_attribute(SpanAttributes.OUTPUT_MIME_TYPE, "application/json" if is_json else "text/plain")
318
+
319
+ end_ns = _ns_timestamp(step.payload.event_timestamp)
320
+
321
+ # End the subspan
322
+ sub_span.end(end_time=end_ns)
323
+
324
+ # Finish corresponding Weave call if Weave is available and initialized
325
+ if self.gc is not None:
326
+ self._finish_weave_call(step, sub_span)
327
+
328
+ @contextmanager
329
+ def parent_call(self, trace_id: str, parent_call_id: str):
330
+ """Context manager to set a parent call context for Weave.
331
+ This allows connecting AIQ spans to existing traces from other frameworks.
332
+ """
333
+ dummy_call = Call(trace_id=trace_id, id=parent_call_id, _op_name="", project_id="", parent_id=None, inputs={})
334
+ with set_call_stack([dummy_call]):
335
+ yield
336
+
337
+ def _create_weave_call(self, step: IntermediateStep, span: Span) -> None:
338
+ """
339
+ Create a Weave call directly from the span and step data,
340
+ connecting to existing framework traces if available.
341
+ """
342
+ # Check for existing Weave trace/call
343
+ existing_call = get_current_call()
344
+
345
+ # Extract parent call if applicable
346
+ parent_call = None
347
+
348
+ # If we have an existing Weave call from another framework (e.g., LangChain),
349
+ # use it as the parent
350
+ if existing_call is not None:
351
+ parent_call = existing_call
352
+ logger.debug(f"Found existing Weave call: {existing_call.id} from trace: {existing_call.trace_id}")
353
+ # Otherwise, check our internal stack for parent relationships
354
+ elif len(self._weave_calls) > 0 and len(self._span_stack) > 1:
355
+ # Get the parent span using stack position (one level up)
356
+ parent_span_id = self._span_stack[-2].get_span_context().span_id
357
+ # Find the corresponding weave call for this parent span
358
+ for uuid, call in self._weave_calls.items():
359
+ if getattr(call, "span_id", None) == parent_span_id:
360
+ parent_call = call
361
+ break
362
+
363
+ # Generate a meaningful operation name based on event type
364
+ event_type = step.payload.event_type.split(".")[-1]
365
+ if step.payload.name:
366
+ op_name = f"aiq.{event_type}.{step.payload.name}"
367
+ else:
368
+ op_name = f"aiq.{event_type}"
369
+
370
+ # Create input dictionary
371
+ inputs = {}
372
+ if step.payload.data and step.payload.data.input is not None:
373
+ try:
374
+ # Add the input to the Weave call
375
+ inputs["input"] = step.payload.data.input
376
+ except Exception:
377
+ # If serialization fails, use string representation
378
+ inputs["input"] = str(step.payload.data.input)
379
+
380
+ # Create the Weave call
381
+ call = self.gc.create_call(
382
+ op_name,
383
+ inputs=inputs,
384
+ parent=parent_call,
385
+ attributes=span.attributes,
386
+ display_name=op_name,
387
+ )
388
+
389
+ # Store the call with step UUID as key
390
+ self._weave_calls[step.UUID] = call
391
+
392
+ # Store span ID for parent reference
393
+ setattr(call, "span_id", span.get_span_context().span_id)
394
+
395
+ return call
396
+
397
+ def _finish_weave_call(self, step: IntermediateStep, span: Span) -> None:
398
+ """
399
+ Finish a previously created Weave call
400
+ """
401
+ # Find the call for this step
402
+ call = self._weave_calls.pop(step.UUID, None)
403
+
404
+ if call is None:
405
+ logger.warning("No Weave call found for step %s", step.UUID)
406
+ return
407
+
408
+ # Create output dictionary
409
+ outputs = {}
410
+ if step.payload.data and step.payload.data.output is not None:
411
+ try:
412
+ # Add the output to the Weave call
413
+ outputs["output"] = step.payload.data.output
414
+ except Exception:
415
+ # If serialization fails, use string representation
416
+ outputs["output"] = str(step.payload.data.output)
417
+
418
+ # Add usage information if available
419
+ usage_info = step.payload.usage_info
420
+ if usage_info:
421
+ if usage_info.token_usage:
422
+ outputs["prompt_tokens"] = usage_info.token_usage.prompt_tokens
423
+ outputs["completion_tokens"] = usage_info.token_usage.completion_tokens
424
+ outputs["total_tokens"] = usage_info.token_usage.total_tokens
425
+
426
+ if usage_info.num_llm_calls:
427
+ outputs["num_llm_calls"] = usage_info.num_llm_calls
428
+
429
+ if usage_info.seconds_between_calls:
430
+ outputs["seconds_between_calls"] = usage_info.seconds_between_calls
431
+
432
+ # Finish the call with outputs
433
+ self.gc.finish_call(call, outputs)
@@ -0,0 +1,99 @@
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 Field
19
+
20
+ from aiq.builder.builder import Builder
21
+ from aiq.cli.register_workflow import register_logging_method
22
+ from aiq.cli.register_workflow import register_telemetry_exporter
23
+ from aiq.data_models.logging import LoggingBaseConfig
24
+ from aiq.data_models.telemetry_exporter import TelemetryExporterBaseConfig
25
+ from aiq.utils.optional_imports import try_import_opentelemetry
26
+ from aiq.utils.optional_imports import try_import_phoenix
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ class PhoenixTelemetryExporter(TelemetryExporterBaseConfig, name="phoenix"):
32
+ """A telemetry exporter to transmit traces to externally hosted phoenix service."""
33
+
34
+ endpoint: str = Field(description="The phoenix endpoint to export telemetry traces.")
35
+ project: str = Field(description="The project name to group the telemetry traces.")
36
+
37
+
38
+ @register_telemetry_exporter(config_type=PhoenixTelemetryExporter)
39
+ async def phoenix_telemetry_exporter(config: PhoenixTelemetryExporter, builder: Builder):
40
+ """Create a Phoenix telemetry exporter."""
41
+ try:
42
+ # If the dependencies are not installed, a TelemetryOptionalImportError will be raised
43
+ phoenix = try_import_phoenix() # noqa: F841
44
+ from phoenix.otel import HTTPSpanExporter
45
+ yield HTTPSpanExporter(config.endpoint)
46
+ except ConnectionError as ex:
47
+ logger.warning("Unable to connect to Phoenix at port 6006. Are you sure Phoenix is running?\n %s",
48
+ ex,
49
+ exc_info=True)
50
+
51
+
52
+ class OtelCollectorTelemetryExporter(TelemetryExporterBaseConfig, name="otelcollector"):
53
+ """A telemetry exporter to transmit traces to externally hosted otel collector service."""
54
+
55
+ endpoint: str = Field(description="The otel endpoint to export telemetry traces.")
56
+ project: str = Field(description="The project name to group the telemetry traces.")
57
+
58
+
59
+ @register_telemetry_exporter(config_type=OtelCollectorTelemetryExporter)
60
+ async def otel_telemetry_exporter(config: OtelCollectorTelemetryExporter, builder: Builder):
61
+ """Create an OpenTelemetry telemetry exporter."""
62
+ # If the dependencies are not installed, a TelemetryOptionalImportError will be raised
63
+ opentelemetry = try_import_opentelemetry()
64
+ yield opentelemetry.sdk.trace.export.OTLPSpanExporter(config.endpoint)
65
+
66
+
67
+ class ConsoleLoggingMethod(LoggingBaseConfig, name="console"):
68
+ """A logger to write runtime logs to the console."""
69
+
70
+ level: str = Field(description="The logging level of console logger.")
71
+
72
+
73
+ @register_logging_method(config_type=ConsoleLoggingMethod)
74
+ async def console_logging_method(config: ConsoleLoggingMethod, builder: Builder):
75
+ """
76
+ Build and return a StreamHandler for console-based logging.
77
+ """
78
+ level = getattr(logging, config.level.upper(), logging.INFO)
79
+ handler = logging.StreamHandler()
80
+ handler.setLevel(level)
81
+ yield handler
82
+
83
+
84
+ class FileLoggingMethod(LoggingBaseConfig, name="file"):
85
+ """A logger to write runtime logs to a file."""
86
+
87
+ path: str = Field(description="The file path to save the logging output.")
88
+ level: str = Field(description="The logging level of file logger.")
89
+
90
+
91
+ @register_logging_method(config_type=FileLoggingMethod)
92
+ async def file_logging_method(config: FileLoggingMethod, builder: Builder):
93
+ """
94
+ Build and return a FileHandler for file-based logging.
95
+ """
96
+ level = getattr(logging, config.level.upper(), logging.INFO)
97
+ handler = logging.FileHandler(filename=config.path, mode="a", encoding="utf-8")
98
+ handler.setLevel(level)
99
+ yield handler
aiq/plugins/.namespace ADDED
@@ -0,0 +1 @@
1
+ Note: This is a python namespace package and this directory should remain empty. Do NOT add a `__init__.py` file or any other files to this directory. This file is also needed to ensure the directory exists in git.
File without changes
File without changes