Skip to content

Commit 0df2ad5

Browse files
agent framework extension update for input and output attributes
1 parent 9a5dbdf commit 0df2ad5

File tree

5 files changed

+136
-11
lines changed

5 files changed

+136
-11
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,10 @@
11
# Copyright (c) Microsoft Corporation.
22
# Licensed under the MIT License.
3+
4+
"""Agent Framework observability extensions for Agent365."""
5+
6+
from .trace_instrumentor import AgentFrameworkInstrumentor
7+
8+
__all__ = [
9+
"AgentFrameworkInstrumentor",
10+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
from microsoft_agents_a365.observability.core.constants import (
5+
EXECUTE_TOOL_OPERATION_NAME,
6+
GEN_AI_INPUT_MESSAGES_KEY,
7+
GEN_AI_OUTPUT_MESSAGES_KEY,
8+
GEN_AI_TOOL_ARGS_KEY,
9+
GEN_AI_TOOL_CALL_RESULT_KEY,
10+
INVOKE_AGENT_OPERATION_NAME,
11+
)
12+
from microsoft_agents_a365.observability.core.exporters.enriched_span import EnrichedReadableSpan
13+
from opentelemetry.sdk.trace import ReadableSpan
14+
15+
from .utils import extract_input_content, extract_output_content
16+
17+
# Agent Framework specific attribute keys
18+
AF_TOOL_CALL_ARGUMENTS_KEY = "gen_ai.tool.call.arguments"
19+
AF_TOOL_CALL_RESULT_KEY = "gen_ai.tool.call.result"
20+
21+
22+
def enrich_agent_framework_span(span: ReadableSpan) -> ReadableSpan:
23+
"""
24+
Enricher function for Agent Framework spans.
25+
"""
26+
extra_attributes = {}
27+
attributes = span.attributes or {}
28+
29+
# Only extract content for invoke_agent spans
30+
if span.name.startswith(INVOKE_AGENT_OPERATION_NAME):
31+
# Extract all text content from input messages
32+
input_messages = attributes.get(GEN_AI_INPUT_MESSAGES_KEY)
33+
if input_messages:
34+
extra_attributes[GEN_AI_INPUT_MESSAGES_KEY] = extract_input_content(input_messages)
35+
36+
output_messages = attributes.get(GEN_AI_OUTPUT_MESSAGES_KEY)
37+
if output_messages:
38+
extra_attributes[GEN_AI_OUTPUT_MESSAGES_KEY] = extract_output_content(output_messages)
39+
40+
# Map tool attributes for execute_tool spans
41+
elif span.name.startswith(EXECUTE_TOOL_OPERATION_NAME):
42+
if AF_TOOL_CALL_ARGUMENTS_KEY in attributes:
43+
extra_attributes[GEN_AI_TOOL_ARGS_KEY] = attributes[AF_TOOL_CALL_ARGUMENTS_KEY]
44+
45+
if AF_TOOL_CALL_RESULT_KEY in attributes:
46+
extra_attributes[GEN_AI_TOOL_CALL_RESULT_KEY] = attributes[AF_TOOL_CALL_RESULT_KEY]
47+
48+
if extra_attributes:
49+
return EnrichedReadableSpan(span, extra_attributes)
50+
51+
return span

libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/span_processor.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
# Copyright (c) Microsoft Corporation.
22
# Licensed under the MIT License.
33

4-
# Custom Span Processor
5-
6-
from opentelemetry.sdk.trace.export import SpanProcessor
7-
84
from microsoft_agents_a365.observability.core.constants import (
9-
GEN_AI_OPERATION_NAME_KEY,
105
EXECUTE_TOOL_OPERATION_NAME,
116
GEN_AI_EVENT_CONTENT,
7+
GEN_AI_OPERATION_NAME_KEY,
128
)
9+
from opentelemetry.sdk.trace.export import SpanProcessor
1310

1411

1512
class AgentFrameworkSpanProcessor(SpanProcessor):

libraries/microsoft-agents-a365-observability-extensions-agentframework/microsoft_agents_a365/observability/extensions/agentframework/trace_instrumentor.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,28 @@
77
from typing import Any
88

99
from microsoft_agents_a365.observability.core.config import get_tracer_provider, is_configured
10+
from microsoft_agents_a365.observability.core.exporters.enriching_span_processor import (
11+
register_span_enricher,
12+
unregister_span_enricher,
13+
)
1014
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
1115

16+
from microsoft_agents_a365.observability.extensions.agentframework.span_enricher import (
17+
enrich_agent_framework_span,
18+
)
1219
from microsoft_agents_a365.observability.extensions.agentframework.span_processor import (
1320
AgentFrameworkSpanProcessor,
1421
)
1522

1623
# -----------------------------
1724
# 3) The Instrumentor class
1825
# -----------------------------
19-
_instruments = ("agent-framework-azure-ai >= 1.0.0b251114",)
26+
_instruments = ("agent-framework-azure-ai >= 1.0.0",)
2027

2128

2229
class AgentFrameworkInstrumentor(BaseInstrumentor):
2330
"""
24-
Instruments Agent Framework:
25-
• Installs your custom OTel SpanProcessor
31+
Instruments Agent Framework with Agent365 observability.
2632
"""
2733

2834
def __init__(self):
@@ -37,13 +43,28 @@ def instrumentation_dependencies(self) -> Collection[str]:
3743

3844
def _instrument(self, **kwargs: Any) -> None:
3945
"""
40-
kwargs (all optional):
41-
"""
46+
Instrument Agent Framework.
4247
48+
Args:
49+
**kwargs: Optional configuration parameters.
50+
"""
4351
# Ensure we have an SDK TracerProvider
4452
provider = get_tracer_provider()
53+
54+
# Add processor for on_start modifications (rename spans, add attributes)
4555
self._processor = AgentFrameworkSpanProcessor()
4656
provider.add_span_processor(self._processor)
4757

58+
# Register enricher for on_end modifications
59+
register_span_enricher(enrich_agent_framework_span)
60+
4861
def _uninstrument(self, **kwargs: Any) -> None:
49-
pass
62+
"""
63+
Remove Agent Framework instrumentation.
64+
"""
65+
# Unregister the enricher
66+
unregister_span_enricher()
67+
68+
# Shutdown the processor
69+
if hasattr(self, "_processor"):
70+
self._processor.shutdown()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
"""Utility functions for Agent Framework observability extensions."""
5+
6+
from __future__ import annotations
7+
8+
import json
9+
10+
11+
def extract_content_as_string_list(messages_json: str, role_filter: str | None = None) -> str:
12+
"""Extract content values from messages JSON and return as JSON string list."""
13+
try:
14+
messages = json.loads(messages_json)
15+
if isinstance(messages, list):
16+
contents = []
17+
for msg in messages:
18+
if isinstance(msg, dict):
19+
role = msg.get("role", "")
20+
21+
# Filter by role if specified
22+
if role_filter and role != role_filter:
23+
continue
24+
25+
# Handle Agent Framework format with "parts"
26+
parts = msg.get("parts")
27+
if parts and isinstance(parts, list):
28+
for part in parts:
29+
if isinstance(part, dict):
30+
part_type = part.get("type", "")
31+
# Only extract text content, not tool_call or tool_call_response
32+
if part_type == "text" and "content" in part:
33+
contents.append(part["content"])
34+
return json.dumps(contents)
35+
return messages_json
36+
except (json.JSONDecodeError, TypeError):
37+
# If parsing fails, return as-is
38+
return messages_json
39+
40+
41+
def extract_input_content(messages_json: str) -> str:
42+
"""Extract text content from user messages only."""
43+
return extract_content_as_string_list(messages_json, role_filter="user")
44+
45+
46+
def extract_output_content(messages_json: str) -> str:
47+
"""Extract only assistant text content from output messages."""
48+
return extract_content_as_string_list(messages_json, role_filter="assistant")

0 commit comments

Comments
 (0)