diff --git a/CHANGELOG.md b/CHANGELOG.md index 43dafd04..32ca2729 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ dict/string payloads. Execute-tool payload serialization is non-throwing: unserializable payloads record `{"serialization_error": "Failed to serialize execute tool payload."}` instead of failing the span. - Add explicit custom baggage APIs and propagate opted-in attributes to supported GenAI spans, including unmodeled operations. ([#264](https://github.com/microsoft/opentelemetry-distro-python/pull/264)) +- Enable Agent Framework GenAI message events by default when supported, with + an `instrumentation_options["agent_framework"]["enable_message_events"]` + override. + ([#270](https://github.com/microsoft/opentelemetry-distro-python/pull/270)) - Add Python-native `InvokeAgentScope` request and response parameter models that emit OpenTelemetry GenAI semantic attributes, including structured system instructions and cache read/write token counts, introduced by .NET diff --git a/README.md b/README.md index b0601355..5475d7d6 100644 --- a/README.md +++ b/README.md @@ -291,13 +291,26 @@ Toggle individual instrumentations: ```python use_microsoft_opentelemetry( + enable_sensitive_data=True, instrumentation_options={ "flask": {"enabled": False}, "openai": {"enabled": True}, + "agent_framework": { + "enabled": True, + "enable_message_events": True, + "force": True, + }, }, ) ``` +For Agent Framework, `enable_sensitive_data` controls prompt, completion, +tool argument, and tool result capture. Message events are enabled by default +when the installed Agent Framework version supports them and can be disabled +with `instrumentation_options["agent_framework"]["enable_message_events"]`. +Set `instrumentation_options["agent_framework"]["force"]` to `True` to +re-enable Agent Framework instrumentation after it was previously disabled. + ### Default Instrumentations When `enable_a365=True` Microsoft OpenTelemetry **automatically disables the diff --git a/pyproject.toml b/pyproject.toml index dc9e850e..c0d11f10 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ dependencies = [ [project.optional-dependencies] agent-framework = [ - "agent-framework>=1.4.0", + "agent-framework>=1.19.0", ] openai-agents = [ "openai-agents>=0.0.7", diff --git a/samples/microsoft_agent_framework/README.md b/samples/microsoft_agent_framework/README.md index 7e32c40e..c37e357a 100644 --- a/samples/microsoft_agent_framework/README.md +++ b/samples/microsoft_agent_framework/README.md @@ -61,3 +61,8 @@ Microsoft Agent Framework is natively instrumented with OpenTelemetry, so you ca ```bash python sample_maf_agent.py ``` + +The sample enables sensitive-data capture and Agent Framework GenAI message +events. Sensitive data can include prompts, completions, tool arguments, and +tool results. Review your data-handling requirements before enabling it in +production. diff --git a/samples/microsoft_agent_framework/requirements.txt b/samples/microsoft_agent_framework/requirements.txt index 8f1d3aef..c494c8b4 100644 --- a/samples/microsoft_agent_framework/requirements.txt +++ b/samples/microsoft_agent_framework/requirements.txt @@ -1,3 +1,3 @@ -agent-framework-core +agent-framework-core>=1.19.0 agent-framework-foundry microsoft-opentelemetry \ No newline at end of file diff --git a/samples/microsoft_agent_framework/sample_maf_agent.py b/samples/microsoft_agent_framework/sample_maf_agent.py index 171a88d5..6f566a35 100644 --- a/samples/microsoft_agent_framework/sample_maf_agent.py +++ b/samples/microsoft_agent_framework/sample_maf_agent.py @@ -31,7 +31,15 @@ async def get_weather( async def main(): # Set up Azure monitor exporters for telemetry # This will automatically enable instrumentation for Agent Framework - use_microsoft_opentelemetry(enable_azure_monitor=True) + use_microsoft_opentelemetry( + enable_azure_monitor=True, + enable_sensitive_data=True, + instrumentation_options={ + "agent_framework": { + "enable_message_events": True, + }, + }, + ) questions = [ "What's the weather in Amsterdam?", diff --git a/src/microsoft/opentelemetry/_agent_framework/_trace_instrumentor.py b/src/microsoft/opentelemetry/_agent_framework/_trace_instrumentor.py index 9e87068d..018d2075 100644 --- a/src/microsoft/opentelemetry/_agent_framework/_trace_instrumentor.py +++ b/src/microsoft/opentelemetry/_agent_framework/_trace_instrumentor.py @@ -5,6 +5,7 @@ import logging from collections.abc import Collection +from inspect import Parameter, signature from typing import Any from opentelemetry.instrumentation.instrumentor import BaseInstrumentor # type: ignore[attr-defined] @@ -13,7 +14,7 @@ from microsoft.opentelemetry._agent_framework._span_processor import AgentFrameworkSpanProcessor _logger = logging.getLogger(__name__) -_instruments = ("agent-framework >= 1.0.0",) +_instruments = ("agent-framework >= 1.19.0",) class AgentFrameworkInstrumentor(BaseInstrumentor): @@ -36,10 +37,24 @@ def _instrument(self, **kwargs: Any) -> None: # Enable the Agent Framework SDK's built-in span generation so users # don't need to call enable_instrumentation() manually. enable_sensitive_data = kwargs.get("enable_sensitive_data", False) + enable_message_events = kwargs.get("enable_message_events", True) + force = kwargs.get("force", False) try: from agent_framework.observability import enable_instrumentation - enable_instrumentation(enable_sensitive_data=enable_sensitive_data) + enable_kwargs = {"enable_sensitive_data": enable_sensitive_data} + parameters = signature(enable_instrumentation).parameters + accepts_kwargs = any(parameter.kind == Parameter.VAR_KEYWORD for parameter in parameters.values()) + optional_kwargs = { + "enable_message_events": enable_message_events, + "force": force, + } + for name, value in optional_kwargs.items(): + if name in parameters or accepts_kwargs: + enable_kwargs[name] = value + else: + _logger.debug("Agent Framework SDK does not support configuring %s.", name) + enable_instrumentation(**enable_kwargs) self._af_instrumentation_enabled = True except ImportError as exc: _logger.debug( diff --git a/tests/agent_framework/test_trace_instrumentor.py b/tests/agent_framework/test_trace_instrumentor.py index a4af9567..d56bdf35 100644 --- a/tests/agent_framework/test_trace_instrumentor.py +++ b/tests/agent_framework/test_trace_instrumentor.py @@ -94,7 +94,7 @@ def test_instrument_calls_enable_instrumentation_when_available(self, mock_get_p instrumentor = AgentFrameworkInstrumentor() instrumentor._instrument() - mock_enable.assert_called_once_with(enable_sensitive_data=False) + mock_enable.assert_called_once_with(enable_sensitive_data=False, enable_message_events=True, force=False) self.assertTrue(instrumentor._af_instrumentation_enabled) @patch("microsoft.opentelemetry._agent_framework._trace_instrumentor.get_tracer_provider") @@ -114,7 +114,64 @@ def test_instrument_enables_sensitive_data_when_kwarg_set(self, mock_get_provide instrumentor = AgentFrameworkInstrumentor() instrumentor._instrument(enable_sensitive_data=True) - mock_enable.assert_called_once_with(enable_sensitive_data=True) + mock_enable.assert_called_once_with(enable_sensitive_data=True, enable_message_events=True, force=False) + self.assertTrue(instrumentor._af_instrumentation_enabled) + + @patch("microsoft.opentelemetry._agent_framework._trace_instrumentor.get_tracer_provider") + def test_instrument_can_disable_message_events(self, mock_get_provider): + mock_get_provider.return_value = MagicMock() + mock_enable = MagicMock() + + with patch.dict( + "sys.modules", + { + "agent_framework": MagicMock(), + "agent_framework.observability": MagicMock(enable_instrumentation=mock_enable), + }, + ): + instrumentor = AgentFrameworkInstrumentor() + instrumentor._instrument(enable_message_events=False) + + mock_enable.assert_called_once_with(enable_sensitive_data=False, enable_message_events=False, force=False) + self.assertTrue(instrumentor._af_instrumentation_enabled) + + @patch("microsoft.opentelemetry._agent_framework._trace_instrumentor.get_tracer_provider") + def test_instrument_can_force_reenable_instrumentation(self, mock_get_provider): + mock_get_provider.return_value = MagicMock() + mock_enable = MagicMock() + + with patch.dict( + "sys.modules", + { + "agent_framework": MagicMock(), + "agent_framework.observability": MagicMock(enable_instrumentation=mock_enable), + }, + ): + instrumentor = AgentFrameworkInstrumentor() + instrumentor._instrument(force=True) + + mock_enable.assert_called_once_with(enable_sensitive_data=False, enable_message_events=True, force=True) + self.assertTrue(instrumentor._af_instrumentation_enabled) + + @patch("microsoft.opentelemetry._agent_framework._trace_instrumentor.get_tracer_provider") + def test_instrument_supports_legacy_enable_instrumentation_signature(self, mock_get_provider): + mock_get_provider.return_value = MagicMock() + mock_enable = MagicMock() + + def legacy_enable_instrumentation(*, enable_sensitive_data=None, force=False): + mock_enable(enable_sensitive_data=enable_sensitive_data, force=force) + + with patch.dict( + "sys.modules", + { + "agent_framework": MagicMock(), + "agent_framework.observability": MagicMock(enable_instrumentation=legacy_enable_instrumentation), + }, + ): + instrumentor = AgentFrameworkInstrumentor() + instrumentor._instrument(enable_sensitive_data=True) + + mock_enable.assert_called_once_with(enable_sensitive_data=True, force=False) self.assertTrue(instrumentor._af_instrumentation_enabled) @patch("microsoft.opentelemetry._agent_framework._trace_instrumentor.get_tracer_provider") @@ -214,7 +271,7 @@ def test_enable_instrumentation_called_in_azure_monitor_only_scenario(self, mock instrumentor._instrument() # AF SDK enabled, span processor added, enricher NOT registered. - mock_enable.assert_called_once_with(enable_sensitive_data=False) + mock_enable.assert_called_once_with(enable_sensitive_data=False, enable_message_events=True, force=False) self.assertTrue(instrumentor._af_instrumentation_enabled) mock_provider.add_span_processor.assert_called_once() self.assertFalse(instrumentor._owns_enricher) diff --git a/tests/test_agent_framework_integration.py b/tests/test_agent_framework_integration.py index 6286d633..dbec30a0 100644 --- a/tests/test_agent_framework_integration.py +++ b/tests/test_agent_framework_integration.py @@ -97,7 +97,11 @@ def test_enable_sensitive_data_kwarg(self, mock_get_provider): with patch.dict("sys.modules", {"agent_framework.observability": mock_af_obs}): inst = AgentFrameworkInstrumentor() inst._instrument(enable_sensitive_data=True) - mock_af_obs.enable_instrumentation.assert_called_once_with(enable_sensitive_data=True) + mock_af_obs.enable_instrumentation.assert_called_once_with( + enable_sensitive_data=True, + enable_message_events=True, + force=False, + ) class TestAgentFrameworkSpanProcessor(unittest.TestCase):