|
1 | 1 | # Copyright (c) Microsoft. All rights reserved. |
| 2 | +import logging |
2 | 3 | from collections.abc import AsyncGenerator, Iterable |
| 4 | +from contextlib import nullcontext |
| 5 | +from importlib import import_module |
3 | 6 | from typing import Annotated, Any |
4 | | -from unittest.mock import Mock |
| 7 | +from unittest.mock import Mock, patch |
5 | 8 |
|
6 | 9 | import pytest |
| 10 | +from opentelemetry.trace import StatusCode |
7 | 11 |
|
8 | 12 | from semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion import OpenAIChatCompletion |
9 | 13 | from semantic_kernel.exceptions import FunctionExecutionException, FunctionInitializationError |
|
18 | 22 | from semantic_kernel.kernel import Kernel |
19 | 23 | from semantic_kernel.kernel_pydantic import KernelBaseModel |
20 | 24 |
|
| 25 | +kernel_function_module = import_module("semantic_kernel.functions.kernel_function") |
| 26 | + |
21 | 27 |
|
22 | 28 | class CustomType(KernelBaseModel): |
23 | 29 | id: str |
@@ -177,6 +183,115 @@ def non_async_function() -> str: |
177 | 183 | pass |
178 | 184 |
|
179 | 185 |
|
| 186 | +async def test_invoke_logs_argument_count_without_values(kernel: Kernel, caplog): |
| 187 | + secret = "SYNTHETIC_SECRET_VALUE" |
| 188 | + |
| 189 | + @kernel_function |
| 190 | + def echo(value: str) -> str: |
| 191 | + return value |
| 192 | + |
| 193 | + native_function = KernelFunction.from_method(method=echo, plugin_name="MockPlugin") |
| 194 | + |
| 195 | + with caplog.at_level(logging.DEBUG, logger="semantic_kernel.functions.kernel_function"): |
| 196 | + result = await native_function.invoke(kernel=kernel, arguments=KernelArguments(value=secret)) |
| 197 | + |
| 198 | + messages = [ |
| 199 | + record.getMessage() for record in caplog.records if record.name == "semantic_kernel.functions.kernel_function" |
| 200 | + ] |
| 201 | + assert result.value == secret |
| 202 | + assert secret not in "\n".join(messages) |
| 203 | + assert "Function arguments provided: 1." in messages |
| 204 | + assert "Function result available." in messages |
| 205 | + |
| 206 | + |
| 207 | +@pytest.mark.parametrize("streaming", [False, True]) |
| 208 | +async def test_invoke_failure_omits_exception_value_without_sensitive_diagnostics( |
| 209 | + kernel: Kernel, caplog, streaming: bool |
| 210 | +): |
| 211 | + secret = "SYNTHETIC_SECRET_VALUE" |
| 212 | + |
| 213 | + if streaming: |
| 214 | + |
| 215 | + @kernel_function |
| 216 | + def fail(value: str) -> Iterable[str]: |
| 217 | + if value: |
| 218 | + raise ValueError(value) |
| 219 | + yield "" |
| 220 | + |
| 221 | + else: |
| 222 | + |
| 223 | + @kernel_function |
| 224 | + def fail(value: str) -> str: |
| 225 | + raise ValueError(value) |
| 226 | + |
| 227 | + native_function = KernelFunction.from_method(method=fail, plugin_name="MockPlugin") |
| 228 | + span = Mock() |
| 229 | + |
| 230 | + with ( |
| 231 | + patch.object( |
| 232 | + kernel_function_module.tracer, |
| 233 | + "start_as_current_span", |
| 234 | + return_value=nullcontext(span), |
| 235 | + ) as start_span, |
| 236 | + patch.object( |
| 237 | + kernel_function_module.function_tracer, |
| 238 | + "are_sensitive_events_enabled", |
| 239 | + return_value=False, |
| 240 | + ), |
| 241 | + caplog.at_level(logging.ERROR, logger="semantic_kernel.functions.kernel_function"), |
| 242 | + pytest.raises(ValueError, match=secret), |
| 243 | + ): |
| 244 | + if streaming: |
| 245 | + async for _ in native_function.invoke_stream(kernel=kernel, arguments=KernelArguments(value=secret)): |
| 246 | + pass |
| 247 | + else: |
| 248 | + await native_function.invoke(kernel=kernel, arguments=KernelArguments(value=secret)) |
| 249 | + |
| 250 | + messages = [ |
| 251 | + record.getMessage() for record in caplog.records if record.name == "semantic_kernel.functions.kernel_function" |
| 252 | + ] |
| 253 | + assert secret not in "\n".join(messages) |
| 254 | + assert "Function failed. Error type: ValueError." in messages |
| 255 | + assert start_span.call_args.kwargs["record_exception"] is False |
| 256 | + assert start_span.call_args.kwargs["set_status_on_exception"] is False |
| 257 | + span.record_exception.assert_not_called() |
| 258 | + span.set_status.assert_called_once_with(StatusCode.ERROR) |
| 259 | + |
| 260 | + |
| 261 | +async def test_invoke_failure_records_exception_with_sensitive_diagnostics(kernel: Kernel, caplog): |
| 262 | + secret = "SYNTHETIC_SECRET_VALUE" |
| 263 | + |
| 264 | + @kernel_function |
| 265 | + def fail(value: str) -> str: |
| 266 | + raise ValueError(value) |
| 267 | + |
| 268 | + native_function = KernelFunction.from_method(method=fail, plugin_name="MockPlugin") |
| 269 | + span = Mock() |
| 270 | + |
| 271 | + with ( |
| 272 | + patch.object( |
| 273 | + kernel_function_module.tracer, |
| 274 | + "start_as_current_span", |
| 275 | + return_value=nullcontext(span), |
| 276 | + ), |
| 277 | + patch.object( |
| 278 | + kernel_function_module.function_tracer, |
| 279 | + "are_sensitive_events_enabled", |
| 280 | + return_value=True, |
| 281 | + ), |
| 282 | + caplog.at_level(logging.ERROR, logger="semantic_kernel.functions.kernel_function"), |
| 283 | + pytest.raises(ValueError, match=secret) as exc_info, |
| 284 | + ): |
| 285 | + await native_function.invoke(kernel=kernel, arguments=KernelArguments(value=secret)) |
| 286 | + |
| 287 | + messages = [ |
| 288 | + record.getMessage() for record in caplog.records if record.name == "semantic_kernel.functions.kernel_function" |
| 289 | + ] |
| 290 | + assert secret not in "\n".join(messages) |
| 291 | + span.record_exception.assert_called_once_with(exc_info.value) |
| 292 | + span.set_status.assert_called_once_with(StatusCode.ERROR, description=secret) |
| 293 | + |
| 294 | + |
180 | 295 | async def test_invoke_async(kernel: Kernel): |
181 | 296 | @kernel_function() |
182 | 297 | async def async_function() -> str: |
|
0 commit comments