aiqtoolkit 1.2.0a20250707__py3-none-any.whl → 1.2.0rc1__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 +170 -8
  2. aiq/agent/dual_node.py +1 -1
  3. aiq/agent/react_agent/agent.py +112 -111
  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 +123 -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.0rc1.dist-info}/METADATA +37 -9
  190. {aiqtoolkit-1.2.0a20250707.dist-info → aiqtoolkit-1.2.0rc1.dist-info}/RECORD +195 -80
  191. {aiqtoolkit-1.2.0a20250707.dist-info → aiqtoolkit-1.2.0rc1.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.0rc1.dist-info}/WHEEL +0 -0
  195. {aiqtoolkit-1.2.0a20250707.dist-info → aiqtoolkit-1.2.0rc1.dist-info}/licenses/LICENSE-3rd-party.txt +0 -0
  196. {aiqtoolkit-1.2.0a20250707.dist-info → aiqtoolkit-1.2.0rc1.dist-info}/licenses/LICENSE.md +0 -0
  197. {aiqtoolkit-1.2.0a20250707.dist-info → aiqtoolkit-1.2.0rc1.dist-info}/top_level.txt +0 -0
@@ -1,153 +0,0 @@
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 asyncio
17
- import logging
18
- import typing
19
- from collections.abc import Awaitable
20
- from collections.abc import Callable
21
- from typing import Any
22
-
23
- from fastapi import WebSocket
24
- from fastapi import WebSocketException
25
- from starlette.endpoints import WebSocketEndpoint
26
- from starlette.websockets import WebSocketDisconnect
27
-
28
- from aiq.data_models.api_server import AIQChatRequest
29
- from aiq.data_models.api_server import AIQChatResponse
30
- from aiq.data_models.api_server import AIQChatResponseChunk
31
- from aiq.data_models.api_server import AIQResponsePayloadOutput
32
- from aiq.data_models.api_server import AIQResponseSerializable
33
- from aiq.data_models.api_server import WebSocketMessageStatus
34
- from aiq.data_models.api_server import WorkflowSchemaType
35
- from aiq.front_ends.fastapi.message_handler import MessageHandler
36
- from aiq.front_ends.fastapi.response_helpers import generate_streaming_response
37
- from aiq.front_ends.fastapi.step_adaptor import StepAdaptor
38
- from aiq.runtime.session import AIQSessionManager
39
-
40
- logger = logging.getLogger(__name__)
41
-
42
-
43
- class AIQWebSocket(WebSocketEndpoint):
44
- encoding = "json"
45
-
46
- def __init__(self, session_manager: AIQSessionManager, step_adaptor: StepAdaptor, *args, **kwargs):
47
- self._session_manager: AIQSessionManager = session_manager
48
- self._message_handler: MessageHandler = MessageHandler(self)
49
- self._process_response_event: asyncio.Event = asyncio.Event()
50
- self._workflow_schema_type: dict[str, Callable[..., Awaitable[Any]]] = {
51
- WorkflowSchemaType.GENERATE_STREAM: self.process_generate_stream,
52
- WorkflowSchemaType.CHAT_STREAM: self.process_chat_stream,
53
- WorkflowSchemaType.GENERATE: self.process_generate,
54
- WorkflowSchemaType.CHAT: self.process_chat
55
- }
56
- self._step_adaptor = step_adaptor
57
- super().__init__(*args, **kwargs)
58
-
59
- @property
60
- def workflow_schema_type(self) -> dict[str, Callable[..., Awaitable[Any]]]:
61
- return self._workflow_schema_type
62
-
63
- @property
64
- def process_response_event(self) -> asyncio.Event:
65
- return self._process_response_event
66
-
67
- async def on_connect(self, websocket: WebSocket):
68
- try:
69
- # Accept the websocket connection
70
- await websocket.accept()
71
- try:
72
- # Start background message processors
73
- self._message_handler.process_messages_task = asyncio.create_task(
74
- self._message_handler.process_messages())
75
-
76
- self._message_handler.process_out_going_messages_task = asyncio.create_task(
77
- self._message_handler.process_out_going_messages(websocket))
78
-
79
- except asyncio.CancelledError:
80
- pass
81
-
82
- except (WebSocketDisconnect, WebSocketException):
83
- logger.error("A WebSocket error occured during `on_connect`. Ignoring the connection.", exc_info=True)
84
-
85
- async def on_send(self, websocket: WebSocket, data: dict[str, str]):
86
- try:
87
- await websocket.send_json(data)
88
- except (WebSocketDisconnect, WebSocketException, Exception):
89
- logger.error("A WebSocket error occurred during `on_send`. Ignoring the connection.", exc_info=True)
90
-
91
- async def on_receive(self, websocket: WebSocket, data: dict[str, Any]):
92
- try:
93
- await self._message_handler.messages_queue.put(data)
94
- except (Exception):
95
- logger.error("An unxpected error occurred during `on_receive`. Ignoring the exception", exc_info=True)
96
-
97
- async def on_disconnect(self, websocket: WebSocket, close_code: Any):
98
- try:
99
- if self._message_handler.process_messages_task:
100
- self._message_handler.process_messages_task.cancel()
101
-
102
- if self._message_handler.process_out_going_messages_task:
103
- self._message_handler.process_out_going_messages_task.cancel()
104
-
105
- if self._message_handler.background_task:
106
- self._message_handler.background_task.cancel()
107
-
108
- except (WebSocketDisconnect, asyncio.CancelledError):
109
- pass
110
-
111
- async def _process_message(self,
112
- payload: typing.Any,
113
- conversation_id: str | None = None,
114
- result_type: type | None = None,
115
- output_type: type | None = None) -> None:
116
-
117
- async with self._session_manager.session(
118
- conversation_id=conversation_id,
119
- user_input_callback=self._message_handler.human_interaction) as session:
120
-
121
- async for value in generate_streaming_response(payload,
122
- session_manager=session,
123
- streaming=True,
124
- step_adaptor=self._step_adaptor,
125
- result_type=result_type,
126
- output_type=output_type):
127
-
128
- await self._process_response_event.wait()
129
-
130
- if not isinstance(value, AIQResponseSerializable):
131
- value = AIQResponsePayloadOutput(payload=value)
132
-
133
- await self._message_handler.create_websocket_message(data_model=value,
134
- status=WebSocketMessageStatus.IN_PROGRESS)
135
-
136
- async def process_generate_stream(self, payload: str, conversation_id: str) -> None:
137
-
138
- return await self._process_message(payload, conversation_id=conversation_id, result_type=None, output_type=None)
139
-
140
- async def process_chat_stream(self, payload: AIQChatRequest, conversation_id: str):
141
-
142
- return await self._process_message(payload,
143
- conversation_id=conversation_id,
144
- result_type=AIQChatResponse,
145
- output_type=AIQChatResponseChunk)
146
-
147
- async def process_generate(self, payload: typing.Any, conversation_id: str):
148
-
149
- return await self._process_message(payload, conversation_id=conversation_id)
150
-
151
- async def process_chat(self, payload: AIQChatRequest, conversation_id: str):
152
-
153
- return await self._process_message(payload, conversation_id=conversation_id, result_type=AIQChatResponse)
@@ -1,470 +0,0 @@
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 json
17
- import logging
18
- import re
19
- import warnings
20
- from contextlib import asynccontextmanager
21
- from contextlib import contextmanager
22
- from typing import Any
23
-
24
- from openinference.semconv.trace import OpenInferenceSpanKindValues
25
- from openinference.semconv.trace import SpanAttributes
26
- from pydantic import TypeAdapter
27
-
28
- from aiq.builder.context import AIQContextState
29
- from aiq.data_models.intermediate_step import IntermediateStep
30
- from aiq.data_models.intermediate_step import IntermediateStepState
31
- from aiq.utils.optional_imports import TelemetryOptionalImportError
32
- from aiq.utils.optional_imports import try_import_opentelemetry
33
-
34
- try:
35
- with warnings.catch_warnings():
36
- # Ignore deprecation warnings being triggered by weave. https://github.com/wandb/weave/issues/3666
37
- # and https://github.com/wandb/weave/issues/4533
38
- warnings.filterwarnings("ignore", category=DeprecationWarning, message=r"^`sentry_sdk\.Hub` is deprecated")
39
- warnings.filterwarnings("ignore",
40
- category=DeprecationWarning,
41
- message=r"^Using extra keyword arguments on `Field` is deprecated")
42
- warnings.filterwarnings("ignore",
43
- category=DeprecationWarning,
44
- message=r"^`include` is deprecated and does nothing")
45
- from weave.trace.context import weave_client_context
46
- from weave.trace.context.call_context import get_current_call
47
- from weave.trace.context.call_context import set_call_stack
48
- from weave.trace.weave_client import Call
49
- WEAVE_AVAILABLE = True
50
- except ImportError:
51
- WEAVE_AVAILABLE = False
52
- # we simply don't do anything if weave is not available
53
- pass
54
-
55
- logger = logging.getLogger(__name__)
56
-
57
- OPENINFERENCE_SPAN_KIND = SpanAttributes.OPENINFERENCE_SPAN_KIND
58
-
59
- # Try to import OpenTelemetry modules
60
- # If the dependencies are not installed, use dummy objects here
61
- try:
62
- opentelemetry = try_import_opentelemetry()
63
- from opentelemetry import trace
64
- from opentelemetry.sdk.trace import TracerProvider
65
- from opentelemetry.trace import Span
66
- from opentelemetry.trace.propagation import set_span_in_context
67
- except TelemetryOptionalImportError:
68
- from aiq.utils.optional_imports import DummySpan # pylint: disable=ungrouped-imports
69
- from aiq.utils.optional_imports import DummyTrace # pylint: disable=ungrouped-imports
70
- from aiq.utils.optional_imports import DummyTracerProvider # pylint: disable=ungrouped-imports
71
- from aiq.utils.optional_imports import dummy_set_span_in_context # pylint: disable=ungrouped-imports
72
-
73
- trace = DummyTrace # pylint: disable=invalid-name
74
- TracerProvider = DummyTracerProvider
75
- Span = DummySpan
76
- set_span_in_context = dummy_set_span_in_context
77
-
78
-
79
- def merge_dicts(dict1: dict, dict2: dict) -> dict:
80
- """
81
- Merge two dictionaries, prioritizing non-null values from the first dictionary.
82
-
83
- Args:
84
- dict1 (dict): First dictionary (higher priority)
85
- dict2 (dict): Second dictionary (lower priority)
86
-
87
- Returns:
88
- dict: Merged dictionary with non-null values from dict1 taking precedence
89
- """
90
- result = dict2.copy() # Start with a copy of the second dictionary
91
- for key, value in dict1.items():
92
- if value is not None: # Only update if value is not None
93
- result[key] = value
94
- return result
95
-
96
-
97
- def _ns_timestamp(seconds_float: float) -> int:
98
- """
99
- Convert AIQ Toolkit's float `event_timestamp` (in seconds) into an integer number
100
- of nanoseconds, as OpenTelemetry expects.
101
- """
102
- return int(seconds_float * 1e9)
103
-
104
-
105
- class AsyncOtelSpanListener:
106
- """
107
- A separate, async class that listens to the AIQ Toolkit intermediate step
108
- event stream and creates proper Otel spans:
109
-
110
- - On FUNCTION_START => open a new top-level span
111
- - On any other intermediate step => open a child subspan (immediate open/close)
112
- - On FUNCTION_END => close the function's top-level span
113
-
114
- This runs fully independently from the normal AIQ Toolkit workflow, so that
115
- the workflow is not blocking or entangled by OTel calls.
116
- """
117
-
118
- def __init__(self, context_state: AIQContextState | None = None):
119
- """
120
- :param context_state: Optionally supply a specific AIQContextState.
121
- If None, uses the global singleton.
122
- """
123
- self._context_state = context_state or AIQContextState.get()
124
-
125
- # Maintain a subscription so we can unsubscribe on shutdown
126
- self._subscription = None
127
-
128
- # Outstanding spans which have been opened but not yet closed
129
- self._outstanding_spans: dict[str, Span] = {}
130
-
131
- # Stack of spans, for when we need to create a child span
132
- self._span_stack: dict[str, Span] = {}
133
-
134
- self._running = False
135
-
136
- # Prepare the tracer (optionally you might already have done this)
137
- if trace.get_tracer_provider() is None or not isinstance(trace.get_tracer_provider(), TracerProvider):
138
- tracer_provider = TracerProvider()
139
- trace.set_tracer_provider(tracer_provider)
140
-
141
- # We'll optionally attach exporters if you want (out of scope to do it here).
142
- # Example: tracer_provider.add_span_processor(BatchSpanProcessor(your_exporter))
143
-
144
- self._tracer = trace.get_tracer("aiq-async-otel-listener")
145
-
146
- # Initialize Weave-specific components if available
147
- self.gc = None
148
- self._weave_calls = {}
149
- if WEAVE_AVAILABLE:
150
- try:
151
- # Try to get the weave client, but don't fail if Weave isn't initialized
152
- self.gc = weave_client_context.require_weave_client()
153
- except Exception:
154
- # Weave is not initialized, so we don't do anything
155
- pass
156
-
157
- def _on_next(self, step: IntermediateStep) -> None:
158
- """
159
- The main logic that reacts to each IntermediateStep.
160
- """
161
- if (step.event_state == IntermediateStepState.START):
162
-
163
- self._process_start_event(step)
164
-
165
- elif (step.event_state == IntermediateStepState.END):
166
-
167
- self._process_end_event(step)
168
-
169
- def _on_error(self, exc: Exception) -> None:
170
- logger.error("Error in intermediate step subscription: %s", exc, exc_info=True)
171
-
172
- def _on_complete(self) -> None:
173
- logger.debug("Intermediate step stream completed. No more events will arrive.")
174
-
175
- @asynccontextmanager
176
- async def start(self):
177
- """
178
- Usage::
179
-
180
- otel_listener = AsyncOtelSpanListener()
181
- async with otel_listener.start():
182
- # run your AIQ Toolkit workflow
183
- ...
184
- # cleans up
185
-
186
- This sets up the subscription to the AIQ Toolkit event stream and starts the background loop.
187
- """
188
- try:
189
- # Subscribe to the event stream
190
- subject = self._context_state.event_stream.get()
191
- self._subscription = subject.subscribe(
192
- on_next=self._on_next,
193
- on_error=self._on_error,
194
- on_complete=self._on_complete,
195
- )
196
-
197
- self._running = True
198
-
199
- yield # let the caller do their workflow
200
-
201
- finally:
202
- # Cleanup
203
- self._running = False
204
- # Close out any running spans
205
- await self._cleanup()
206
-
207
- if self._subscription:
208
- self._subscription.unsubscribe()
209
- self._subscription = None
210
-
211
- async def _cleanup(self):
212
- """
213
- Close any remaining open spans.
214
- """
215
- if self._outstanding_spans:
216
- logger.warning(
217
- "Not all spans were closed. Ensure all start events have a corresponding end event. Remaining: %s",
218
- self._outstanding_spans)
219
-
220
- for span_info in self._outstanding_spans.values():
221
- span_info.end()
222
-
223
- self._outstanding_spans.clear()
224
-
225
- self._span_stack.clear()
226
-
227
- # Clean up any lingering Weave calls if Weave is available and initialized
228
- if self.gc is not None and self._weave_calls:
229
- for _, call in list(self._weave_calls.items()):
230
- self.gc.finish_call(call, {"status": "incomplete"})
231
- self._weave_calls.clear()
232
-
233
- def _serialize_payload(self, input_value: Any) -> tuple[str, bool]:
234
- """
235
- Serialize the input value to a string. Returns a tuple with the serialized value and a boolean indicating if the
236
- serialization is JSON or a string
237
- """
238
- try:
239
- return TypeAdapter(type(input_value)).dump_json(input_value).decode('utf-8'), True
240
- except Exception:
241
- # Fallback to string representation if we can't serialize using pydantic
242
- return str(input_value), False
243
-
244
- def _process_start_event(self, step: IntermediateStep):
245
-
246
- parent_ctx = None
247
-
248
- if (len(self._span_stack) > 0):
249
- parent_span = self._span_stack.get(step.function_ancestry.parent_id, None)
250
- if parent_span is None:
251
- logger.warning("No parent span found for step %s", step.UUID)
252
- return
253
-
254
- parent_ctx = set_span_in_context(parent_span)
255
-
256
- # Extract start/end times from the step
257
- # By convention, `span_event_timestamp` is the time we started, `event_timestamp` is the time we ended.
258
- # If span_event_timestamp is missing, we default to event_timestamp (meaning zero-length).
259
- s_ts = step.payload.span_event_timestamp or step.payload.event_timestamp
260
- start_ns = _ns_timestamp(s_ts)
261
-
262
- # Optional: embed the LLM/tool name if present
263
- if step.payload.name:
264
- sub_span_name = f"{step.payload.name}"
265
- else:
266
- sub_span_name = f"{step.payload.event_type}"
267
-
268
- # Start the subspan
269
- sub_span = self._tracer.start_span(
270
- name=sub_span_name,
271
- context=parent_ctx,
272
- attributes={
273
- "aiq.event_type": step.payload.event_type.value,
274
- "aiq.function.id": step.function_ancestry.function_id,
275
- "aiq.function.name": step.function_ancestry.function_name,
276
- "aiq.subspan.name": step.payload.name or "",
277
- "aiq.event_timestamp": step.event_timestamp,
278
- "aiq.framework": step.payload.framework.value if step.payload.framework else "unknown",
279
- },
280
- start_time=start_ns,
281
- )
282
-
283
- event_type_to_span_kind = {
284
- "LLM_START": OpenInferenceSpanKindValues.LLM,
285
- "LLM_END": OpenInferenceSpanKindValues.LLM,
286
- "LLM_NEW_TOKEN": OpenInferenceSpanKindValues.LLM,
287
- "TOOL_START": OpenInferenceSpanKindValues.TOOL,
288
- "TOOL_END": OpenInferenceSpanKindValues.TOOL,
289
- "FUNCTION_START": OpenInferenceSpanKindValues.CHAIN,
290
- "FUNCTION_END": OpenInferenceSpanKindValues.CHAIN,
291
- }
292
-
293
- span_kind = event_type_to_span_kind.get(step.event_type, OpenInferenceSpanKindValues.UNKNOWN)
294
- sub_span.set_attribute(SpanAttributes.OPENINFERENCE_SPAN_KIND, span_kind.value)
295
-
296
- if step.payload.data and step.payload.data.input:
297
- # optional parse
298
- match = re.search(r"Human:\s*Question:\s*(.*)", str(step.payload.data.input))
299
- if match:
300
- human_question = match.group(1).strip()
301
- sub_span.set_attribute(SpanAttributes.INPUT_VALUE, human_question)
302
- else:
303
- serialized_input, is_json = self._serialize_payload(step.payload.data.input)
304
- sub_span.set_attribute(SpanAttributes.INPUT_VALUE, serialized_input)
305
- sub_span.set_attribute(SpanAttributes.INPUT_MIME_TYPE, "application/json" if is_json else "text/plain")
306
-
307
- # Optional: add metadata to the span from TraceMetadata
308
- if step.payload.metadata:
309
- sub_span.set_attribute("aiq.metadata", step.payload.metadata.model_dump_json())
310
-
311
- self._span_stack[step.UUID] = sub_span
312
-
313
- self._outstanding_spans[step.UUID] = sub_span
314
-
315
- # Create corresponding Weave call if Weave is available and initialized
316
- if self.gc is not None:
317
- self._create_weave_call(step, sub_span)
318
-
319
- def _process_end_event(self, step: IntermediateStep):
320
-
321
- # Find the subspan that was created in the start event
322
- sub_span = self._outstanding_spans.pop(step.UUID, None)
323
-
324
- if sub_span is None:
325
- logger.warning("No subspan found for step %s", step.UUID)
326
- return
327
-
328
- self._span_stack.pop(step.UUID, None)
329
-
330
- # Optionally add more attributes from usage_info or data
331
- usage_info = step.payload.usage_info
332
- if usage_info:
333
- sub_span.set_attribute("aiq.usage.num_llm_calls",
334
- usage_info.num_llm_calls if usage_info.num_llm_calls else 0)
335
- sub_span.set_attribute("aiq.usage.seconds_between_calls",
336
- usage_info.seconds_between_calls if usage_info.seconds_between_calls else 0)
337
- sub_span.set_attribute(SpanAttributes.LLM_TOKEN_COUNT_PROMPT,
338
- usage_info.token_usage.prompt_tokens if usage_info.token_usage else 0)
339
- sub_span.set_attribute(SpanAttributes.LLM_TOKEN_COUNT_COMPLETION,
340
- usage_info.token_usage.completion_tokens if usage_info.token_usage else 0)
341
- sub_span.set_attribute(SpanAttributes.LLM_TOKEN_COUNT_TOTAL,
342
- usage_info.token_usage.total_tokens if usage_info.token_usage else 0)
343
-
344
- if step.payload.data and step.payload.data.output is not None:
345
- serialized_output, is_json = self._serialize_payload(step.payload.data.output)
346
- sub_span.set_attribute(SpanAttributes.OUTPUT_VALUE, serialized_output)
347
- sub_span.set_attribute(SpanAttributes.OUTPUT_MIME_TYPE, "application/json" if is_json else "text/plain")
348
-
349
- # # Optional: add metadata to the span from TraceMetadata
350
- if step.payload.metadata:
351
- start_event_metadata = json.loads(sub_span.attributes.get("aiq.metadata", {}))
352
- end_event_metadata = json.loads(step.payload.metadata.model_dump_json())
353
- merged_event_metadata = merge_dicts(start_event_metadata, end_event_metadata)
354
- sub_span.set_attribute("aiq.metadata", json.dumps(merged_event_metadata))
355
-
356
- end_ns = _ns_timestamp(step.payload.event_timestamp)
357
-
358
- # End the subspan
359
- sub_span.end(end_time=end_ns)
360
-
361
- # Finish corresponding Weave call if Weave is available and initialized
362
- if self.gc is not None:
363
- self._finish_weave_call(step)
364
-
365
- @contextmanager
366
- def parent_call(self, trace_id: str, parent_call_id: str):
367
- """Context manager to set a parent call context for Weave.
368
- This allows connecting AIQ spans to existing traces from other frameworks.
369
- """
370
- dummy_call = Call(trace_id=trace_id, id=parent_call_id, _op_name="", project_id="", parent_id=None, inputs={})
371
- with set_call_stack([dummy_call]):
372
- yield
373
-
374
- def _create_weave_call(self, step: IntermediateStep, span: Span) -> None:
375
- """
376
- Create a Weave call directly from the span and step data,
377
- connecting to existing framework traces if available.
378
- """
379
- # Check for existing Weave trace/call
380
- existing_call = get_current_call()
381
-
382
- # Extract parent call if applicable
383
- parent_call = None
384
-
385
- # If we have an existing Weave call from another framework (e.g., LangChain),
386
- # use it as the parent
387
- if existing_call is not None:
388
- parent_call = existing_call
389
- logger.debug("Found existing Weave call: %s from trace: %s", existing_call.id, existing_call.trace_id)
390
- # Otherwise, check our internal stack for parent relationships
391
- elif len(self._weave_calls) > 0 and len(self._span_stack) > 1:
392
- # Get the parent span using stack position (one level up)
393
- parent_span_id = self._span_stack[-2].get_span_context().span_id
394
- # Find the corresponding weave call for this parent span
395
- for call in self._weave_calls.values():
396
- if getattr(call, "span_id", None) == parent_span_id:
397
- parent_call = call
398
- break
399
-
400
- # Generate a meaningful operation name based on event type
401
- event_type = step.payload.event_type.split(".")[-1]
402
- if step.payload.name:
403
- op_name = f"aiq.{event_type}.{step.payload.name}"
404
- else:
405
- op_name = f"aiq.{event_type}"
406
-
407
- # Create input dictionary
408
- inputs = {}
409
- if step.payload.data and step.payload.data.input is not None:
410
- try:
411
- # Add the input to the Weave call
412
- inputs["input"] = step.payload.data.input
413
- except Exception:
414
- # If serialization fails, use string representation
415
- inputs["input"] = str(step.payload.data.input)
416
-
417
- # Create the Weave call
418
- call = self.gc.create_call(
419
- op_name,
420
- inputs=inputs,
421
- parent=parent_call,
422
- attributes=span.attributes,
423
- display_name=op_name,
424
- )
425
-
426
- # Store the call with step UUID as key
427
- self._weave_calls[step.UUID] = call
428
-
429
- # Store span ID for parent reference
430
- setattr(call, "span_id", span.get_span_context().span_id)
431
-
432
- return call
433
-
434
- def _finish_weave_call(self, step: IntermediateStep) -> None:
435
- """
436
- Finish a previously created Weave call
437
- """
438
- # Find the call for this step
439
- call = self._weave_calls.pop(step.UUID, None)
440
-
441
- if call is None:
442
- logger.warning("No Weave call found for step %s", step.UUID)
443
- return
444
-
445
- # Create output dictionary
446
- outputs = {}
447
- if step.payload.data and step.payload.data.output is not None:
448
- try:
449
- # Add the output to the Weave call
450
- outputs["output"] = step.payload.data.output
451
- except Exception:
452
- # If serialization fails, use string representation
453
- outputs["output"] = str(step.payload.data.output)
454
-
455
- # Add usage information if available
456
- usage_info = step.payload.usage_info
457
- if usage_info:
458
- if usage_info.token_usage:
459
- outputs["prompt_tokens"] = usage_info.token_usage.prompt_tokens
460
- outputs["completion_tokens"] = usage_info.token_usage.completion_tokens
461
- outputs["total_tokens"] = usage_info.token_usage.total_tokens
462
-
463
- if usage_info.num_llm_calls:
464
- outputs["num_llm_calls"] = usage_info.num_llm_calls
465
-
466
- if usage_info.seconds_between_calls:
467
- outputs["seconds_between_calls"] = usage_info.seconds_between_calls
468
-
469
- # Finish the call with outputs
470
- self.gc.finish_call(call, outputs)