aiqtoolkit 1.2.0.dev0__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 (220) hide show
  1. aiq/agent/base.py +170 -8
  2. aiq/agent/dual_node.py +1 -1
  3. aiq/agent/react_agent/agent.py +146 -112
  4. aiq/agent/react_agent/prompt.py +1 -6
  5. aiq/agent/react_agent/register.py +36 -35
  6. aiq/agent/rewoo_agent/agent.py +36 -35
  7. aiq/agent/rewoo_agent/register.py +2 -2
  8. aiq/agent/tool_calling_agent/agent.py +3 -7
  9. aiq/agent/tool_calling_agent/register.py +1 -1
  10. aiq/authentication/__init__.py +14 -0
  11. aiq/authentication/api_key/__init__.py +14 -0
  12. aiq/authentication/api_key/api_key_auth_provider.py +92 -0
  13. aiq/authentication/api_key/api_key_auth_provider_config.py +124 -0
  14. aiq/authentication/api_key/register.py +26 -0
  15. aiq/authentication/exceptions/__init__.py +14 -0
  16. aiq/authentication/exceptions/api_key_exceptions.py +38 -0
  17. aiq/authentication/exceptions/auth_code_grant_exceptions.py +86 -0
  18. aiq/authentication/exceptions/call_back_exceptions.py +38 -0
  19. aiq/authentication/exceptions/request_exceptions.py +54 -0
  20. aiq/authentication/http_basic_auth/__init__.py +0 -0
  21. aiq/authentication/http_basic_auth/http_basic_auth_provider.py +81 -0
  22. aiq/authentication/http_basic_auth/register.py +30 -0
  23. aiq/authentication/interfaces.py +93 -0
  24. aiq/authentication/oauth2/__init__.py +14 -0
  25. aiq/authentication/oauth2/oauth2_auth_code_flow_provider.py +107 -0
  26. aiq/authentication/oauth2/oauth2_auth_code_flow_provider_config.py +39 -0
  27. aiq/authentication/oauth2/register.py +25 -0
  28. aiq/authentication/register.py +21 -0
  29. aiq/builder/builder.py +64 -2
  30. aiq/builder/component_utils.py +16 -3
  31. aiq/builder/context.py +37 -0
  32. aiq/builder/eval_builder.py +43 -2
  33. aiq/builder/function.py +44 -12
  34. aiq/builder/function_base.py +1 -1
  35. aiq/builder/intermediate_step_manager.py +6 -8
  36. aiq/builder/user_interaction_manager.py +3 -0
  37. aiq/builder/workflow.py +23 -18
  38. aiq/builder/workflow_builder.py +421 -61
  39. aiq/cli/commands/info/list_mcp.py +103 -16
  40. aiq/cli/commands/sizing/__init__.py +14 -0
  41. aiq/cli/commands/sizing/calc.py +294 -0
  42. aiq/cli/commands/sizing/sizing.py +27 -0
  43. aiq/cli/commands/start.py +2 -1
  44. aiq/cli/entrypoint.py +2 -0
  45. aiq/cli/register_workflow.py +80 -0
  46. aiq/cli/type_registry.py +151 -30
  47. aiq/data_models/api_server.py +124 -12
  48. aiq/data_models/authentication.py +231 -0
  49. aiq/data_models/common.py +35 -7
  50. aiq/data_models/component.py +17 -9
  51. aiq/data_models/component_ref.py +33 -0
  52. aiq/data_models/config.py +60 -3
  53. aiq/data_models/dataset_handler.py +2 -1
  54. aiq/data_models/embedder.py +1 -0
  55. aiq/data_models/evaluate.py +23 -0
  56. aiq/data_models/function_dependencies.py +8 -0
  57. aiq/data_models/interactive.py +10 -1
  58. aiq/data_models/intermediate_step.py +38 -5
  59. aiq/data_models/its_strategy.py +30 -0
  60. aiq/data_models/llm.py +1 -0
  61. aiq/data_models/memory.py +1 -0
  62. aiq/data_models/object_store.py +44 -0
  63. aiq/data_models/profiler.py +1 -0
  64. aiq/data_models/retry_mixin.py +35 -0
  65. aiq/data_models/span.py +187 -0
  66. aiq/data_models/telemetry_exporter.py +2 -2
  67. aiq/embedder/nim_embedder.py +2 -1
  68. aiq/embedder/openai_embedder.py +2 -1
  69. aiq/eval/config.py +19 -1
  70. aiq/eval/dataset_handler/dataset_handler.py +87 -2
  71. aiq/eval/evaluate.py +208 -27
  72. aiq/eval/evaluator/base_evaluator.py +73 -0
  73. aiq/eval/evaluator/evaluator_model.py +1 -0
  74. aiq/eval/intermediate_step_adapter.py +11 -5
  75. aiq/eval/rag_evaluator/evaluate.py +55 -15
  76. aiq/eval/rag_evaluator/register.py +6 -1
  77. aiq/eval/remote_workflow.py +7 -2
  78. aiq/eval/runners/__init__.py +14 -0
  79. aiq/eval/runners/config.py +39 -0
  80. aiq/eval/runners/multi_eval_runner.py +54 -0
  81. aiq/eval/trajectory_evaluator/evaluate.py +22 -65
  82. aiq/eval/tunable_rag_evaluator/evaluate.py +150 -168
  83. aiq/eval/tunable_rag_evaluator/register.py +2 -0
  84. aiq/eval/usage_stats.py +41 -0
  85. aiq/eval/utils/output_uploader.py +10 -1
  86. aiq/eval/utils/weave_eval.py +184 -0
  87. aiq/experimental/__init__.py +0 -0
  88. aiq/experimental/decorators/__init__.py +0 -0
  89. aiq/experimental/decorators/experimental_warning_decorator.py +130 -0
  90. aiq/experimental/inference_time_scaling/__init__.py +0 -0
  91. aiq/experimental/inference_time_scaling/editing/__init__.py +0 -0
  92. aiq/experimental/inference_time_scaling/editing/iterative_plan_refinement_editor.py +147 -0
  93. aiq/experimental/inference_time_scaling/editing/llm_as_a_judge_editor.py +204 -0
  94. aiq/experimental/inference_time_scaling/editing/motivation_aware_summarization.py +107 -0
  95. aiq/experimental/inference_time_scaling/functions/__init__.py +0 -0
  96. aiq/experimental/inference_time_scaling/functions/execute_score_select_function.py +105 -0
  97. aiq/experimental/inference_time_scaling/functions/its_tool_orchestration_function.py +205 -0
  98. aiq/experimental/inference_time_scaling/functions/its_tool_wrapper_function.py +146 -0
  99. aiq/experimental/inference_time_scaling/functions/plan_select_execute_function.py +224 -0
  100. aiq/experimental/inference_time_scaling/models/__init__.py +0 -0
  101. aiq/experimental/inference_time_scaling/models/editor_config.py +132 -0
  102. aiq/experimental/inference_time_scaling/models/its_item.py +48 -0
  103. aiq/experimental/inference_time_scaling/models/scoring_config.py +112 -0
  104. aiq/experimental/inference_time_scaling/models/search_config.py +120 -0
  105. aiq/experimental/inference_time_scaling/models/selection_config.py +154 -0
  106. aiq/experimental/inference_time_scaling/models/stage_enums.py +43 -0
  107. aiq/experimental/inference_time_scaling/models/strategy_base.py +66 -0
  108. aiq/experimental/inference_time_scaling/models/tool_use_config.py +41 -0
  109. aiq/experimental/inference_time_scaling/register.py +36 -0
  110. aiq/experimental/inference_time_scaling/scoring/__init__.py +0 -0
  111. aiq/experimental/inference_time_scaling/scoring/llm_based_agent_scorer.py +168 -0
  112. aiq/experimental/inference_time_scaling/scoring/llm_based_plan_scorer.py +168 -0
  113. aiq/experimental/inference_time_scaling/scoring/motivation_aware_scorer.py +111 -0
  114. aiq/experimental/inference_time_scaling/search/__init__.py +0 -0
  115. aiq/experimental/inference_time_scaling/search/multi_llm_planner.py +128 -0
  116. aiq/experimental/inference_time_scaling/search/multi_query_retrieval_search.py +122 -0
  117. aiq/experimental/inference_time_scaling/search/single_shot_multi_plan_planner.py +128 -0
  118. aiq/experimental/inference_time_scaling/selection/__init__.py +0 -0
  119. aiq/experimental/inference_time_scaling/selection/best_of_n_selector.py +63 -0
  120. aiq/experimental/inference_time_scaling/selection/llm_based_agent_output_selector.py +131 -0
  121. aiq/experimental/inference_time_scaling/selection/llm_based_output_merging_selector.py +159 -0
  122. aiq/experimental/inference_time_scaling/selection/llm_based_plan_selector.py +128 -0
  123. aiq/experimental/inference_time_scaling/selection/threshold_selector.py +58 -0
  124. aiq/front_ends/console/authentication_flow_handler.py +233 -0
  125. aiq/front_ends/console/console_front_end_plugin.py +11 -2
  126. aiq/front_ends/fastapi/auth_flow_handlers/__init__.py +0 -0
  127. aiq/front_ends/fastapi/auth_flow_handlers/http_flow_handler.py +27 -0
  128. aiq/front_ends/fastapi/auth_flow_handlers/websocket_flow_handler.py +107 -0
  129. aiq/front_ends/fastapi/fastapi_front_end_config.py +93 -9
  130. aiq/front_ends/fastapi/fastapi_front_end_controller.py +68 -0
  131. aiq/front_ends/fastapi/fastapi_front_end_plugin.py +14 -1
  132. aiq/front_ends/fastapi/fastapi_front_end_plugin_worker.py +537 -52
  133. aiq/front_ends/fastapi/html_snippets/__init__.py +14 -0
  134. aiq/front_ends/fastapi/html_snippets/auth_code_grant_success.py +35 -0
  135. aiq/front_ends/fastapi/job_store.py +47 -25
  136. aiq/front_ends/fastapi/main.py +2 -0
  137. aiq/front_ends/fastapi/message_handler.py +108 -89
  138. aiq/front_ends/fastapi/step_adaptor.py +2 -1
  139. aiq/llm/aws_bedrock_llm.py +57 -0
  140. aiq/llm/nim_llm.py +2 -1
  141. aiq/llm/openai_llm.py +3 -2
  142. aiq/llm/register.py +1 -0
  143. aiq/meta/pypi.md +12 -12
  144. aiq/object_store/__init__.py +20 -0
  145. aiq/object_store/in_memory_object_store.py +74 -0
  146. aiq/object_store/interfaces.py +84 -0
  147. aiq/object_store/models.py +36 -0
  148. aiq/object_store/register.py +20 -0
  149. aiq/observability/__init__.py +14 -0
  150. aiq/observability/exporter/__init__.py +14 -0
  151. aiq/observability/exporter/base_exporter.py +449 -0
  152. aiq/observability/exporter/exporter.py +78 -0
  153. aiq/observability/exporter/file_exporter.py +33 -0
  154. aiq/observability/exporter/processing_exporter.py +269 -0
  155. aiq/observability/exporter/raw_exporter.py +52 -0
  156. aiq/observability/exporter/span_exporter.py +264 -0
  157. aiq/observability/exporter_manager.py +335 -0
  158. aiq/observability/mixin/__init__.py +14 -0
  159. aiq/observability/mixin/batch_config_mixin.py +26 -0
  160. aiq/observability/mixin/collector_config_mixin.py +23 -0
  161. aiq/observability/mixin/file_mixin.py +288 -0
  162. aiq/observability/mixin/file_mode.py +23 -0
  163. aiq/observability/mixin/resource_conflict_mixin.py +134 -0
  164. aiq/observability/mixin/serialize_mixin.py +61 -0
  165. aiq/observability/mixin/type_introspection_mixin.py +183 -0
  166. aiq/observability/processor/__init__.py +14 -0
  167. aiq/observability/processor/batching_processor.py +316 -0
  168. aiq/observability/processor/intermediate_step_serializer.py +28 -0
  169. aiq/observability/processor/processor.py +68 -0
  170. aiq/observability/register.py +36 -39
  171. aiq/observability/utils/__init__.py +14 -0
  172. aiq/observability/utils/dict_utils.py +236 -0
  173. aiq/observability/utils/time_utils.py +31 -0
  174. aiq/profiler/calc/__init__.py +14 -0
  175. aiq/profiler/calc/calc_runner.py +623 -0
  176. aiq/profiler/calc/calculations.py +288 -0
  177. aiq/profiler/calc/data_models.py +176 -0
  178. aiq/profiler/calc/plot.py +345 -0
  179. aiq/profiler/callbacks/langchain_callback_handler.py +22 -10
  180. aiq/profiler/data_models.py +24 -0
  181. aiq/profiler/inference_metrics_model.py +3 -0
  182. aiq/profiler/inference_optimization/bottleneck_analysis/nested_stack_analysis.py +8 -0
  183. aiq/profiler/inference_optimization/data_models.py +2 -2
  184. aiq/profiler/inference_optimization/llm_metrics.py +2 -2
  185. aiq/profiler/profile_runner.py +61 -21
  186. aiq/runtime/loader.py +9 -3
  187. aiq/runtime/runner.py +23 -9
  188. aiq/runtime/session.py +25 -7
  189. aiq/runtime/user_metadata.py +2 -3
  190. aiq/tool/chat_completion.py +74 -0
  191. aiq/tool/code_execution/README.md +152 -0
  192. aiq/tool/code_execution/code_sandbox.py +151 -72
  193. aiq/tool/code_execution/local_sandbox/.gitignore +1 -0
  194. aiq/tool/code_execution/local_sandbox/local_sandbox_server.py +139 -24
  195. aiq/tool/code_execution/local_sandbox/sandbox.requirements.txt +3 -1
  196. aiq/tool/code_execution/local_sandbox/start_local_sandbox.sh +27 -2
  197. aiq/tool/code_execution/register.py +7 -3
  198. aiq/tool/code_execution/test_code_execution_sandbox.py +414 -0
  199. aiq/tool/mcp/exceptions.py +142 -0
  200. aiq/tool/mcp/mcp_client.py +41 -6
  201. aiq/tool/mcp/mcp_tool.py +3 -2
  202. aiq/tool/register.py +1 -0
  203. aiq/tool/server_tools.py +6 -3
  204. aiq/utils/exception_handlers/automatic_retries.py +289 -0
  205. aiq/utils/exception_handlers/mcp.py +211 -0
  206. aiq/utils/io/model_processing.py +28 -0
  207. aiq/utils/log_utils.py +37 -0
  208. aiq/utils/string_utils.py +38 -0
  209. aiq/utils/type_converter.py +18 -2
  210. aiq/utils/type_utils.py +87 -0
  211. {aiqtoolkit-1.2.0.dev0.dist-info → aiqtoolkit-1.2.0rc1.dist-info}/METADATA +53 -21
  212. aiqtoolkit-1.2.0rc1.dist-info/RECORD +436 -0
  213. {aiqtoolkit-1.2.0.dev0.dist-info → aiqtoolkit-1.2.0rc1.dist-info}/WHEEL +1 -1
  214. {aiqtoolkit-1.2.0.dev0.dist-info → aiqtoolkit-1.2.0rc1.dist-info}/entry_points.txt +3 -0
  215. aiq/front_ends/fastapi/websocket.py +0 -148
  216. aiq/observability/async_otel_listener.py +0 -429
  217. aiqtoolkit-1.2.0.dev0.dist-info/RECORD +0 -316
  218. {aiqtoolkit-1.2.0.dev0.dist-info → aiqtoolkit-1.2.0rc1.dist-info}/licenses/LICENSE-3rd-party.txt +0 -0
  219. {aiqtoolkit-1.2.0.dev0.dist-info → aiqtoolkit-1.2.0rc1.dist-info}/licenses/LICENSE.md +0 -0
  220. {aiqtoolkit-1.2.0.dev0.dist-info → aiqtoolkit-1.2.0rc1.dist-info}/top_level.txt +0 -0
