microsoft-agents-a365-observability-extensions-openai 0.2.0.dev5__tar.gz → 0.2.1.dev0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (15) hide show
  1. {microsoft_agents_a365_observability_extensions_openai-0.2.0.dev5 → microsoft_agents_a365_observability_extensions_openai-0.2.1.dev0}/PKG-INFO +3 -1
  2. {microsoft_agents_a365_observability_extensions_openai-0.2.0.dev5 → microsoft_agents_a365_observability_extensions_openai-0.2.1.dev0}/microsoft_agents_a365/observability/extensions/openai/__init__.py +2 -1
  3. {microsoft_agents_a365_observability_extensions_openai-0.2.0.dev5 → microsoft_agents_a365_observability_extensions_openai-0.2.1.dev0}/microsoft_agents_a365/observability/extensions/openai/constants.py +2 -1
  4. {microsoft_agents_a365_observability_extensions_openai-0.2.0.dev5 → microsoft_agents_a365_observability_extensions_openai-0.2.1.dev0}/microsoft_agents_a365/observability/extensions/openai/trace_instrumentor.py +2 -1
  5. {microsoft_agents_a365_observability_extensions_openai-0.2.0.dev5 → microsoft_agents_a365_observability_extensions_openai-0.2.1.dev0}/microsoft_agents_a365/observability/extensions/openai/trace_processor.py +58 -4
  6. {microsoft_agents_a365_observability_extensions_openai-0.2.0.dev5 → microsoft_agents_a365_observability_extensions_openai-0.2.1.dev0}/microsoft_agents_a365/observability/extensions/openai/utils.py +102 -3
  7. {microsoft_agents_a365_observability_extensions_openai-0.2.0.dev5 → microsoft_agents_a365_observability_extensions_openai-0.2.1.dev0}/microsoft_agents_a365_observability_extensions_openai.egg-info/PKG-INFO +3 -1
  8. {microsoft_agents_a365_observability_extensions_openai-0.2.0.dev5 → microsoft_agents_a365_observability_extensions_openai-0.2.1.dev0}/microsoft_agents_a365_observability_extensions_openai.egg-info/SOURCES.txt +3 -0
  9. {microsoft_agents_a365_observability_extensions_openai-0.2.0.dev5 → microsoft_agents_a365_observability_extensions_openai-0.2.1.dev0}/microsoft_agents_a365_observability_extensions_openai.egg-info/top_level.txt +1 -0
  10. {microsoft_agents_a365_observability_extensions_openai-0.2.0.dev5 → microsoft_agents_a365_observability_extensions_openai-0.2.1.dev0}/pyproject.toml +4 -0
  11. {microsoft_agents_a365_observability_extensions_openai-0.2.0.dev5 → microsoft_agents_a365_observability_extensions_openai-0.2.1.dev0}/setup.py +1 -1
  12. {microsoft_agents_a365_observability_extensions_openai-0.2.0.dev5 → microsoft_agents_a365_observability_extensions_openai-0.2.1.dev0}/README.md +0 -0
  13. {microsoft_agents_a365_observability_extensions_openai-0.2.0.dev5 → microsoft_agents_a365_observability_extensions_openai-0.2.1.dev0}/microsoft_agents_a365_observability_extensions_openai.egg-info/dependency_links.txt +0 -0
  14. {microsoft_agents_a365_observability_extensions_openai-0.2.0.dev5 → microsoft_agents_a365_observability_extensions_openai-0.2.1.dev0}/microsoft_agents_a365_observability_extensions_openai.egg-info/requires.txt +0 -0
  15. {microsoft_agents_a365_observability_extensions_openai-0.2.0.dev5 → microsoft_agents_a365_observability_extensions_openai-0.2.1.dev0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microsoft-agents-a365-observability-extensions-openai
3
- Version: 0.2.0.dev5
3
+ Version: 0.2.1.dev0
4
4
  Summary: OpenAI Agents SDK observability and tracing extensions for Microsoft Agent 365
5
5
  Author-email: Microsoft <support@microsoft.com>
6
6
  License: MIT
@@ -20,6 +20,7 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
20
  Classifier: Topic :: System :: Monitoring
21
21
  Requires-Python: >=3.11
22
22
  Description-Content-Type: text/markdown
23
+ License-File: LICENSE
23
24
  Requires-Dist: microsoft-agents-a365-observability-core>=0.0.0
24
25
  Requires-Dist: openai-agents>=0.2.6
25
26
  Requires-Dist: opentelemetry-api>=1.36.0
@@ -34,6 +35,7 @@ Requires-Dist: mypy>=1.0.0; extra == "dev"
34
35
  Provides-Extra: test
35
36
  Requires-Dist: pytest>=7.0.0; extra == "test"
36
37
  Requires-Dist: pytest-asyncio>=0.21.0; extra == "test"
38
+ Dynamic: license-file
37
39
 
38
40
  # microsoft-agents-a365-observability-extensions-openai
39
41
 
@@ -1,4 +1,5 @@
1
- # Copyright (c) Microsoft. All rights reserved.
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
2
3
 
3
4
  """
4
5
  Wraps the OpenAI Agents SDK tracer to integrate with the Microsoft Agent 365 Telemetry Solution.
@@ -1,4 +1,5 @@
1
- # Copyright (c) Microsoft. All rights reserved.
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
2
3
 
3
4
  # Span Attribute Types
4
5
  from microsoft_agents_a365.observability.core.constants import (
@@ -1,4 +1,5 @@
1
- # Copyright (c) Microsoft. All rights reserved.
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
2
3
 
3
4
  # Wrapper for OpenAI Agents SDK
4
5
 
@@ -1,4 +1,5 @@
1
- # Copyright (c) Microsoft. All rights reserved.
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
2
3
 
3
4
  # Processor for OpenAI Agents SDK
4
5
 
@@ -21,13 +22,17 @@ from agents.tracing.span_data import (
21
22
  from microsoft_agents_a365.observability.core.constants import (
22
23
  CUSTOM_PARENT_SPAN_ID_KEY,
23
24
  EXECUTE_TOOL_OPERATION_NAME,
25
+ GEN_AI_EXECUTION_TYPE_KEY,
24
26
  GEN_AI_INPUT_MESSAGES_KEY,
25
27
  GEN_AI_OPERATION_NAME_KEY,
26
28
  GEN_AI_OUTPUT_MESSAGES_KEY,
27
29
  GEN_AI_REQUEST_MODEL_KEY,
28
30
  GEN_AI_SYSTEM_KEY,
31
+ GEN_AI_TOOL_CALL_ID_KEY,
32
+ GEN_AI_TOOL_TYPE_KEY,
29
33
  INVOKE_AGENT_OPERATION_NAME,
30
34
  )
35
+ from microsoft_agents_a365.observability.core.execution_type import ExecutionType
31
36
  from microsoft_agents_a365.observability.core.utils import as_utc_nano, safe_json_dumps
32
37
  from opentelemetry import trace as ot_trace
33
38
  from opentelemetry.context import attach, detach
@@ -44,10 +49,13 @@ from openai.types.responses import (
44
49
  )
45
50
 
46
51
  from .constants import (
47
- GEN_AI_GRAPH_NODE_ID,
48
52
  GEN_AI_GRAPH_NODE_PARENT_ID,
49
53
  )
50
54
  from .utils import (
55
+ capture_input_message,
56
+ capture_output_message,
57
+ capture_tool_call_ids,
58
+ find_ancestor_agent_span_id,
51
59
  get_attributes_from_function_span_data,
52
60
  get_attributes_from_generation_span_data,
53
61
  get_attributes_from_input,
@@ -56,6 +64,7 @@ from .utils import (
56
64
  get_span_kind,
57
65
  get_span_name,
58
66
  get_span_status,
67
+ get_tool_call_id,
59
68
  )
60
69
 
61
70
  logger = logging.getLogger(__name__)
@@ -68,6 +77,7 @@ Custom Trace Processor for OpenAI Agents SDK
68
77
 
69
78
  class OpenAIAgentsTraceProcessor(TracingProcessor):
70
79
  _MAX_HANDOFFS_IN_FLIGHT = 1000
80
+ _MAX_PENDING_TOOL_CALLS = 1000
71
81
 
72
82
  def __init__(self, tracer: Tracer) -> None:
73
83
  self._tracer = tracer
@@ -79,6 +89,17 @@ class OpenAIAgentsTraceProcessor(TracingProcessor):
79
89
  # Use an OrderedDict and _MAX_HANDOFFS_IN_FLIGHT to cap the size of the dict
80
90
  # in case there are large numbers of orphaned handoffs
81
91
  self._reverse_handoffs_dict: OrderedDict[str, str] = OrderedDict()
92
+ # Track input/output messages for agent spans (keyed by agent span_id)
93
+ self._agent_inputs: dict[str, str] = {}
94
+ self._agent_outputs: dict[str, str] = {}
95
+ # Track agent span IDs to find nearest ancestor
96
+ self._agent_span_ids: set[str] = set()
97
+ # Track parent-child relationships: child_span_id -> parent_span_id
98
+ self._span_parents: dict[str, str] = {}
99
+ # Track tool_call_ids from GenerationSpan: (function_name, trace_id) -> call_id
100
+ # Use an OrderedDict and _MAX_PENDING_TOOL_CALLS to cap the size of the dict
101
+ # in case tool calls are captured but never consumed
102
+ self._pending_tool_calls: OrderedDict[str, str] = OrderedDict()
82
103
 
83
104
  # helper
84
105
  def _stamp_custom_parent(self, otel_span: OtelSpan, trace_id: str) -> None:
@@ -133,6 +154,12 @@ class OpenAIAgentsTraceProcessor(TracingProcessor):
133
154
  )
134
155
  self._otel_spans[span.span_id] = otel_span
135
156
  self._tokens[span.span_id] = attach(set_span_in_context(otel_span))
157
+ # Track parent-child relationship for ancestor lookup
158
+ if span.parent_id:
159
+ self._span_parents[span.span_id] = span.parent_id
160
+ # Track AgentSpan IDs
161
+ if isinstance(span.span_data, AgentSpanData):
162
+ self._agent_span_ids.add(span.span_id)
136
163
 
137
164
  def on_span_end(self, span: Span[Any]) -> None:
138
165
  """Called when a span is finished. Should not block or raise exceptions.
@@ -142,6 +169,8 @@ class OpenAIAgentsTraceProcessor(TracingProcessor):
142
169
  """
143
170
  if token := self._tokens.pop(span.span_id, None):
144
171
  detach(token) # type: ignore[arg-type]
172
+ # Clean up parent tracking
173
+ self._span_parents.pop(span.span_id, None)
145
174
  if not (otel_span := self._otel_spans.pop(span.span_id, None)):
146
175
  return
147
176
  otel_span.update_name(get_span_name(span))
@@ -167,6 +196,19 @@ class OpenAIAgentsTraceProcessor(TracingProcessor):
167
196
  for k, v in get_attributes_from_generation_span_data(data):
168
197
  otel_span.set_attribute(k, v)
169
198
  self._stamp_custom_parent(otel_span, span.trace_id)
199
+ # Capture input/output messages for nearest ancestor agent span
200
+ if agent_span_id := find_ancestor_agent_span_id(
201
+ span.parent_id, self._agent_span_ids, self._span_parents
202
+ ):
203
+ if data.input:
204
+ capture_input_message(agent_span_id, data.input, self._agent_inputs)
205
+ if data.output:
206
+ capture_output_message(agent_span_id, data.output, self._agent_outputs)
207
+ # Capture tool_call_ids for later use by FunctionSpan
208
+ if data.output:
209
+ capture_tool_call_ids(
210
+ data.output, self._pending_tool_calls, self._MAX_PENDING_TOOL_CALLS
211
+ )
170
212
  otel_span.update_name(
171
213
  f"{otel_span.attributes[GEN_AI_OPERATION_NAME_KEY]} {otel_span.attributes[GEN_AI_REQUEST_MODEL_KEY]}"
172
214
  )
@@ -174,7 +216,12 @@ class OpenAIAgentsTraceProcessor(TracingProcessor):
174
216
  for k, v in get_attributes_from_function_span_data(data):
175
217
  otel_span.set_attribute(k, v)
176
218
  self._stamp_custom_parent(otel_span, span.trace_id)
177
- otel_span.update_name(f"{EXECUTE_TOOL_OPERATION_NAME} {data.function_name}")
219
+ otel_span.set_attribute(GEN_AI_TOOL_TYPE_KEY, data.type)
220
+ # Set tool_call_id if available from preceding GenerationSpan
221
+ func_args = data.input if data.input else ""
222
+ if tool_call_id := get_tool_call_id(data.name, func_args, self._pending_tool_calls):
223
+ otel_span.set_attribute(GEN_AI_TOOL_CALL_ID_KEY, tool_call_id)
224
+ otel_span.update_name(f"{EXECUTE_TOOL_OPERATION_NAME} {data.name}")
178
225
  elif isinstance(data, MCPListToolsSpanData):
179
226
  for k, v in get_attributes_from_mcp_list_tool_span_data(data):
180
227
  otel_span.set_attribute(k, v)
@@ -187,12 +234,19 @@ class OpenAIAgentsTraceProcessor(TracingProcessor):
187
234
  while len(self._reverse_handoffs_dict) > self._MAX_HANDOFFS_IN_FLIGHT:
188
235
  self._reverse_handoffs_dict.popitem(last=False)
189
236
  elif isinstance(data, AgentSpanData):
190
- otel_span.set_attribute(GEN_AI_GRAPH_NODE_ID, data.name)
237
+ otel_span.set_attribute(GEN_AI_EXECUTION_TYPE_KEY, ExecutionType.HUMAN_TO_AGENT.value)
191
238
  # Lookup the parent node if exists
192
239
  key = f"{data.name}:{span.trace_id}"
193
240
  if parent_node := self._reverse_handoffs_dict.pop(key, None):
194
241
  otel_span.set_attribute(GEN_AI_GRAPH_NODE_PARENT_ID, parent_node)
242
+ # Apply captured input/output messages from child spans
243
+ if input_msg := self._agent_inputs.pop(span.span_id, None):
244
+ otel_span.set_attribute(GEN_AI_INPUT_MESSAGES_KEY, input_msg)
245
+ if output_msg := self._agent_outputs.pop(span.span_id, None):
246
+ otel_span.set_attribute(GEN_AI_OUTPUT_MESSAGES_KEY, output_msg)
195
247
  otel_span.update_name(f"{INVOKE_AGENT_OPERATION_NAME} {get_span_name(span)}")
248
+ # Clean up tracking
249
+ self._agent_span_ids.discard(span.span_id)
196
250
 
197
251
  end_time: int | None = None
198
252
  if span.ended_at:
@@ -1,4 +1,5 @@
1
- # Copyright (c) Microsoft. All rights reserved.
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
2
3
 
3
4
  # -------------------------------------------------- #
4
5
  # HELPER FUNCTIONS ###
@@ -22,6 +23,7 @@ from agents.tracing.span_data import (
22
23
  )
23
24
  from microsoft_agents_a365.observability.core.constants import (
24
25
  GEN_AI_CHOICE,
26
+ GEN_AI_EVENT_CONTENT,
25
27
  GEN_AI_EXECUTION_PAYLOAD_KEY,
26
28
  GEN_AI_INPUT_MESSAGES_KEY,
27
29
  GEN_AI_OUTPUT_MESSAGES_KEY,
@@ -364,9 +366,9 @@ def get_attributes_from_function_span_data(
364
366
  ) -> Iterator[tuple[str, AttributeValue]]:
365
367
  yield GEN_AI_TOOL_NAME_KEY, obj.name
366
368
  if obj.input:
367
- yield GEN_AI_INPUT_MESSAGES_KEY, obj.input
369
+ yield GEN_AI_TOOL_ARGS_KEY, obj.input
368
370
  if obj.output is not None:
369
- yield GEN_AI_OUTPUT_MESSAGES_KEY, _convert_to_primitive(obj.output)
371
+ yield GEN_AI_EVENT_CONTENT, _convert_to_primitive(obj.output)
370
372
 
371
373
 
372
374
  def get_attributes_from_message_content_list(
@@ -534,3 +536,100 @@ def get_span_status(obj: Span[Any]) -> Status:
534
536
  )
535
537
  else:
536
538
  return Status(StatusCode.OK)
539
+
540
+
541
+ def capture_tool_call_ids(
542
+ output_list: Any, pending_tool_calls: dict[str, str], max_size: int = 1000
543
+ ) -> None:
544
+ """Extract and store tool_call_ids from generation output for later use by FunctionSpan.
545
+
546
+ Args:
547
+ output_list: The generation output containing tool calls
548
+ pending_tool_calls: OrderedDict to store pending tool calls
549
+ max_size: Maximum number of pending tool calls to keep in memory
550
+ """
551
+ if not output_list:
552
+ return
553
+ try:
554
+ for msg in output_list:
555
+ if isinstance(msg, dict) and msg.get("role") == "assistant":
556
+ tool_calls = msg.get("tool_calls")
557
+ if tool_calls:
558
+ for tc in tool_calls:
559
+ if isinstance(tc, dict):
560
+ call_id = tc.get("id")
561
+ func = tc.get("function", {})
562
+ func_name = func.get("name") if isinstance(func, dict) else None
563
+ func_args = func.get("arguments", "") if isinstance(func, dict) else ""
564
+ if call_id and func_name:
565
+ # Key by (function_name, arguments) to uniquely identify each call
566
+ key = f"{func_name}:{func_args}"
567
+ pending_tool_calls[key] = call_id
568
+ # Cap the size of the dict to prevent unbounded growth
569
+ while len(pending_tool_calls) > max_size:
570
+ pending_tool_calls.popitem(last=False)
571
+ except Exception:
572
+ pass
573
+
574
+
575
+ def get_tool_call_id(
576
+ function_name: str, function_args: str, pending_tool_calls: dict[str, str]
577
+ ) -> str | None:
578
+ """Get and remove the tool_call_id for a function with specific arguments."""
579
+ key = f"{function_name}:{function_args}"
580
+ return pending_tool_calls.pop(key, None)
581
+
582
+
583
+ def capture_input_message(
584
+ parent_span_id: str, input_list: Any, agent_inputs: dict[str, str]
585
+ ) -> None:
586
+ """Extract and store the first user message from input list for parent agent span."""
587
+ if parent_span_id in agent_inputs:
588
+ return # Already captured
589
+ if not input_list:
590
+ return
591
+ try:
592
+ for msg in input_list:
593
+ if isinstance(msg, dict) and msg.get("role") == "user":
594
+ content = msg.get("content", "")
595
+ if content:
596
+ agent_inputs[parent_span_id] = str(content)
597
+ return
598
+ except Exception:
599
+ pass
600
+
601
+
602
+ def capture_output_message(
603
+ parent_span_id: str, output_list: Any, agent_outputs: dict[str, str]
604
+ ) -> None:
605
+ """Extract and store the last assistant message with actual content (no tool calls) for parent agent span."""
606
+ if not output_list:
607
+ return
608
+ try:
609
+ # Iterate in reverse to get the last assistant message with content (not a tool call)
610
+ output_items = list(output_list) if not isinstance(output_list, list) else output_list
611
+ for msg in reversed(output_items):
612
+ if isinstance(msg, dict) and msg.get("role") == "assistant":
613
+ content = msg.get("content")
614
+ tool_calls = msg.get("tool_calls")
615
+ # Only capture if there's actual content and no tool_calls
616
+ # (tool_calls means this is an intermediate step, not the final response)
617
+ if content and not tool_calls:
618
+ agent_outputs[parent_span_id] = str(content)
619
+ return
620
+ except Exception:
621
+ pass
622
+
623
+
624
+ def find_ancestor_agent_span_id(
625
+ span_id: str | None, agent_span_ids: set[str], span_parents: dict[str, str]
626
+ ) -> str | None:
627
+ """Walk up the parent chain to find the nearest ancestor AgentSpan."""
628
+ current = span_id
629
+ visited: set[str] = set() # Prevent infinite loops
630
+ while current and current not in visited:
631
+ if current in agent_span_ids:
632
+ return current
633
+ visited.add(current)
634
+ current = span_parents.get(current)
635
+ return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microsoft-agents-a365-observability-extensions-openai
3
- Version: 0.2.0.dev5
3
+ Version: 0.2.1.dev0
4
4
  Summary: OpenAI Agents SDK observability and tracing extensions for Microsoft Agent 365
5
5
  Author-email: Microsoft <support@microsoft.com>
6
6
  License: MIT
@@ -20,6 +20,7 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
20
  Classifier: Topic :: System :: Monitoring
21
21
  Requires-Python: >=3.11
22
22
  Description-Content-Type: text/markdown
23
+ License-File: LICENSE
23
24
  Requires-Dist: microsoft-agents-a365-observability-core>=0.0.0
24
25
  Requires-Dist: openai-agents>=0.2.6
25
26
  Requires-Dist: opentelemetry-api>=1.36.0
@@ -34,6 +35,7 @@ Requires-Dist: mypy>=1.0.0; extra == "dev"
34
35
  Provides-Extra: test
35
36
  Requires-Dist: pytest>=7.0.0; extra == "test"
36
37
  Requires-Dist: pytest-asyncio>=0.21.0; extra == "test"
38
+ Dynamic: license-file
37
39
 
38
40
  # microsoft-agents-a365-observability-extensions-openai
39
41
 
@@ -1,6 +1,9 @@
1
1
  README.md
2
2
  pyproject.toml
3
3
  setup.py
4
+ ../../LICENSE
5
+ docs/../../LICENSE
6
+ microsoft_agents_a365/../../LICENSE
4
7
  microsoft_agents_a365/observability/extensions/openai/__init__.py
5
8
  microsoft_agents_a365/observability/extensions/openai/constants.py
6
9
  microsoft_agents_a365/observability/extensions/openai/trace_instrumentor.py
@@ -69,6 +69,10 @@ target-version = ['py311']
69
69
  line-length = 100
70
70
  target-version = "py311"
71
71
 
72
+ [tool.ruff.lint.flake8-copyright]
73
+ notice-rgx = "# Copyright \\(c\\) Microsoft Corporation\\.\\r?\\n# Licensed under the MIT License\\."
74
+ min-file-size = 1
75
+
72
76
  [tool.mypy]
73
77
  python_version = "3.11"
74
78
  strict = true
@@ -13,7 +13,7 @@ package_version = environ.get("AGENT365_PYTHON_SDK_PACKAGE_VERSION", "0.0.0")
13
13
  helper_path = Path(__file__).parent.parent.parent / "versioning" / "helper"
14
14
  sys.path.insert(0, str(helper_path))
15
15
 
16
- from setup_utils import get_dynamic_dependencies
16
+ from setup_utils import get_dynamic_dependencies # noqa: E402
17
17
 
18
18
  # Use minimum version strategy:
19
19
  # - Internal packages get: >= current_base_version (e.g., >= 0.1.0)