Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions python/packages/core/agent_framework/_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -1230,7 +1230,9 @@ async def invoke(
logger.debug("Function result: None")
return parsed

attributes = get_function_span_attributes(self, tool_call_id=tool_call_id)
span_attributes = get_function_span_attributes(self, tool_call_id=tool_call_id)
metric_attributes = dict(span_attributes)
metric_attributes[OtelAttr.MEASUREMENT_FUNCTION_TAG_NAME] = self.name
# Filter out framework kwargs that are not JSON serializable.
serializable_kwargs = {
k: v
Expand All @@ -1250,13 +1252,12 @@ async def invoke(
# as span attributes when that semconv version is active.
emit_tool_call_attrs = OBSERVABILITY_SETTINGS.emit_tool_call_attributes
if emit_tool_call_attrs:
attributes.update({
span_attributes.update({
OtelAttr.TOOL_ARGUMENTS: (
json.dumps(serializable_kwargs, default=str, ensure_ascii=False) if serializable_kwargs else "None"
)
})
with get_function_span(attributes=attributes) as span:
attributes[OtelAttr.MEASUREMENT_FUNCTION_TAG_NAME] = self.name
with get_function_span(attributes=span_attributes) as span:
logger.info(f"Function name: {self.name}")
if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED:
logger.debug(f"Function arguments: {serializable_kwargs}")
Expand All @@ -1267,7 +1268,7 @@ async def invoke(
end_time_stamp = perf_counter()
except Exception as exception:
end_time_stamp = perf_counter()
attributes[OtelAttr.ERROR_TYPE] = type(exception).__name__
metric_attributes[OtelAttr.ERROR_TYPE] = type(exception).__name__
capture_exception(span=span, exception=exception, timestamp=time_ns())
logger.error(f"Function failed. Error: {exception}")
raise
Expand All @@ -1294,7 +1295,7 @@ async def invoke(
parsed = configured_parser(result)
except Exception as exception:
self.invocation_exception_count += 1
attributes[OtelAttr.ERROR_TYPE] = type(exception).__name__
metric_attributes[OtelAttr.ERROR_TYPE] = type(exception).__name__
if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED:
capture_exception(span=span, exception=exception, timestamp=time_ns())
logger.error(f"Function {self.name}: result parser failed. Error: {exception}")
Expand All @@ -1321,7 +1322,7 @@ async def invoke(
finally:
duration = (end_time_stamp or perf_counter()) - start_time_stamp
span.set_attribute(OtelAttr.MEASUREMENT_FUNCTION_INVOCATION_DURATION, duration)
self._invocation_duration_histogram.record(duration, attributes=attributes)
self._invocation_duration_histogram.record(duration, attributes=metric_attributes)
logger.info("Function duration: %fs", duration)

@property
Expand Down
20 changes: 13 additions & 7 deletions python/packages/core/tests/core/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -1003,20 +1003,21 @@ async def test_tool_invoke_telemetry_enabled(span_exporter: InMemorySpanExporter
name="telemetry_test_tool",
description="A test tool for telemetry",
)
def telemetry_test_tool(x: int, y: int) -> int:
"""A function that adds two numbers for telemetry testing."""
return x + y
def telemetry_test_tool(expression: str) -> str:
"""A function that echoes an expression for telemetry testing."""
return expression

# Mock the histogram
mock_histogram = Mock()
telemetry_test_tool._invocation_duration_histogram = mock_histogram
span_exporter.clear()
expression = "(12345 + 67890) / 3"
# Call invoke
result = await telemetry_test_tool.invoke(x=1, y=2, tool_call_id="test_call_id")
result = await telemetry_test_tool.invoke(expression=expression, tool_call_id="test_call_id")

# Verify result
assert isinstance(result, list)
assert result[0].text == "3"
assert result[0].text == expression

# Verify telemetry calls
spans = span_exporter.get_finished_spans()
Expand All @@ -1028,8 +1029,8 @@ def telemetry_test_tool(x: int, y: int) -> int:
assert span.attributes[OtelAttr.TOOL_CALL_ID] == "test_call_id" # type: ignore[index] # pyrefly: ignore[unsupported-operation] # ty: ignore[not-subscriptable]
assert span.attributes[OtelAttr.TOOL_TYPE] == "function" # type: ignore[index] # pyrefly: ignore[unsupported-operation] # ty: ignore[not-subscriptable]
assert span.attributes[OtelAttr.TOOL_DESCRIPTION] == "A test tool for telemetry" # type: ignore[index] # pyrefly: ignore[unsupported-operation] # ty: ignore[not-subscriptable]
assert span.attributes[OtelAttr.TOOL_ARGUMENTS] == '{"x": 1, "y": 2}' # type: ignore[index] # pyrefly: ignore[unsupported-operation] # ty: ignore[not-subscriptable]
assert span.attributes[OtelAttr.TOOL_RESULT] == "3" # type: ignore[index] # pyrefly: ignore[unsupported-operation] # ty: ignore[not-subscriptable]
assert span.attributes[OtelAttr.TOOL_ARGUMENTS] == '{"expression": "(12345 + 67890) / 3"}' # type: ignore[index] # pyrefly: ignore[unsupported-operation] # ty: ignore[not-subscriptable]
assert span.attributes[OtelAttr.TOOL_RESULT] == expression # type: ignore[index] # pyrefly: ignore[unsupported-operation] # ty: ignore[not-subscriptable]

# Verify histogram was called with correct attributes
mock_histogram.record.assert_called_once()
Expand All @@ -1038,6 +1039,9 @@ def telemetry_test_tool(x: int, y: int) -> int:
attributes = call_args[1]["attributes"]
assert attributes[OtelAttr.MEASUREMENT_FUNCTION_TAG_NAME] == "telemetry_test_tool"
assert attributes[OtelAttr.TOOL_CALL_ID] == "test_call_id"
assert OtelAttr.TOOL_ARGUMENTS not in attributes
assert OtelAttr.TOOL_RESULT not in attributes
assert expression not in attributes.values()


@pytest.mark.parametrize("enable_sensitive_data", [False], indirect=True)
Expand Down Expand Up @@ -1206,6 +1210,8 @@ def exception_test_tool(x: int, y: int) -> int:
call_args = mock_histogram.record.call_args
attributes = call_args[1]["attributes"]
assert attributes[OtelAttr.ERROR_TYPE] == ValueError.__name__
assert OtelAttr.TOOL_ARGUMENTS not in attributes
assert OtelAttr.TOOL_RESULT not in attributes


async def test_tool_invoke_telemetry_async_function(span_exporter: InMemorySpanExporter):
Expand Down
Loading