@@ -1,429 +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 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
-
61
- trace = DummyTrace # pylint: disable=invalid-name
62
- TracerProvider = DummyTracerProvider
63
- Span = DummySpan
64
- set_span_in_context = dummy_set_span_in_context
65
-
66
-
67
- def _ns_timestamp(seconds_float: float) -> int:
68
- """
69
- Convert AIQ Toolkit's float `event_timestamp` (in seconds) into an integer number
70
- of nanoseconds, as OpenTelemetry expects.
71
- """
72
- return int(seconds_float * 1e9)
73
-
74
-
75
- class AsyncOtelSpanListener:
76
- """
77
- A separate, async class that listens to the AIQ Toolkit intermediate step
78
- event stream and creates proper Otel spans:
79
-
80
- - On FUNCTION_START => open a new top-level span
81
- - On any other intermediate step => open a child subspan (immediate open/close)
82
- - On FUNCTION_END => close the function's top-level span
83
-
84
- This runs fully independently from the normal AIQ Toolkit workflow, so that
85
- the workflow is not blocking or entangled by OTel calls.
86
- """
87
-
88
- def __init__(self, context_state: AIQContextState | None = None):
89
- """
90
- :param context_state: Optionally supply a specific AIQContextState.
91
- If None, uses the global singleton.
92
- """
93
- self._context_state = context_state or AIQContextState.get()
94
-
95
- # Maintain a subscription so we can unsubscribe on shutdown
96
- self._subscription = None
97
-
98
- # Outstanding spans which have been opened but not yet closed
99
- self._outstanding_spans: dict[str, Span] = {}
100
-
101
- # Stack of spans, for when we need to create a child span
102
- self._span_stack: dict[str, Span] = {}
103
-
104
- self._running = False
105
-
106
- # Prepare the tracer (optionally you might already have done this)
107
- if trace.get_tracer_provider() is None or not isinstance(trace.get_tracer_provider(), TracerProvider):
108
- tracer_provider = TracerProvider()
109
- trace.set_tracer_provider(tracer_provider)
110
-
111
- # We'll optionally attach exporters if you want (out of scope to do it here).
112
- # Example: tracer_provider.add_span_processor(BatchSpanProcessor(your_exporter))
113
-
114
- self._tracer = trace.get_tracer("aiq-async-otel-listener")
115
-
116
- # Initialize Weave-specific components if available
117
- self.gc = None
118
- self._weave_calls = {}
119
- if WEAVE_AVAILABLE:
120
- try:
121
- # Try to get the weave client, but don't fail if Weave isn't initialized
122
- self.gc = weave_client_context.require_weave_client()
123
- except Exception:
124
- # Weave is not initialized, so we don't do anything
125
- pass
126
-
127
- def _on_next(self, step: IntermediateStep) -> None:
128
- """
129
- The main logic that reacts to each IntermediateStep.
130
- """
131
- if (step.event_state == IntermediateStepState.START):
132
-
133
- self._process_start_event(step)
134
-
135
- elif (step.event_state == IntermediateStepState.END):
136
-
137
- self._process_end_event(step)
138
-
139
- def _on_error(self, exc: Exception) -> None:
140
- logger.error("Error in intermediate step subscription: %s", exc, exc_info=True)
141
-
142
- def _on_complete(self) -> None:
143
- logger.debug("Intermediate step stream completed. No more events will arrive.")
144
-
145
- @asynccontextmanager
146
- async def start(self):
147
- """
148
- Usage::
149
-
150
- otel_listener = AsyncOtelSpanListener()
151
- async with otel_listener.start():
152
- # run your AIQ Toolkit workflow
153
- ...
154
- # cleans up
155
-
156
- This sets up the subscription to the AIQ Toolkit event stream and starts the background loop.
157
- """
158
- try:
159
- # Subscribe to the event stream
160
- subject = self._context_state.event_stream.get()
161
- self._subscription = subject.subscribe(
162
- on_next=self._on_next,
163
- on_error=self._on_error,
164
- on_complete=self._on_complete,
165
- )
166
-
167
- self._running = True
168
-
169
- yield # let the caller do their workflow
170
-
171
- finally:
172
- # Cleanup
173
- self._running = False
174
- # Close out any running spans
175
- await self._cleanup()
176
-
177
- if self._subscription:
178
- self._subscription.unsubscribe()
179
- self._subscription = None
180
-
181
- async def _cleanup(self):
182
- """
183
- Close any remaining open spans.
184
- """
185
- if self._outstanding_spans:
186
- logger.warning(
187
- "Not all spans were closed. Ensure all start events have a corresponding end event. Remaining: %s",
188
- self._outstanding_spans)
189
-
190
- for span_info in self._outstanding_spans.values():
191
- span_info.end()
192
-
193
- self._outstanding_spans.clear()
194
-
195
- self._span_stack.clear()
196
-
197
- # Clean up any lingering Weave calls if Weave is available and initialized
198
- if self.gc is not None and self._weave_calls:
199
- for _, call in list(self._weave_calls.items()):
200
- self.gc.finish_call(call, {"status": "incomplete"})
201
- self._weave_calls.clear()
202
-
203
- def _serialize_payload(self, input_value: Any) -> tuple[str, bool]:
204
- """
205
- Serialize the input value to a string. Returns a tuple with the serialized value and a boolean indicating if the
206
- serialization is JSON or a string
207
- """
208
- try:
209
- return TypeAdapter(type(input_value)).dump_json(input_value).decode('utf-8'), True
210
- except Exception:
211
- # Fallback to string representation if we can't serialize using pydantic
212
- return str(input_value), False
213
-
214
- def _process_start_event(self, step: IntermediateStep):
215
-
216
- parent_ctx = None
217
-
218
- if (len(self._span_stack) > 0):
219
- parent_span = self._span_stack.get(step.function_ancestry.parent_id, None)
220
- if parent_span is None:
221
- logger.warning("No parent span found for step %s", step.UUID)
222
- return
223
-
224
- parent_ctx = set_span_in_context(parent_span)
225
-
226
- # Extract start/end times from the step
227
- # By convention, `span_event_timestamp` is the time we started, `event_timestamp` is the time we ended.
228
- # If span_event_timestamp is missing, we default to event_timestamp (meaning zero-length).
229
- s_ts = step.payload.span_event_timestamp or step.payload.event_timestamp
230
- start_ns = _ns_timestamp(s_ts)
231
-
232
- # Optional: embed the LLM/tool name if present
233
- if step.payload.name:
234
- sub_span_name = f"{step.payload.name}"
235
- else:
236
- sub_span_name = f"{step.payload.event_type}"
237
-
238
- # Start the subspan
239
- sub_span = self._tracer.start_span(
240
- name=sub_span_name,
241
- context=parent_ctx,
242
- attributes={
243
- "aiq.event_type": step.payload.event_type.value,
244
- "aiq.function.id": step.function_ancestry.function_id,
245
- "aiq.function.name": step.function_ancestry.function_name,
246
- "aiq.subspan.name": step.payload.name or "",
247
- "aiq.event_timestamp": step.event_timestamp,
248
- "aiq.framework": step.payload.framework.value if step.payload.framework else "unknown",
249
- },
250
- start_time=start_ns,
251
- )
252
-
253
- event_type_to_span_kind = {
254
- "LLM_START": OpenInferenceSpanKindValues.LLM,
255
- "LLM_END": OpenInferenceSpanKindValues.LLM,
256
- "LLM_NEW_TOKEN": OpenInferenceSpanKindValues.LLM,
257
- "TOOL_START": OpenInferenceSpanKindValues.TOOL,
258
- "TOOL_END": OpenInferenceSpanKindValues.TOOL,
259
- "FUNCTION_START": OpenInferenceSpanKindValues.CHAIN,
260
- "FUNCTION_END": OpenInferenceSpanKindValues.CHAIN,
261
- }
262
-
263
- span_kind = event_type_to_span_kind.get(step.event_type, OpenInferenceSpanKindValues.UNKNOWN)
264
- sub_span.set_attribute(SpanAttributes.OPENINFERENCE_SPAN_KIND, span_kind.value)
265
-
266
- if step.payload.data and step.payload.data.input:
267
- # optional parse
268
- match = re.search(r"Human:\s*Question:\s*(.*)", str(step.payload.data.input))
269
- if match:
270
- human_question = match.group(1).strip()
271
- sub_span.set_attribute(SpanAttributes.INPUT_VALUE, human_question)
272
- else:
273
- serialized_input, is_json = self._serialize_payload(step.payload.data.input)
274
- sub_span.set_attribute(SpanAttributes.INPUT_VALUE, serialized_input)
275
- sub_span.set_attribute(SpanAttributes.INPUT_MIME_TYPE, "application/json" if is_json else "text/plain")
276
-
277
- self._span_stack[step.UUID] = sub_span
278
-
279
- self._outstanding_spans[step.UUID] = sub_span
280
-
281
- # Create corresponding Weave call if Weave is available and initialized
282
- if self.gc is not None:
283
- self._create_weave_call(step, sub_span)
284
-
285
- def _process_end_event(self, step: IntermediateStep):
286
-
287
- # Find the subspan that was created in the start event
288
- sub_span = self._outstanding_spans.pop(step.UUID, None)
289
-
290
- if sub_span is None:
291
- logger.warning("No subspan found for step %s", step.UUID)
292
- return
293
-
294
- self._span_stack.pop(step.UUID, None)
295
-
296
- # Optionally add more attributes from usage_info or data
297
- usage_info = step.payload.usage_info
298
- if usage_info:
299
- sub_span.set_attribute("aiq.usage.num_llm_calls",
300
- usage_info.num_llm_calls if usage_info.num_llm_calls else 0)
301
- sub_span.set_attribute("aiq.usage.seconds_between_calls",
302
- usage_info.seconds_between_calls if usage_info.seconds_between_calls else 0)
303
- sub_span.set_attribute(SpanAttributes.LLM_TOKEN_COUNT_PROMPT,
304
- usage_info.token_usage.prompt_tokens if usage_info.token_usage else 0)
305
- sub_span.set_attribute(SpanAttributes.LLM_TOKEN_COUNT_COMPLETION,
306
- usage_info.token_usage.completion_tokens if usage_info.token_usage else 0)
307
- sub_span.set_attribute(SpanAttributes.LLM_TOKEN_COUNT_TOTAL,
308
- usage_info.token_usage.total_tokens if usage_info.token_usage else 0)
309
-
310
- if step.payload.data and step.payload.data.output is not None:
311
- serialized_output, is_json = self._serialize_payload(step.payload.data.output)
312
- sub_span.set_attribute(SpanAttributes.OUTPUT_VALUE, serialized_output)
313
- sub_span.set_attribute(SpanAttributes.OUTPUT_MIME_TYPE, "application/json" if is_json else "text/plain")
314
-
315
- end_ns = _ns_timestamp(step.payload.event_timestamp)
316
-
317
- # End the subspan
318
- sub_span.end(end_time=end_ns)
319
-
320
- # Finish corresponding Weave call if Weave is available and initialized
321
- if self.gc is not None:
322
- self._finish_weave_call(step)
323
-
324
- @contextmanager
325
- def parent_call(self, trace_id: str, parent_call_id: str):
326
- """Context manager to set a parent call context for Weave.
327
- This allows connecting AIQ spans to existing traces from other frameworks.
328
- """
329
- dummy_call = Call(trace_id=trace_id, id=parent_call_id, _op_name="", project_id="", parent_id=None, inputs={})
330
- with set_call_stack([dummy_call]):
331
- yield
332
-
333
- def _create_weave_call(self, step: IntermediateStep, span: Span) -> None:
334
- """
335
- Create a Weave call directly from the span and step data,
336
- connecting to existing framework traces if available.
337
- """
338
- # Check for existing Weave trace/call
339
- existing_call = get_current_call()
340
-
341
- # Extract parent call if applicable
342
- parent_call = None
343
-
344
- # If we have an existing Weave call from another framework (e.g., LangChain),
345
- # use it as the parent
346
- if existing_call is not None:
347
- parent_call = existing_call
348
- logger.debug("Found existing Weave call: %s from trace: %s", existing_call.id, existing_call.trace_id)
349
- # Otherwise, check our internal stack for parent relationships
350
- elif len(self._weave_calls) > 0 and len(self._span_stack) > 1:
351
- # Get the parent span using stack position (one level up)
352
- parent_span_id = self._span_stack[-2].get_span_context().span_id
353
- # Find the corresponding weave call for this parent span
354
- for call in self._weave_calls.values():
355
- if getattr(call, "span_id", None) == parent_span_id:
356
- parent_call = call
357
- break
358
-
359
- # Generate a meaningful operation name based on event type
360
- event_type = step.payload.event_type.split(".")[-1]
361
- if step.payload.name:
362
- op_name = f"aiq.{event_type}.{step.payload.name}"
363
- else:
364
- op_name = f"aiq.{event_type}"
365
-
366
- # Create input dictionary
367
- inputs = {}
368
- if step.payload.data and step.payload.data.input is not None:
369
- try:
370
- # Add the input to the Weave call
371
- inputs["input"] = step.payload.data.input
372
- except Exception:
373
- # If serialization fails, use string representation
374
- inputs["input"] = str(step.payload.data.input)
375
-
376
- # Create the Weave call
377
- call = self.gc.create_call(
378
- op_name,
379
- inputs=inputs,
380
- parent=parent_call,
381
- attributes=span.attributes,
382
- display_name=op_name,
383
- )
384
-
385
- # Store the call with step UUID as key
386
- self._weave_calls[step.UUID] = call
387
-
388
- # Store span ID for parent reference
389
- setattr(call, "span_id", span.get_span_context().span_id)
390
-
391
- return call
392
-
393
- def _finish_weave_call(self, step: IntermediateStep) -> None:
394
- """
395
- Finish a previously created Weave call
396
- """
397
- # Find the call for this step
398
- call = self._weave_calls.pop(step.UUID, None)
399
-
400
- if call is None:
401
- logger.warning("No Weave call found for step %s", step.UUID)
402
- return
403
-
404
- # Create output dictionary
405
- outputs = {}
406
- if step.payload.data and step.payload.data.output is not None:
407
- try:
408
- # Add the output to the Weave call
409
- outputs["output"] = step.payload.data.output
410
- except Exception:
411
- # If serialization fails, use string representation
412
- outputs["output"] = str(step.payload.data.output)
413
-
414
- # Add usage information if available
415
- usage_info = step.payload.usage_info
416
- if usage_info:
417
- if usage_info.token_usage:
418
- outputs["prompt_tokens"] = usage_info.token_usage.prompt_tokens
419
- outputs["completion_tokens"] = usage_info.token_usage.completion_tokens
420
- outputs["total_tokens"] = usage_info.token_usage.total_tokens
421
-
422
- if usage_info.num_llm_calls:
423
- outputs["num_llm_calls"] = usage_info.num_llm_calls
424
-
425
- if usage_info.seconds_between_calls:
426
- outputs["seconds_between_calls"] = usage_info.seconds_between_calls
427
-
428
- # Finish the call with outputs
429
- self.gc.finish_call(call, outputs)