aiqa-client 0.4.7__py3-none-any.whl → 0.6.1__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.
- aiqa/__init__.py +9 -3
- aiqa/client.py +113 -16
- aiqa/constants.py +1 -1
- aiqa/experiment_runner.py +248 -77
- aiqa/http_utils.py +85 -11
- aiqa/llm_as_judge.py +281 -0
- aiqa/span_helpers.py +511 -0
- aiqa/tracing.py +202 -566
- aiqa/tracing_llm_utils.py +20 -9
- aiqa/types.py +61 -0
- {aiqa_client-0.4.7.dist-info → aiqa_client-0.6.1.dist-info}/METADATA +1 -1
- aiqa_client-0.6.1.dist-info/RECORD +17 -0
- {aiqa_client-0.4.7.dist-info → aiqa_client-0.6.1.dist-info}/WHEEL +1 -1
- aiqa/aiqa_exporter.py +0 -772
- aiqa_client-0.4.7.dist-info/RECORD +0 -15
- {aiqa_client-0.4.7.dist-info → aiqa_client-0.6.1.dist-info}/licenses/LICENSE.txt +0 -0
- {aiqa_client-0.4.7.dist-info → aiqa_client-0.6.1.dist-info}/top_level.txt +0 -0
aiqa/tracing.py
CHANGED
|
@@ -1,53 +1,24 @@
|
|
|
1
1
|
"""
|
|
2
|
-
OpenTelemetry tracing
|
|
3
|
-
Provides WithTracing decorator to automatically trace function calls.
|
|
2
|
+
OpenTelemetry tracing decorator. Provides WithTracing decorator to automatically trace function calls.
|
|
4
3
|
"""
|
|
5
4
|
|
|
6
|
-
import json
|
|
7
5
|
import logging
|
|
8
6
|
import inspect
|
|
9
|
-
import
|
|
10
|
-
import copy
|
|
7
|
+
import fnmatch
|
|
11
8
|
from typing import Any, Callable, Optional, List
|
|
12
9
|
from functools import wraps
|
|
13
10
|
from opentelemetry import trace
|
|
14
|
-
from opentelemetry.
|
|
15
|
-
|
|
16
|
-
from
|
|
17
|
-
from .
|
|
18
|
-
from .client import get_aiqa_client, get_component_tag, set_component_tag as _set_component_tag, get_aiqa_tracer
|
|
19
|
-
from .constants import AIQA_TRACER_NAME, LOG_TAG
|
|
11
|
+
from opentelemetry.trace import Status, StatusCode
|
|
12
|
+
|
|
13
|
+
from .client import get_aiqa_client, get_component_tag, get_aiqa_tracer
|
|
14
|
+
from .constants import LOG_TAG
|
|
20
15
|
from .object_serialiser import serialize_for_span
|
|
21
|
-
from .http_utils import build_headers, get_server_url, get_api_key
|
|
22
16
|
from .tracing_llm_utils import _extract_and_set_token_usage, _extract_and_set_provider_and_model
|
|
23
17
|
|
|
24
18
|
logger = logging.getLogger(LOG_TAG)
|
|
25
19
|
|
|
26
20
|
|
|
27
|
-
|
|
28
|
-
"""
|
|
29
|
-
Flush all pending spans to the server.
|
|
30
|
-
Flushes also happen automatically every few seconds. So you only need to call this function
|
|
31
|
-
if you want to flush immediately, e.g. before exiting a process.
|
|
32
|
-
A common use is if you are tracing unit tests or experiment runs.
|
|
33
|
-
|
|
34
|
-
This flushes both the BatchSpanProcessor and the exporter buffer.
|
|
35
|
-
"""
|
|
36
|
-
client = get_aiqa_client()
|
|
37
|
-
if client.provider:
|
|
38
|
-
client.provider.force_flush() # Synchronous method
|
|
39
|
-
if client.exporter:
|
|
40
|
-
await client.exporter.flush()
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
# Export provider and exporter accessors for advanced usage
|
|
44
|
-
|
|
45
|
-
__all__ = [
|
|
46
|
-
"flush_tracing", "WithTracing",
|
|
47
|
-
"set_span_attribute", "set_span_name", "get_active_span",
|
|
48
|
-
"get_active_trace_id", "get_span_id", "create_span_from_trace_id", "inject_trace_context", "extract_trace_context",
|
|
49
|
-
"set_conversation_id", "set_component_tag", "set_token_usage", "set_provider_and_model", "get_span", "submit_feedback"
|
|
50
|
-
]
|
|
21
|
+
__all__ = ["WithTracing"]
|
|
51
22
|
|
|
52
23
|
|
|
53
24
|
class TracingOptions:
|
|
@@ -75,20 +46,25 @@ class TracingOptions:
|
|
|
75
46
|
descriptive names.
|
|
76
47
|
|
|
77
48
|
ignore_input: Iterable of keys (e.g., list, set) to exclude from
|
|
78
|
-
input data when recording span attributes.
|
|
79
|
-
|
|
80
|
-
|
|
49
|
+
input data when recording span attributes. Applies after filter_input if both are set.
|
|
50
|
+
Only applies when
|
|
51
|
+
input is a dictionary. Supports simple wildcards (e.g., `"_*"`
|
|
52
|
+
matches `"_apple"`, `"_fruit"`). For example, use `["password", "api_key"]`
|
|
53
|
+
or `["_*", "password"]` to exclude sensitive fields from being traced.
|
|
81
54
|
|
|
82
55
|
ignore_output: Iterable of keys (e.g., list, set) to exclude from
|
|
83
56
|
output data when recording span attributes. Only applies when
|
|
84
|
-
output is a dictionary.
|
|
57
|
+
output is a dictionary. Supports simple wildcards (e.g., `"_*"`
|
|
58
|
+
matches `"_apple"`, `"_fruit"`). Useful for excluding large or sensitive
|
|
85
59
|
fields from traces.
|
|
86
60
|
|
|
87
|
-
filter_input: Callable function that receives the
|
|
88
|
-
and returns a filtered/transformed
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
61
|
+
filter_input: Callable function that receives the same arguments as the
|
|
62
|
+
decorated function (*args, **kwargs) and returns a filtered/transformed
|
|
63
|
+
version to be recorded in the span. This allows you to extract specific
|
|
64
|
+
properties from any kind of object, including `self` for methods.
|
|
65
|
+
The function receives the exact same inputs as the decorated function,
|
|
66
|
+
including `self` for bound methods. Returns a dict or any value that
|
|
67
|
+
will be converted to a dict. Applied before ignore_input filtering.
|
|
92
68
|
|
|
93
69
|
filter_output: Callable function that receives the output data and
|
|
94
70
|
returns a filtered/transformed version to be recorded in the span.
|
|
@@ -109,6 +85,22 @@ class TracingOptions:
|
|
|
109
85
|
)
|
|
110
86
|
def process_data(items):
|
|
111
87
|
return items
|
|
88
|
+
|
|
89
|
+
# Extract properties from self in a method
|
|
90
|
+
class ExperimentRunner:
|
|
91
|
+
def __init__(self, dataset_id, experiment_id):
|
|
92
|
+
self.dataset_id = dataset_id
|
|
93
|
+
self.experiment_id = experiment_id
|
|
94
|
+
|
|
95
|
+
@WithTracing(
|
|
96
|
+
filter_input=lambda self, example: {
|
|
97
|
+
"dataset": self.dataset_id,
|
|
98
|
+
"experiment": self.experiment_id,
|
|
99
|
+
"example_id": example.id if hasattr(example, 'id') else None
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
def run_example(self, example):
|
|
103
|
+
return self.process(example)
|
|
112
104
|
"""
|
|
113
105
|
self.name = name
|
|
114
106
|
self.ignore_input = ignore_input
|
|
@@ -117,12 +109,27 @@ class TracingOptions:
|
|
|
117
109
|
self.filter_output = filter_output
|
|
118
110
|
|
|
119
111
|
|
|
120
|
-
def
|
|
112
|
+
def _matches_ignore_pattern(key: str, ignore_patterns: List[str]) -> bool:
|
|
113
|
+
"""
|
|
114
|
+
Check if a key matches any pattern in the ignore list.
|
|
115
|
+
Supports simple wildcards (e.g., "_*" matches "_apple", "_fruit").
|
|
116
|
+
"""
|
|
117
|
+
for pattern in ignore_patterns:
|
|
118
|
+
if "*" in pattern or "?" in pattern:
|
|
119
|
+
# Use fnmatch for wildcard matching
|
|
120
|
+
if fnmatch.fnmatch(key, pattern):
|
|
121
|
+
return True
|
|
122
|
+
else:
|
|
123
|
+
# Exact match for non-wildcard patterns
|
|
124
|
+
if key == pattern:
|
|
125
|
+
return True
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _prepare_input(args: tuple, kwargs: dict, sig: Optional[inspect.Signature] = None) -> Any:
|
|
121
130
|
"""Prepare input for span attributes.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
For single-arg-dicts or kwargs-only, returns a shallow copy of the input data.
|
|
131
|
+
Converts args and kwargs into a unified dict structure using function signature when available.
|
|
132
|
+
Falls back to legacy behavior for functions without inspectable signatures.
|
|
126
133
|
|
|
127
134
|
Note: This function does NOT serialize values - it just structures the data.
|
|
128
135
|
Serialization happens later via serialize_for_span() to avoid double-encoding
|
|
@@ -130,6 +137,22 @@ def _prepare_input(args: tuple, kwargs: dict) -> Any:
|
|
|
130
137
|
"""
|
|
131
138
|
if not args and not kwargs:
|
|
132
139
|
return None
|
|
140
|
+
|
|
141
|
+
# Try to bind args to parameter names using function signature
|
|
142
|
+
if sig is not None:
|
|
143
|
+
try:
|
|
144
|
+
bound = sig.bind(*args, **kwargs)
|
|
145
|
+
bound.apply_defaults()
|
|
146
|
+
# Return dict of all arguments (positional args are now named)
|
|
147
|
+
result = bound.arguments.copy()
|
|
148
|
+
# Shallow copy to protect against mutating the input
|
|
149
|
+
return result
|
|
150
|
+
except (TypeError, ValueError):
|
|
151
|
+
# Binding failed (e.g., wrong number of args, *args/**kwargs issues)
|
|
152
|
+
# Fall through to legacy behavior
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
# in case binding fails
|
|
133
156
|
if not kwargs:
|
|
134
157
|
if len(args) == 1:
|
|
135
158
|
arg0 = args[0]
|
|
@@ -145,44 +168,99 @@ def _prepare_input(args: tuple, kwargs: dict) -> Any:
|
|
|
145
168
|
return result
|
|
146
169
|
|
|
147
170
|
|
|
171
|
+
def _apply_ignore_patterns(data_dict: dict, ignore_patterns: Optional[List[str]]) -> dict:
|
|
172
|
+
"""
|
|
173
|
+
Apply ignore patterns to a dict.
|
|
174
|
+
Supports string keys, wildcard patterns (*), and list of patterns.
|
|
175
|
+
Used for both ignore_input and ignore_output.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
data_dict: Dictionary to filter
|
|
179
|
+
ignore_patterns: List of patterns to exclude (e.g., ["self", "_*", "password"])
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Filtered dictionary with matching keys removed
|
|
183
|
+
"""
|
|
184
|
+
if not ignore_patterns or not isinstance(data_dict, dict):
|
|
185
|
+
return data_dict
|
|
186
|
+
|
|
187
|
+
result = data_dict.copy()
|
|
188
|
+
keys_to_delete = [
|
|
189
|
+
key for key in result.keys()
|
|
190
|
+
if _matches_ignore_pattern(key, ignore_patterns)
|
|
191
|
+
]
|
|
192
|
+
for key in keys_to_delete:
|
|
193
|
+
del result[key]
|
|
194
|
+
|
|
195
|
+
return result
|
|
196
|
+
|
|
197
|
+
|
|
148
198
|
def _prepare_and_filter_input(
|
|
149
199
|
args: tuple,
|
|
150
200
|
kwargs: dict,
|
|
151
201
|
filter_input: Optional[Callable[[Any], Any]],
|
|
152
202
|
ignore_input: Optional[List[str]],
|
|
203
|
+
sig: Optional[inspect.Signature] = None,
|
|
153
204
|
) -> Any:
|
|
154
205
|
"""
|
|
155
|
-
Prepare and filter input for span attributes
|
|
156
|
-
|
|
206
|
+
Prepare and filter input for span attributes.
|
|
207
|
+
|
|
208
|
+
Process flow:
|
|
209
|
+
1. Apply filter_input to args, kwargs (receives same inputs as decorated function, including self)
|
|
210
|
+
2. Convert into dict ready for span.attributes.input
|
|
211
|
+
3. Apply ignore_input to the dict (supports string, wildcard, and list patterns)
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
args: Positional arguments (including self for bound methods)
|
|
215
|
+
kwargs: Keyword arguments
|
|
216
|
+
filter_input: Optional function to filter/transform args and kwargs before conversion.
|
|
217
|
+
Receives *args, **kwargs with the same signature as the function being decorated,
|
|
218
|
+
including `self` for bound methods. This allows extracting properties from any object.
|
|
219
|
+
ignore_input: Optional list of keys/patterns to exclude from the final dict.
|
|
220
|
+
If "self" is in ignore_input, it will be removed from the final dict but filter_input
|
|
221
|
+
still receives it.
|
|
222
|
+
sig: Optional function signature for proper arg name resolution
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Prepared input data (dict, list, or other) ready for span.attributes.input
|
|
157
226
|
"""
|
|
158
|
-
#
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
input_data = _prepare_input(filtered_args, filtered_kwargs)
|
|
173
|
-
if filter_input and input_data is not None:
|
|
174
|
-
input_data = filter_input(input_data)
|
|
175
|
-
if filtered_ignore_input and len(filtered_ignore_input) > 0:
|
|
176
|
-
if not isinstance(input_data, dict):
|
|
177
|
-
logger.warning(f"_prepare_and_filter_input: skip: ignore_input is set beyond 'self': {filtered_ignore_input} but input_data is not a dict: {type(input_data)}")
|
|
227
|
+
# Step 1: Apply filter_input to args, kwargs (same inputs as decorated function, including self)
|
|
228
|
+
if filter_input:
|
|
229
|
+
# filter_input receives the exact same args/kwargs as the decorated function
|
|
230
|
+
# This allows it to access self and extract properties from any object
|
|
231
|
+
try:
|
|
232
|
+
filtered_result = filter_input(*args, **kwargs)
|
|
233
|
+
except TypeError:
|
|
234
|
+
# Fallback: backward compatibility - convert to dict first
|
|
235
|
+
temp_dict = _prepare_input(args, kwargs, sig)
|
|
236
|
+
filtered_result = filter_input(temp_dict)
|
|
237
|
+
|
|
238
|
+
# Step 2: Convert filter_input result into dict ready for span.attributes.input
|
|
239
|
+
if isinstance(filtered_result, dict):
|
|
240
|
+
input_data = filtered_result
|
|
178
241
|
else:
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
242
|
+
# Convert filter_input result to dict using signature
|
|
243
|
+
# Use original sig (not filtered) since filter_input received all args including self
|
|
244
|
+
input_data = _prepare_input(
|
|
245
|
+
(filtered_result,) if not isinstance(filtered_result, tuple) else filtered_result,
|
|
246
|
+
{},
|
|
247
|
+
sig
|
|
248
|
+
)
|
|
249
|
+
else:
|
|
250
|
+
# Step 2: Convert into dict ready for span.attributes.input
|
|
251
|
+
input_data = _prepare_input(args, kwargs, sig)
|
|
252
|
+
|
|
253
|
+
# Step 3: Apply ignore_input to the dict (removes "self" from final dict if specified)
|
|
254
|
+
should_ignore_self = ignore_input and "self" in ignore_input
|
|
255
|
+
if isinstance(input_data, dict):
|
|
256
|
+
input_data = _apply_ignore_patterns(input_data, ignore_input)
|
|
257
|
+
# Handle case where we removed self and there are no remaining args/kwargs
|
|
258
|
+
if should_ignore_self and not input_data:
|
|
259
|
+
return None
|
|
260
|
+
elif ignore_input:
|
|
261
|
+
# Warn if ignore_input is set but input_data is not a dict
|
|
262
|
+
logger.warning(f"_prepare_and_filter_input: skip: ignore_input is set but input_data is not a dict: {type(input_data)}")
|
|
263
|
+
|
|
186
264
|
return input_data
|
|
187
265
|
|
|
188
266
|
|
|
@@ -197,11 +275,13 @@ def _filter_and_serialize_output(
|
|
|
197
275
|
if isinstance(output_data, dict):
|
|
198
276
|
output_data = output_data.copy() # copy to provide shallow protection against the user accidentally mutating the output with filter_output
|
|
199
277
|
output_data = filter_output(output_data)
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
278
|
+
|
|
279
|
+
# Apply ignore_output patterns (supports key, wildcard, and list patterns)
|
|
280
|
+
if isinstance(output_data, dict):
|
|
281
|
+
output_data = _apply_ignore_patterns(output_data, ignore_output)
|
|
282
|
+
elif ignore_output:
|
|
283
|
+
# Warn if ignore_output is set but output_data is not a dict
|
|
284
|
+
logger.warning(f"_filter_and_serialize_output: skip: ignore_output is set but output_data is not a dict: {type(output_data)}")
|
|
205
285
|
|
|
206
286
|
# Serialize immediately to create immutable result (removes mutable structures)
|
|
207
287
|
return serialize_for_span(output_data)
|
|
@@ -418,13 +498,22 @@ def WithTracing(
|
|
|
418
498
|
func: The function to trace (when used as @WithTracing)
|
|
419
499
|
name: Optional custom name for the span (defaults to function name)
|
|
420
500
|
ignore_input: List of keys to exclude from input data when recording span attributes.
|
|
421
|
-
|
|
422
|
-
|
|
501
|
+
self is handled as "self"
|
|
502
|
+
Supports simple wildcards (e.g., "_*"
|
|
503
|
+
matches "_apple", "_fruit"). For example, use ["password", "api_key"] or
|
|
504
|
+
["_*", "password"] to exclude sensitive fields from being traced.
|
|
423
505
|
ignore_output: List of keys to exclude from output data when recording span attributes.
|
|
424
|
-
Only applies when output is a dictionary.
|
|
506
|
+
Only applies when output is a dictionary. Supports simple wildcards (e.g., "_*"
|
|
507
|
+
matches "_apple", "_fruit"). Useful for excluding large or sensitive
|
|
425
508
|
fields from traces.
|
|
426
|
-
filter_input: Function to filter/transform input before recording
|
|
427
|
-
|
|
509
|
+
filter_input: Function to filter/transform input before recording.
|
|
510
|
+
Receives the same arguments as the decorated function (*args, **kwargs),
|
|
511
|
+
including `self` for bound methods. This allows you to extract specific
|
|
512
|
+
properties from any kind of object. For example, to extract `dataset_id`
|
|
513
|
+
from `self` in a method: `filter_input=lambda self, x: {"dataset": self.dataset_id, "x": x}`.
|
|
514
|
+
Returns a dict or any value (will be converted to dict). Applied before ignore_input.
|
|
515
|
+
filter_output: Function to filter/transform output before recording.
|
|
516
|
+
Receives the output value and returns a filtered/transformed version.
|
|
428
517
|
|
|
429
518
|
Example:
|
|
430
519
|
@WithTracing
|
|
@@ -434,6 +523,17 @@ def WithTracing(
|
|
|
434
523
|
@WithTracing(name="custom_name")
|
|
435
524
|
def another_function():
|
|
436
525
|
pass
|
|
526
|
+
|
|
527
|
+
# Extract properties from self in a method
|
|
528
|
+
class MyClass:
|
|
529
|
+
def __init__(self, dataset_id):
|
|
530
|
+
self.dataset_id = dataset_id
|
|
531
|
+
|
|
532
|
+
@WithTracing(
|
|
533
|
+
filter_input=lambda self, x: {"dataset": self.dataset_id, "x": x}
|
|
534
|
+
)
|
|
535
|
+
def process(self, x):
|
|
536
|
+
return x * 2
|
|
437
537
|
"""
|
|
438
538
|
def decorator(fn: Callable) -> Callable:
|
|
439
539
|
fn_name = name or fn.__name__ or "_"
|
|
@@ -447,6 +547,15 @@ def WithTracing(
|
|
|
447
547
|
is_generator = inspect.isgeneratorfunction(fn)
|
|
448
548
|
is_async_generator = inspect.isasyncgenfunction(fn) if hasattr(inspect, 'isasyncgenfunction') else False
|
|
449
549
|
|
|
550
|
+
# Get function signature once at decoration time for efficient arg name resolution
|
|
551
|
+
fn_sig: Optional[inspect.Signature] = None
|
|
552
|
+
try:
|
|
553
|
+
fn_sig = inspect.signature(fn)
|
|
554
|
+
except (ValueError, TypeError):
|
|
555
|
+
# Some callables (e.g., builtins, C extensions) don't have inspectable signatures
|
|
556
|
+
# Will fall back to legacy behavior
|
|
557
|
+
pass
|
|
558
|
+
|
|
450
559
|
# Don't get tracer here - get it lazily when function is called
|
|
451
560
|
# This ensures initialization only happens when tracing is actually used
|
|
452
561
|
|
|
@@ -595,7 +704,7 @@ def WithTracing(
|
|
|
595
704
|
if is_async_generator:
|
|
596
705
|
@wraps(fn)
|
|
597
706
|
async def async_gen_traced_fn(*args, **kwargs):
|
|
598
|
-
input_data = _prepare_and_filter_input(args, kwargs, filter_input, ignore_input)
|
|
707
|
+
input_data = _prepare_and_filter_input(args, kwargs, filter_input, ignore_input, fn_sig)
|
|
599
708
|
return await _execute_generator_async(
|
|
600
709
|
lambda: fn(*args, **kwargs),
|
|
601
710
|
input_data
|
|
@@ -607,7 +716,7 @@ def WithTracing(
|
|
|
607
716
|
elif is_generator:
|
|
608
717
|
@wraps(fn)
|
|
609
718
|
def gen_traced_fn(*args, **kwargs):
|
|
610
|
-
input_data = _prepare_and_filter_input(args, kwargs, filter_input, ignore_input)
|
|
719
|
+
input_data = _prepare_and_filter_input(args, kwargs, filter_input, ignore_input, fn_sig)
|
|
611
720
|
return _execute_generator_sync(
|
|
612
721
|
lambda: fn(*args, **kwargs),
|
|
613
722
|
input_data
|
|
@@ -619,7 +728,7 @@ def WithTracing(
|
|
|
619
728
|
elif is_async:
|
|
620
729
|
@wraps(fn)
|
|
621
730
|
async def async_traced_fn(*args, **kwargs):
|
|
622
|
-
input_data = _prepare_and_filter_input(args, kwargs, filter_input, ignore_input)
|
|
731
|
+
input_data = _prepare_and_filter_input(args, kwargs, filter_input, ignore_input, fn_sig)
|
|
623
732
|
return await _execute_with_span_async(
|
|
624
733
|
lambda: fn(*args, **kwargs),
|
|
625
734
|
input_data
|
|
@@ -631,7 +740,7 @@ def WithTracing(
|
|
|
631
740
|
else:
|
|
632
741
|
@wraps(fn)
|
|
633
742
|
def sync_traced_fn(*args, **kwargs):
|
|
634
|
-
input_data = _prepare_and_filter_input(args, kwargs, filter_input, ignore_input)
|
|
743
|
+
input_data = _prepare_and_filter_input(args, kwargs, filter_input, ignore_input, fn_sig)
|
|
635
744
|
return _execute_with_span_sync(
|
|
636
745
|
lambda: fn(*args, **kwargs),
|
|
637
746
|
input_data
|
|
@@ -648,477 +757,4 @@ def WithTracing(
|
|
|
648
757
|
return decorator(func)
|
|
649
758
|
|
|
650
759
|
|
|
651
|
-
def set_span_attribute(attribute_name: str, attribute_value: Any) -> bool:
|
|
652
|
-
"""
|
|
653
|
-
Set an attribute on the active span.
|
|
654
|
-
|
|
655
|
-
Returns:
|
|
656
|
-
True if attribute was set, False if no active span found
|
|
657
|
-
"""
|
|
658
|
-
span = trace.get_current_span()
|
|
659
|
-
if span and span.is_recording():
|
|
660
|
-
span.set_attribute(attribute_name, serialize_for_span(attribute_value))
|
|
661
|
-
return True
|
|
662
|
-
return False
|
|
663
|
-
|
|
664
|
-
def set_span_name(span_name: str) -> bool:
|
|
665
|
-
"""
|
|
666
|
-
Set the name of the active span.
|
|
667
|
-
"""
|
|
668
|
-
span = trace.get_current_span()
|
|
669
|
-
if span and span.is_recording():
|
|
670
|
-
span.update_name(span_name)
|
|
671
|
-
return True
|
|
672
|
-
return False
|
|
673
|
-
|
|
674
|
-
def get_active_span() -> Optional[trace.Span]:
|
|
675
|
-
"""Get the currently active span."""
|
|
676
|
-
return trace.get_current_span()
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
def set_conversation_id(conversation_id: str) -> bool:
|
|
680
|
-
"""
|
|
681
|
-
Set the gen_ai.conversation.id attribute on the active span.
|
|
682
|
-
This allows you to group multiple traces together that are part of the same conversation.
|
|
683
|
-
See https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-events/ for more details.
|
|
684
|
-
|
|
685
|
-
Args:
|
|
686
|
-
conversation_id: A unique identifier for the conversation (e.g., user session ID, chat ID, etc.)
|
|
687
|
-
|
|
688
|
-
Returns:
|
|
689
|
-
True if gen_ai.conversation.id was set, False if no active span found
|
|
690
|
-
|
|
691
|
-
Example:
|
|
692
|
-
from aiqa import WithTracing, set_conversation_id
|
|
693
|
-
|
|
694
|
-
@WithTracing
|
|
695
|
-
def handle_user_request(user_id: str, request: dict):
|
|
696
|
-
# Set conversation ID to group all traces for this user session
|
|
697
|
-
set_conversation_id(f"user_{user_id}_session_{request.get('session_id')}")
|
|
698
|
-
# ... rest of function
|
|
699
|
-
"""
|
|
700
|
-
return set_span_attribute("gen_ai.conversation.id", conversation_id)
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
def set_token_usage(
|
|
704
|
-
input_tokens: Optional[int] = None,
|
|
705
|
-
output_tokens: Optional[int] = None,
|
|
706
|
-
total_tokens: Optional[int] = None,
|
|
707
|
-
) -> bool:
|
|
708
|
-
"""
|
|
709
|
-
Set token usage attributes on the active span using OpenTelemetry semantic conventions for gen_ai.
|
|
710
|
-
This allows you to explicitly record token usage information.
|
|
711
|
-
AIQA tracing will automatically detect and set token usage from standard OpenAI-like API responses.
|
|
712
|
-
See https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/ for more details.
|
|
713
|
-
|
|
714
|
-
Args:
|
|
715
|
-
input_tokens: Number of input tokens used (maps to gen_ai.usage.input_tokens)
|
|
716
|
-
output_tokens: Number of output tokens generated (maps to gen_ai.usage.output_tokens)
|
|
717
|
-
total_tokens: Total number of tokens used (maps to gen_ai.usage.total_tokens)
|
|
718
|
-
|
|
719
|
-
Returns:
|
|
720
|
-
True if at least one token usage attribute was set, False if no active span found
|
|
721
|
-
|
|
722
|
-
Example:
|
|
723
|
-
from aiqa import WithTracing, set_token_usage
|
|
724
|
-
|
|
725
|
-
@WithTracing
|
|
726
|
-
def call_llm(prompt: str):
|
|
727
|
-
response = openai_client.chat.completions.create(...)
|
|
728
|
-
# Explicitly set token usage
|
|
729
|
-
set_token_usage(
|
|
730
|
-
input_tokens=response.usage.prompt_tokens,
|
|
731
|
-
output_tokens=response.usage.completion_tokens,
|
|
732
|
-
total_tokens=response.usage.total_tokens
|
|
733
|
-
)
|
|
734
|
-
return response
|
|
735
|
-
"""
|
|
736
|
-
span = trace.get_current_span()
|
|
737
|
-
if not span or not span.is_recording():
|
|
738
|
-
return False
|
|
739
|
-
|
|
740
|
-
set_count = 0
|
|
741
|
-
try:
|
|
742
|
-
if input_tokens is not None:
|
|
743
|
-
span.set_attribute("gen_ai.usage.input_tokens", input_tokens)
|
|
744
|
-
set_count += 1
|
|
745
|
-
if output_tokens is not None:
|
|
746
|
-
span.set_attribute("gen_ai.usage.output_tokens", output_tokens)
|
|
747
|
-
set_count += 1
|
|
748
|
-
if total_tokens is not None:
|
|
749
|
-
span.set_attribute("gen_ai.usage.total_tokens", total_tokens)
|
|
750
|
-
set_count += 1
|
|
751
|
-
except Exception as e:
|
|
752
|
-
logger.warning(f"Failed to set token usage attributes: {e}")
|
|
753
|
-
return False
|
|
754
|
-
|
|
755
|
-
return set_count > 0
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
def set_provider_and_model(
|
|
759
|
-
provider: Optional[str] = None,
|
|
760
|
-
model: Optional[str] = None,
|
|
761
|
-
) -> bool:
|
|
762
|
-
"""
|
|
763
|
-
Set provider and model attributes on the active span using OpenTelemetry semantic conventions for gen_ai.
|
|
764
|
-
This allows you to explicitly record provider and model information.
|
|
765
|
-
AIQA tracing will automatically detect and set provider/model from standard API responses.
|
|
766
|
-
See https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/ for more details.
|
|
767
|
-
|
|
768
|
-
Args:
|
|
769
|
-
provider: Name of the AI provider (e.g., "openai", "anthropic", "google") (maps to gen_ai.provider.name)
|
|
770
|
-
model: Name of the model used (e.g., "gpt-4", "claude-3-5-sonnet") (maps to gen_ai.request.model)
|
|
771
|
-
|
|
772
|
-
Returns:
|
|
773
|
-
True if at least one attribute was set, False if no active span found
|
|
774
|
-
|
|
775
|
-
Example:
|
|
776
|
-
from aiqa import WithTracing, set_provider_and_model
|
|
777
|
-
|
|
778
|
-
@WithTracing
|
|
779
|
-
def call_llm(prompt: str):
|
|
780
|
-
response = openai_client.chat.completions.create(...)
|
|
781
|
-
# Explicitly set provider and model
|
|
782
|
-
set_provider_and_model(
|
|
783
|
-
provider="openai",
|
|
784
|
-
model=response.model
|
|
785
|
-
)
|
|
786
|
-
return response
|
|
787
|
-
"""
|
|
788
|
-
span = trace.get_current_span()
|
|
789
|
-
if not span or not span.is_recording():
|
|
790
|
-
return False
|
|
791
|
-
|
|
792
|
-
set_count = 0
|
|
793
|
-
try:
|
|
794
|
-
if provider is not None:
|
|
795
|
-
span.set_attribute("gen_ai.provider.name", str(provider))
|
|
796
|
-
set_count += 1
|
|
797
|
-
if model is not None:
|
|
798
|
-
span.set_attribute("gen_ai.request.model", str(model))
|
|
799
|
-
set_count += 1
|
|
800
|
-
except Exception as e:
|
|
801
|
-
logger.warning(f"Failed to set provider/model attributes: {e}")
|
|
802
|
-
return False
|
|
803
|
-
|
|
804
|
-
return set_count > 0
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
def set_component_tag(tag: str) -> None:
|
|
808
|
-
"""
|
|
809
|
-
Set the component tag that will be added to all spans created by AIQA.
|
|
810
|
-
This can also be set via the AIQA_COMPONENT_TAG environment variable.
|
|
811
|
-
The component tag allows you to identify which component/system generated the spans.
|
|
812
|
-
|
|
813
|
-
Note: Initialization is automatic when WithTracing is first used. You can also call
|
|
814
|
-
get_aiqa_client() explicitly if needed.
|
|
815
|
-
the client and load environment variables.
|
|
816
|
-
|
|
817
|
-
Args:
|
|
818
|
-
tag: A component identifier (e.g., "mynamespace.mysystem", "backend.api", etc.)
|
|
819
|
-
|
|
820
|
-
Example:
|
|
821
|
-
from aiqa import get_aiqa_client, set_component_tag, WithTracing
|
|
822
|
-
|
|
823
|
-
# Initialize client (loads env vars including AIQA_COMPONENT_TAG)
|
|
824
|
-
get_aiqa_client()
|
|
825
|
-
|
|
826
|
-
# Or set component tag programmatically (overrides env var)
|
|
827
|
-
set_component_tag("mynamespace.mysystem")
|
|
828
|
-
|
|
829
|
-
@WithTracing
|
|
830
|
-
def my_function():
|
|
831
|
-
pass
|
|
832
|
-
"""
|
|
833
|
-
_set_component_tag(tag)
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
def get_active_trace_id() -> Optional[str]:
|
|
837
|
-
"""
|
|
838
|
-
Get the current trace ID as a hexadecimal string (32 characters).
|
|
839
|
-
|
|
840
|
-
Returns:
|
|
841
|
-
The trace ID as a hex string, or None if no active span exists.
|
|
842
|
-
|
|
843
|
-
Example:
|
|
844
|
-
trace_id = get_active_trace_id()
|
|
845
|
-
# Pass trace_id to another service/agent
|
|
846
|
-
# e.g., include in HTTP headers, message queue metadata, etc.
|
|
847
|
-
# Within a single thread, OpenTelemetry normally does this for you.
|
|
848
|
-
"""
|
|
849
|
-
span = trace.get_current_span()
|
|
850
|
-
if span and span.get_span_context().is_valid:
|
|
851
|
-
return format(span.get_span_context().trace_id, "032x")
|
|
852
|
-
return None
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
def get_span_id() -> Optional[str]:
|
|
856
|
-
"""
|
|
857
|
-
Get the current span ID as a hexadecimal string (16 characters).
|
|
858
|
-
|
|
859
|
-
Returns:
|
|
860
|
-
The span ID as a hex string, or None if no active span exists.
|
|
861
|
-
|
|
862
|
-
Example:
|
|
863
|
-
span_id = get_span_id()
|
|
864
|
-
# Can be used to create child spans in other services
|
|
865
|
-
"""
|
|
866
|
-
span = trace.get_current_span()
|
|
867
|
-
if span and span.get_span_context().is_valid:
|
|
868
|
-
return format(span.get_span_context().span_id, "016x")
|
|
869
|
-
return None
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
def create_span_from_trace_id(
|
|
873
|
-
trace_id: str,
|
|
874
|
-
parent_span_id: Optional[str] = None,
|
|
875
|
-
span_name: str = "continued_span",
|
|
876
|
-
) -> trace.Span:
|
|
877
|
-
"""
|
|
878
|
-
Create a new span that continues from an existing trace ID.
|
|
879
|
-
This is useful for linking traces across different services or agents.
|
|
880
|
-
|
|
881
|
-
Args:
|
|
882
|
-
trace_id: The trace ID as a hexadecimal string (32 characters)
|
|
883
|
-
parent_span_id: Optional parent span ID as a hexadecimal string (16 characters).
|
|
884
|
-
If provided, the new span will be a child of this span.
|
|
885
|
-
span_name: Name for the new span (default: "continued_span")
|
|
886
|
-
|
|
887
|
-
Returns:
|
|
888
|
-
A new span that continues the trace. Use it in a context manager or call end() manually.
|
|
889
|
-
|
|
890
|
-
Example:
|
|
891
|
-
# In service A: get trace ID
|
|
892
|
-
trace_id = get_active_trace_id()
|
|
893
|
-
span_id = get_span_id()
|
|
894
|
-
|
|
895
|
-
# Send to service B (e.g., via HTTP, message queue, etc.)
|
|
896
|
-
# ...
|
|
897
|
-
|
|
898
|
-
# In service B: continue the trace
|
|
899
|
-
with create_span_from_trace_id(trace_id, parent_span_id=span_id, span_name="service_b_operation"):
|
|
900
|
-
# Your code here
|
|
901
|
-
pass
|
|
902
|
-
"""
|
|
903
|
-
try:
|
|
904
|
-
# Parse trace ID from hex string
|
|
905
|
-
trace_id_int = int(trace_id, 16)
|
|
906
|
-
|
|
907
|
-
# Parse parent span ID if provided
|
|
908
|
-
parent_span_id_int = None
|
|
909
|
-
if parent_span_id:
|
|
910
|
-
parent_span_id_int = int(parent_span_id, 16)
|
|
911
|
-
|
|
912
|
-
# Create a parent span context
|
|
913
|
-
parent_span_context = SpanContext(
|
|
914
|
-
trace_id=trace_id_int,
|
|
915
|
-
span_id=parent_span_id_int if parent_span_id_int else 0,
|
|
916
|
-
is_remote=True,
|
|
917
|
-
trace_flags=TraceFlags(0x01), # SAMPLED flag
|
|
918
|
-
)
|
|
919
|
-
|
|
920
|
-
# Create a context with this span context as the parent
|
|
921
|
-
from opentelemetry.trace import set_span_in_context
|
|
922
|
-
parent_context = set_span_in_context(trace.NonRecordingSpan(parent_span_context))
|
|
923
|
-
|
|
924
|
-
# Ensure initialization before creating span
|
|
925
|
-
get_aiqa_client()
|
|
926
|
-
# Start a new span in this context (it will be a child of the parent span)
|
|
927
|
-
tracer = get_aiqa_tracer()
|
|
928
|
-
span = tracer.start_span(span_name, context=parent_context)
|
|
929
|
-
|
|
930
|
-
# Set component tag if configured
|
|
931
|
-
component_tag = get_component_tag()
|
|
932
|
-
if component_tag:
|
|
933
|
-
span.set_attribute("gen_ai.component.id", component_tag)
|
|
934
|
-
|
|
935
|
-
return span
|
|
936
|
-
except (ValueError, AttributeError) as e:
|
|
937
|
-
logger.error(f"Error creating span from trace_id: {e}")
|
|
938
|
-
# Ensure initialization before creating span
|
|
939
|
-
get_aiqa_client()
|
|
940
|
-
# Fallback: create a new span
|
|
941
|
-
tracer = get_aiqa_tracer()
|
|
942
|
-
span = tracer.start_span(span_name)
|
|
943
|
-
component_tag = get_component_tag()
|
|
944
|
-
if component_tag:
|
|
945
|
-
span.set_attribute("gen_ai.component.id", component_tag)
|
|
946
|
-
return span
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
def inject_trace_context(carrier: dict) -> None:
|
|
950
|
-
"""
|
|
951
|
-
Inject the current trace context into a carrier (e.g., HTTP headers).
|
|
952
|
-
This allows you to pass trace context to another service.
|
|
953
|
-
|
|
954
|
-
Args:
|
|
955
|
-
carrier: Dictionary to inject trace context into (e.g., HTTP headers dict)
|
|
956
|
-
|
|
957
|
-
Example:
|
|
958
|
-
import requests
|
|
959
|
-
|
|
960
|
-
headers = {}
|
|
961
|
-
inject_trace_context(headers)
|
|
962
|
-
response = requests.get("http://other-service/api", headers=headers)
|
|
963
|
-
"""
|
|
964
|
-
try:
|
|
965
|
-
inject(carrier)
|
|
966
|
-
except Exception as e:
|
|
967
|
-
logger.warning(f"Error injecting trace context: {e}")
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
def extract_trace_context(carrier: dict) -> Any:
|
|
971
|
-
"""
|
|
972
|
-
Extract trace context from a carrier (e.g., HTTP headers).
|
|
973
|
-
Use this to continue a trace that was started in another service.
|
|
974
|
-
|
|
975
|
-
Args:
|
|
976
|
-
carrier: Dictionary containing trace context (e.g., HTTP headers dict)
|
|
977
|
-
|
|
978
|
-
Returns:
|
|
979
|
-
A context object that can be used with trace.use_span() or tracer.start_span()
|
|
980
|
-
|
|
981
|
-
Example:
|
|
982
|
-
from opentelemetry.trace import use_span
|
|
983
|
-
|
|
984
|
-
# Extract context from incoming request headers
|
|
985
|
-
ctx = extract_trace_context(request.headers)
|
|
986
|
-
|
|
987
|
-
# Use the context to create a span
|
|
988
|
-
with use_span(ctx):
|
|
989
|
-
# Your code here
|
|
990
|
-
pass
|
|
991
|
-
|
|
992
|
-
# Or create a span with the context
|
|
993
|
-
tracer = get_aiqa_tracer()
|
|
994
|
-
with tracer.start_as_current_span("operation", context=ctx):
|
|
995
|
-
# Your code here
|
|
996
|
-
pass
|
|
997
|
-
"""
|
|
998
|
-
try:
|
|
999
|
-
return extract(carrier)
|
|
1000
|
-
except Exception as e:
|
|
1001
|
-
logger.warning(f"Error extracting trace context: {e}")
|
|
1002
|
-
return None
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
def get_span(span_id: str, organisation_id: Optional[str] = None, exclude: Optional[List[str]] = None) -> Optional[dict]:
|
|
1006
|
-
"""
|
|
1007
|
-
Get a span by its ID from the AIQA server.
|
|
1008
|
-
|
|
1009
|
-
Expected usage is: re-playing a specific function call in a unit test (either a developer debugging an issue, or as part of a test suite).
|
|
1010
|
-
|
|
1011
|
-
Args:
|
|
1012
|
-
span_id: The span ID as a hexadecimal string (16 characters) or client span ID
|
|
1013
|
-
organisation_id: Optional organisation ID. If not provided, will try to get from
|
|
1014
|
-
AIQA_ORGANISATION_ID environment variable. The organisation is typically
|
|
1015
|
-
extracted from the API key during authentication, but the API requires it
|
|
1016
|
-
as a query parameter.
|
|
1017
|
-
exclude: Optional list of fields to exclude from the span data. By default this function WILL return 'attributes' (often large).
|
|
1018
|
-
|
|
1019
|
-
Returns:
|
|
1020
|
-
The span data as a dictionary, or None if not found
|
|
1021
|
-
|
|
1022
|
-
Example:
|
|
1023
|
-
from aiqa import get_span
|
|
1024
|
-
|
|
1025
|
-
span = get_span('abc123...')
|
|
1026
|
-
if span:
|
|
1027
|
-
print(f"Found span: {span['name']}")
|
|
1028
|
-
my_function(**span['input'])
|
|
1029
|
-
"""
|
|
1030
|
-
import os
|
|
1031
|
-
import requests
|
|
1032
|
-
|
|
1033
|
-
server_url = get_server_url()
|
|
1034
|
-
api_key = get_api_key()
|
|
1035
|
-
org_id = organisation_id or os.getenv("AIQA_ORGANISATION_ID", "")
|
|
1036
|
-
|
|
1037
|
-
if not server_url:
|
|
1038
|
-
raise ValueError("AIQA_SERVER_URL is not set. Cannot retrieve span.")
|
|
1039
|
-
if not org_id:
|
|
1040
|
-
raise ValueError("Organisation ID is required. Provide it as parameter or set AIQA_ORGANISATION_ID environment variable.")
|
|
1041
|
-
if not api_key:
|
|
1042
|
-
raise ValueError("API key is required. Set AIQA_API_KEY environment variable.")
|
|
1043
|
-
|
|
1044
|
-
# Try both spanId and clientSpanId queries
|
|
1045
|
-
for query_field in ["spanId", "clientSpanId"]:
|
|
1046
|
-
url = f"{server_url}/span"
|
|
1047
|
-
params = {
|
|
1048
|
-
"q": f"{query_field}:{span_id}",
|
|
1049
|
-
"organisation": org_id,
|
|
1050
|
-
"limit": "1",
|
|
1051
|
-
"exclude": ",".join(exclude) if exclude else None,
|
|
1052
|
-
"fields": "*" if not exclude else None,
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
headers = build_headers(api_key)
|
|
1056
|
-
|
|
1057
|
-
response = requests.get(url, params=params, headers=headers)
|
|
1058
|
-
if response.status_code == 200:
|
|
1059
|
-
result = response.json()
|
|
1060
|
-
hits = result.get("hits", [])
|
|
1061
|
-
if hits and len(hits) > 0:
|
|
1062
|
-
return hits[0]
|
|
1063
|
-
elif response.status_code == 404:
|
|
1064
|
-
# Try next query field
|
|
1065
|
-
continue
|
|
1066
|
-
else:
|
|
1067
|
-
error_text = response.text
|
|
1068
|
-
raise ValueError(f"Failed to get span: {response.status_code} - {error_text[:500]}")
|
|
1069
|
-
# not found
|
|
1070
|
-
return None
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
async def submit_feedback(
|
|
1074
|
-
trace_id: str,
|
|
1075
|
-
thumbs_up: Optional[bool] = None,
|
|
1076
|
-
comment: Optional[str] = None,
|
|
1077
|
-
) -> None:
|
|
1078
|
-
"""
|
|
1079
|
-
Submit feedback for a trace by creating a new span with the same trace ID.
|
|
1080
|
-
This allows you to add feedback (thumbs-up, thumbs-down, comment) to a trace after it has completed.
|
|
1081
|
-
|
|
1082
|
-
Args:
|
|
1083
|
-
trace_id: The trace ID as a hexadecimal string (32 characters)
|
|
1084
|
-
thumbs_up: True for positive feedback, False for negative feedback, None for neutral
|
|
1085
|
-
comment: Optional text comment
|
|
1086
|
-
|
|
1087
|
-
Example:
|
|
1088
|
-
from aiqa import submit_feedback
|
|
1089
|
-
|
|
1090
|
-
# Submit positive feedback
|
|
1091
|
-
await submit_feedback('abc123...', thumbs_up=True, comment='Great response!')
|
|
1092
|
-
|
|
1093
|
-
# Submit negative feedback
|
|
1094
|
-
await submit_feedback('abc123...', thumbs_up=False, comment='Incorrect answer')
|
|
1095
|
-
"""
|
|
1096
|
-
if not trace_id or len(trace_id) != 32:
|
|
1097
|
-
raise ValueError('Invalid trace ID: must be 32 hexadecimal characters')
|
|
1098
|
-
|
|
1099
|
-
# Create a span for feedback with the same trace ID
|
|
1100
|
-
span = create_span_from_trace_id(trace_id, span_name='feedback')
|
|
1101
|
-
|
|
1102
|
-
try:
|
|
1103
|
-
# Set feedback attributes
|
|
1104
|
-
if thumbs_up is not None:
|
|
1105
|
-
span.set_attribute('feedback.thumbs_up', thumbs_up)
|
|
1106
|
-
span.set_attribute('feedback.type', 'positive' if thumbs_up else 'negative')
|
|
1107
|
-
else:
|
|
1108
|
-
span.set_attribute('feedback.type', 'neutral')
|
|
1109
|
-
|
|
1110
|
-
if comment:
|
|
1111
|
-
span.set_attribute('feedback.comment', comment)
|
|
1112
|
-
|
|
1113
|
-
# Mark as feedback span
|
|
1114
|
-
span.set_attribute('aiqa.span_type', 'feedback')
|
|
1115
|
-
|
|
1116
|
-
# End the span
|
|
1117
|
-
span.end()
|
|
1118
|
-
|
|
1119
|
-
# Flush to ensure it's sent immediately
|
|
1120
|
-
await flush_tracing()
|
|
1121
|
-
except Exception as e:
|
|
1122
|
-
span.end()
|
|
1123
|
-
raise e
|
|
1124
760
|
|