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.
- aiq/agent/base.py +170 -8
- aiq/agent/dual_node.py +1 -1
- aiq/agent/react_agent/agent.py +146 -112
- aiq/agent/react_agent/prompt.py +1 -6
- aiq/agent/react_agent/register.py +36 -35
- aiq/agent/rewoo_agent/agent.py +36 -35
- aiq/agent/rewoo_agent/register.py +2 -2
- aiq/agent/tool_calling_agent/agent.py +3 -7
- aiq/agent/tool_calling_agent/register.py +1 -1
- aiq/authentication/__init__.py +14 -0
- aiq/authentication/api_key/__init__.py +14 -0
- aiq/authentication/api_key/api_key_auth_provider.py +92 -0
- aiq/authentication/api_key/api_key_auth_provider_config.py +124 -0
- aiq/authentication/api_key/register.py +26 -0
- aiq/authentication/exceptions/__init__.py +14 -0
- aiq/authentication/exceptions/api_key_exceptions.py +38 -0
- aiq/authentication/exceptions/auth_code_grant_exceptions.py +86 -0
- aiq/authentication/exceptions/call_back_exceptions.py +38 -0
- aiq/authentication/exceptions/request_exceptions.py +54 -0
- aiq/authentication/http_basic_auth/__init__.py +0 -0
- aiq/authentication/http_basic_auth/http_basic_auth_provider.py +81 -0
- aiq/authentication/http_basic_auth/register.py +30 -0
- aiq/authentication/interfaces.py +93 -0
- aiq/authentication/oauth2/__init__.py +14 -0
- aiq/authentication/oauth2/oauth2_auth_code_flow_provider.py +107 -0
- aiq/authentication/oauth2/oauth2_auth_code_flow_provider_config.py +39 -0
- aiq/authentication/oauth2/register.py +25 -0
- aiq/authentication/register.py +21 -0
- aiq/builder/builder.py +64 -2
- aiq/builder/component_utils.py +16 -3
- aiq/builder/context.py +37 -0
- aiq/builder/eval_builder.py +43 -2
- aiq/builder/function.py +44 -12
- aiq/builder/function_base.py +1 -1
- aiq/builder/intermediate_step_manager.py +6 -8
- aiq/builder/user_interaction_manager.py +3 -0
- aiq/builder/workflow.py +23 -18
- aiq/builder/workflow_builder.py +421 -61
- aiq/cli/commands/info/list_mcp.py +103 -16
- aiq/cli/commands/sizing/__init__.py +14 -0
- aiq/cli/commands/sizing/calc.py +294 -0
- aiq/cli/commands/sizing/sizing.py +27 -0
- aiq/cli/commands/start.py +2 -1
- aiq/cli/entrypoint.py +2 -0
- aiq/cli/register_workflow.py +80 -0
- aiq/cli/type_registry.py +151 -30
- aiq/data_models/api_server.py +124 -12
- aiq/data_models/authentication.py +231 -0
- aiq/data_models/common.py +35 -7
- aiq/data_models/component.py +17 -9
- aiq/data_models/component_ref.py +33 -0
- aiq/data_models/config.py +60 -3
- aiq/data_models/dataset_handler.py +2 -1
- aiq/data_models/embedder.py +1 -0
- aiq/data_models/evaluate.py +23 -0
- aiq/data_models/function_dependencies.py +8 -0
- aiq/data_models/interactive.py +10 -1
- aiq/data_models/intermediate_step.py +38 -5
- aiq/data_models/its_strategy.py +30 -0
- aiq/data_models/llm.py +1 -0
- aiq/data_models/memory.py +1 -0
- aiq/data_models/object_store.py +44 -0
- aiq/data_models/profiler.py +1 -0
- aiq/data_models/retry_mixin.py +35 -0
- aiq/data_models/span.py +187 -0
- aiq/data_models/telemetry_exporter.py +2 -2
- aiq/embedder/nim_embedder.py +2 -1
- aiq/embedder/openai_embedder.py +2 -1
- aiq/eval/config.py +19 -1
- aiq/eval/dataset_handler/dataset_handler.py +87 -2
- aiq/eval/evaluate.py +208 -27
- aiq/eval/evaluator/base_evaluator.py +73 -0
- aiq/eval/evaluator/evaluator_model.py +1 -0
- aiq/eval/intermediate_step_adapter.py +11 -5
- aiq/eval/rag_evaluator/evaluate.py +55 -15
- aiq/eval/rag_evaluator/register.py +6 -1
- aiq/eval/remote_workflow.py +7 -2
- aiq/eval/runners/__init__.py +14 -0
- aiq/eval/runners/config.py +39 -0
- aiq/eval/runners/multi_eval_runner.py +54 -0
- aiq/eval/trajectory_evaluator/evaluate.py +22 -65
- aiq/eval/tunable_rag_evaluator/evaluate.py +150 -168
- aiq/eval/tunable_rag_evaluator/register.py +2 -0
- aiq/eval/usage_stats.py +41 -0
- aiq/eval/utils/output_uploader.py +10 -1
- aiq/eval/utils/weave_eval.py +184 -0
- aiq/experimental/__init__.py +0 -0
- aiq/experimental/decorators/__init__.py +0 -0
- aiq/experimental/decorators/experimental_warning_decorator.py +130 -0
- aiq/experimental/inference_time_scaling/__init__.py +0 -0
- aiq/experimental/inference_time_scaling/editing/__init__.py +0 -0
- aiq/experimental/inference_time_scaling/editing/iterative_plan_refinement_editor.py +147 -0
- aiq/experimental/inference_time_scaling/editing/llm_as_a_judge_editor.py +204 -0
- aiq/experimental/inference_time_scaling/editing/motivation_aware_summarization.py +107 -0
- aiq/experimental/inference_time_scaling/functions/__init__.py +0 -0
- aiq/experimental/inference_time_scaling/functions/execute_score_select_function.py +105 -0
- aiq/experimental/inference_time_scaling/functions/its_tool_orchestration_function.py +205 -0
- aiq/experimental/inference_time_scaling/functions/its_tool_wrapper_function.py +146 -0
- aiq/experimental/inference_time_scaling/functions/plan_select_execute_function.py +224 -0
- aiq/experimental/inference_time_scaling/models/__init__.py +0 -0
- aiq/experimental/inference_time_scaling/models/editor_config.py +132 -0
- aiq/experimental/inference_time_scaling/models/its_item.py +48 -0
- aiq/experimental/inference_time_scaling/models/scoring_config.py +112 -0
- aiq/experimental/inference_time_scaling/models/search_config.py +120 -0
- aiq/experimental/inference_time_scaling/models/selection_config.py +154 -0
- aiq/experimental/inference_time_scaling/models/stage_enums.py +43 -0
- aiq/experimental/inference_time_scaling/models/strategy_base.py +66 -0
- aiq/experimental/inference_time_scaling/models/tool_use_config.py +41 -0
- aiq/experimental/inference_time_scaling/register.py +36 -0
- aiq/experimental/inference_time_scaling/scoring/__init__.py +0 -0
- aiq/experimental/inference_time_scaling/scoring/llm_based_agent_scorer.py +168 -0
- aiq/experimental/inference_time_scaling/scoring/llm_based_plan_scorer.py +168 -0
- aiq/experimental/inference_time_scaling/scoring/motivation_aware_scorer.py +111 -0
- aiq/experimental/inference_time_scaling/search/__init__.py +0 -0
- aiq/experimental/inference_time_scaling/search/multi_llm_planner.py +128 -0
- aiq/experimental/inference_time_scaling/search/multi_query_retrieval_search.py +122 -0
- aiq/experimental/inference_time_scaling/search/single_shot_multi_plan_planner.py +128 -0
- aiq/experimental/inference_time_scaling/selection/__init__.py +0 -0
- aiq/experimental/inference_time_scaling/selection/best_of_n_selector.py +63 -0
- aiq/experimental/inference_time_scaling/selection/llm_based_agent_output_selector.py +131 -0
- aiq/experimental/inference_time_scaling/selection/llm_based_output_merging_selector.py +159 -0
- aiq/experimental/inference_time_scaling/selection/llm_based_plan_selector.py +128 -0
- aiq/experimental/inference_time_scaling/selection/threshold_selector.py +58 -0
- aiq/front_ends/console/authentication_flow_handler.py +233 -0
- aiq/front_ends/console/console_front_end_plugin.py +11 -2
- aiq/front_ends/fastapi/auth_flow_handlers/__init__.py +0 -0
- aiq/front_ends/fastapi/auth_flow_handlers/http_flow_handler.py +27 -0
- aiq/front_ends/fastapi/auth_flow_handlers/websocket_flow_handler.py +107 -0
- aiq/front_ends/fastapi/fastapi_front_end_config.py +93 -9
- aiq/front_ends/fastapi/fastapi_front_end_controller.py +68 -0
- aiq/front_ends/fastapi/fastapi_front_end_plugin.py +14 -1
- aiq/front_ends/fastapi/fastapi_front_end_plugin_worker.py +537 -52
- aiq/front_ends/fastapi/html_snippets/__init__.py +14 -0
- aiq/front_ends/fastapi/html_snippets/auth_code_grant_success.py +35 -0
- aiq/front_ends/fastapi/job_store.py +47 -25
- aiq/front_ends/fastapi/main.py +2 -0
- aiq/front_ends/fastapi/message_handler.py +108 -89
- aiq/front_ends/fastapi/step_adaptor.py +2 -1
- aiq/llm/aws_bedrock_llm.py +57 -0
- aiq/llm/nim_llm.py +2 -1
- aiq/llm/openai_llm.py +3 -2
- aiq/llm/register.py +1 -0
- aiq/meta/pypi.md +12 -12
- aiq/object_store/__init__.py +20 -0
- aiq/object_store/in_memory_object_store.py +74 -0
- aiq/object_store/interfaces.py +84 -0
- aiq/object_store/models.py +36 -0
- aiq/object_store/register.py +20 -0
- aiq/observability/__init__.py +14 -0
- aiq/observability/exporter/__init__.py +14 -0
- aiq/observability/exporter/base_exporter.py +449 -0
- aiq/observability/exporter/exporter.py +78 -0
- aiq/observability/exporter/file_exporter.py +33 -0
- aiq/observability/exporter/processing_exporter.py +269 -0
- aiq/observability/exporter/raw_exporter.py +52 -0
- aiq/observability/exporter/span_exporter.py +264 -0
- aiq/observability/exporter_manager.py +335 -0
- aiq/observability/mixin/__init__.py +14 -0
- aiq/observability/mixin/batch_config_mixin.py +26 -0
- aiq/observability/mixin/collector_config_mixin.py +23 -0
- aiq/observability/mixin/file_mixin.py +288 -0
- aiq/observability/mixin/file_mode.py +23 -0
- aiq/observability/mixin/resource_conflict_mixin.py +134 -0
- aiq/observability/mixin/serialize_mixin.py +61 -0
- aiq/observability/mixin/type_introspection_mixin.py +183 -0
- aiq/observability/processor/__init__.py +14 -0
- aiq/observability/processor/batching_processor.py +316 -0
- aiq/observability/processor/intermediate_step_serializer.py +28 -0
- aiq/observability/processor/processor.py +68 -0
- aiq/observability/register.py +36 -39
- aiq/observability/utils/__init__.py +14 -0
- aiq/observability/utils/dict_utils.py +236 -0
- aiq/observability/utils/time_utils.py +31 -0
- aiq/profiler/calc/__init__.py +14 -0
- aiq/profiler/calc/calc_runner.py +623 -0
- aiq/profiler/calc/calculations.py +288 -0
- aiq/profiler/calc/data_models.py +176 -0
- aiq/profiler/calc/plot.py +345 -0
- aiq/profiler/callbacks/langchain_callback_handler.py +22 -10
- aiq/profiler/data_models.py +24 -0
- aiq/profiler/inference_metrics_model.py +3 -0
- aiq/profiler/inference_optimization/bottleneck_analysis/nested_stack_analysis.py +8 -0
- aiq/profiler/inference_optimization/data_models.py +2 -2
- aiq/profiler/inference_optimization/llm_metrics.py +2 -2
- aiq/profiler/profile_runner.py +61 -21
- aiq/runtime/loader.py +9 -3
- aiq/runtime/runner.py +23 -9
- aiq/runtime/session.py +25 -7
- aiq/runtime/user_metadata.py +2 -3
- aiq/tool/chat_completion.py +74 -0
- aiq/tool/code_execution/README.md +152 -0
- aiq/tool/code_execution/code_sandbox.py +151 -72
- aiq/tool/code_execution/local_sandbox/.gitignore +1 -0
- aiq/tool/code_execution/local_sandbox/local_sandbox_server.py +139 -24
- aiq/tool/code_execution/local_sandbox/sandbox.requirements.txt +3 -1
- aiq/tool/code_execution/local_sandbox/start_local_sandbox.sh +27 -2
- aiq/tool/code_execution/register.py +7 -3
- aiq/tool/code_execution/test_code_execution_sandbox.py +414 -0
- aiq/tool/mcp/exceptions.py +142 -0
- aiq/tool/mcp/mcp_client.py +41 -6
- aiq/tool/mcp/mcp_tool.py +3 -2
- aiq/tool/register.py +1 -0
- aiq/tool/server_tools.py +6 -3
- aiq/utils/exception_handlers/automatic_retries.py +289 -0
- aiq/utils/exception_handlers/mcp.py +211 -0
- aiq/utils/io/model_processing.py +28 -0
- aiq/utils/log_utils.py +37 -0
- aiq/utils/string_utils.py +38 -0
- aiq/utils/type_converter.py +18 -2
- aiq/utils/type_utils.py +87 -0
- {aiqtoolkit-1.2.0.dev0.dist-info → aiqtoolkit-1.2.0rc1.dist-info}/METADATA +53 -21
- aiqtoolkit-1.2.0rc1.dist-info/RECORD +436 -0
- {aiqtoolkit-1.2.0.dev0.dist-info → aiqtoolkit-1.2.0rc1.dist-info}/WHEEL +1 -1
- {aiqtoolkit-1.2.0.dev0.dist-info → aiqtoolkit-1.2.0rc1.dist-info}/entry_points.txt +3 -0
- aiq/front_ends/fastapi/websocket.py +0 -148
- aiq/observability/async_otel_listener.py +0 -429
- aiqtoolkit-1.2.0.dev0.dist-info/RECORD +0 -316
- {aiqtoolkit-1.2.0.dev0.dist-info → aiqtoolkit-1.2.0rc1.dist-info}/licenses/LICENSE-3rd-party.txt +0 -0
- {aiqtoolkit-1.2.0.dev0.dist-info → aiqtoolkit-1.2.0rc1.dist-info}/licenses/LICENSE.md +0 -0
- {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)
|