Skip to content

Commit bb807f1

Browse files
CopilotsergioescalerajuliomenendeznikhilNava
authored
Add suppress_invoke_agent_input option to core observability SDK (#111)
* Initial plan * Add suppress_invoke_agent_input option to OpenAI trace processor Co-authored-by: sergioescalera <8428450+sergioescalera@users.noreply.github.com> * Refactor: Address code review feedback - reduce duplication and improve efficiency Co-authored-by: sergioescalera <8428450+sergioescalera@users.noreply.github.com> * Refactor: Use parent span operation name check instead of tracking dictionary Co-authored-by: sergioescalera <8428450+sergioescalera@users.noreply.github.com> * Fix: Pass both span and otel_span to _should_suppress_input as suggested in review Co-authored-by: juliomenendez <9697+juliomenendez@users.noreply.github.com> * Move suppress_invoke_agent_input to core SDK (exporter and span processor) Co-authored-by: juliomenendez <9697+juliomenendez@users.noreply.github.com> * Fix suppression logic: check current span instead of parent, remove unused span_processor param Co-authored-by: nikhilNava <211831449+nikhilNava@users.noreply.github.com> * Fix formatting: multi-line imports and remove trailing whitespace Co-authored-by: nikhilNava <211831449+nikhilNava@users.noreply.github.com> * Fix test: add suppress_invoke_agent_input parameter to exporter assertion Co-authored-by: nikhilNava <211831449+nikhilNava@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sergioescalera <8428450+sergioescalera@users.noreply.github.com> Co-authored-by: juliomenendez <9697+juliomenendez@users.noreply.github.com> Co-authored-by: nikhilNava <211831449+nikhilNava@users.noreply.github.com>
1 parent 1cda09d commit bb807f1

File tree

4 files changed

+86
-0
lines changed

4 files changed

+86
-0
lines changed

libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ def configure(
5353
token_resolver: Callable[[str, str], str | None] | None = None,
5454
cluster_category: str = "prod",
5555
exporter_options: Optional[Agent365ExporterOptions] = None,
56+
suppress_invoke_agent_input: bool = False,
5657
**kwargs: Any,
5758
) -> bool:
5859
"""
@@ -67,6 +68,7 @@ def configure(
6768
Use exporter_options instead.
6869
:param exporter_options: Agent365ExporterOptions instance for configuring the exporter.
6970
If provided, exporter_options takes precedence. If exporter_options is None, the token_resolver and cluster_category parameters are used as fallback/legacy support to construct a default Agent365ExporterOptions instance.
71+
:param suppress_invoke_agent_input: If True, suppress input messages for spans that are children of InvokeAgent spans.
7072
:return: True if configuration succeeded, False otherwise.
7173
"""
7274
try:
@@ -78,6 +80,7 @@ def configure(
7880
token_resolver,
7981
cluster_category,
8082
exporter_options,
83+
suppress_invoke_agent_input,
8184
**kwargs,
8285
)
8386
except Exception as e:
@@ -92,6 +95,7 @@ def _configure_internal(
9295
token_resolver: Callable[[str, str], str | None] | None = None,
9396
cluster_category: str = "prod",
9497
exporter_options: Optional[Agent365ExporterOptions] = None,
98+
suppress_invoke_agent_input: bool = False,
9599
**kwargs: Any,
96100
) -> bool:
97101
"""Internal configuration method - not thread-safe, must be called with lock."""
@@ -151,6 +155,7 @@ def _configure_internal(
151155
token_resolver=exporter_options.token_resolver,
152156
cluster_category=exporter_options.cluster_category,
153157
use_s2s_endpoint=exporter_options.use_s2s_endpoint,
158+
suppress_invoke_agent_input=suppress_invoke_agent_input,
154159
)
155160
else:
156161
exporter = ConsoleSpanExporter()

libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/agent365_exporter.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@
1818
from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
1919
from opentelemetry.trace import StatusCode
2020

21+
from ..constants import (
22+
GEN_AI_INPUT_MESSAGES_KEY,
23+
GEN_AI_OPERATION_NAME_KEY,
24+
INVOKE_AGENT_OPERATION_NAME,
25+
)
2126
from .utils import (
2227
get_validated_domain_override,
2328
hex_span_id,
@@ -53,6 +58,7 @@ def __init__(
5358
token_resolver: Callable[[str, str], str | None],
5459
cluster_category: str = "prod",
5560
use_s2s_endpoint: bool = False,
61+
suppress_invoke_agent_input: bool = False,
5662
):
5763
if token_resolver is None:
5864
raise ValueError("token_resolver must be provided.")
@@ -62,6 +68,7 @@ def __init__(
6268
self._token_resolver = token_resolver
6369
self._cluster_category = cluster_category
6470
self._use_s2s_endpoint = use_s2s_endpoint
71+
self._suppress_invoke_agent_input = suppress_invoke_agent_input
6572
# Read domain override once at initialization
6673
self._domain_override = get_validated_domain_override()
6774

@@ -279,6 +286,20 @@ def _map_span(self, sp: ReadableSpan) -> dict[str, Any]:
279286

280287
# attributes
281288
attrs = dict(sp.attributes or {})
289+
290+
# Suppress input messages if configured and current span is an InvokeAgent span
291+
if self._suppress_invoke_agent_input:
292+
# Check if current span is an InvokeAgent span by:
293+
# 1. Span name starts with "invoke_agent"
294+
# 2. Has attribute gen_ai.operation.name set to INVOKE_AGENT_OPERATION_NAME
295+
operation_name = attrs.get(GEN_AI_OPERATION_NAME_KEY)
296+
if (
297+
sp.name.startswith(INVOKE_AGENT_OPERATION_NAME)
298+
and operation_name == INVOKE_AGENT_OPERATION_NAME
299+
):
300+
# Remove input messages attribute
301+
attrs.pop(GEN_AI_INPUT_MESSAGES_KEY, None)
302+
282303
# events
283304
events = []
284305
for ev in sp.events:

tests/observability/core/test_agent365.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ def test_batch_span_processor_and_exporter_called_with_correct_values(
119119
token_resolver=self.mock_token_resolver,
120120
cluster_category="staging",
121121
use_s2s_endpoint=True,
122+
suppress_invoke_agent_input=False,
122123
)
123124

124125
# Verify BatchSpanProcessor was called with correct parameters from exporter_options
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Copyright (c) Microsoft. All rights reserved.
2+
3+
import unittest
4+
5+
from microsoft_agents_a365.observability.core.exporters.agent365_exporter import _Agent365Exporter
6+
7+
8+
class TestPromptSuppressionConfiguration(unittest.TestCase):
9+
"""Unit tests for prompt suppression configuration in the core SDK."""
10+
11+
def test_exporter_default_suppression_is_false(self):
12+
"""Test that the default value for suppress_invoke_agent_input is False in exporter."""
13+
exporter = _Agent365Exporter(token_resolver=lambda x, y: "test")
14+
15+
self.assertFalse(
16+
exporter._suppress_invoke_agent_input,
17+
"Default value for suppress_invoke_agent_input should be False",
18+
)
19+
20+
def test_exporter_can_enable_suppression(self):
21+
"""Test that suppression can be enabled via exporter constructor."""
22+
exporter = _Agent365Exporter(
23+
token_resolver=lambda x, y: "test", suppress_invoke_agent_input=True
24+
)
25+
26+
self.assertTrue(
27+
exporter._suppress_invoke_agent_input,
28+
"suppress_invoke_agent_input should be True when explicitly set",
29+
)
30+
31+
32+
def run_tests():
33+
"""Run all prompt suppression configuration tests."""
34+
print("🧪 Running prompt suppression configuration tests...")
35+
print("=" * 80)
36+
37+
loader = unittest.TestLoader()
38+
suite = loader.loadTestsFromTestCase(TestPromptSuppressionConfiguration)
39+
40+
runner = unittest.TextTestRunner(verbosity=2)
41+
result = runner.run(suite)
42+
43+
print("\n" + "=" * 80)
44+
print("🏁 Test Summary:")
45+
print(f"Tests run: {result.testsRun}")
46+
print(f"Failures: {len(result.failures)}")
47+
print(f"Errors: {len(result.errors)}")
48+
49+
if result.wasSuccessful():
50+
print("🎉 All tests passed!")
51+
return True
52+
else:
53+
print("🔧 Some tests failed. Check output above.")
54+
return False
55+
56+
57+
if __name__ == "__main__":
58+
success = run_tests()
59+
exit(0 if success else 1)

0 commit comments

Comments
 (0)