microsoft-agents-a365-observability-core 0.1.0__py3-none-any.whl

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 (33) hide show
  1. microsoft_agents_a365/observability/core/__init__.py +61 -0
  2. microsoft_agents_a365/observability/core/agent_details.py +42 -0
  3. microsoft_agents_a365/observability/core/config.py +246 -0
  4. microsoft_agents_a365/observability/core/constants.py +107 -0
  5. microsoft_agents_a365/observability/core/execute_tool_scope.py +88 -0
  6. microsoft_agents_a365/observability/core/execution_type.py +13 -0
  7. microsoft_agents_a365/observability/core/exporters/agent365_exporter.py +310 -0
  8. microsoft_agents_a365/observability/core/exporters/utils.py +72 -0
  9. microsoft_agents_a365/observability/core/inference_call_details.py +18 -0
  10. microsoft_agents_a365/observability/core/inference_operation_type.py +11 -0
  11. microsoft_agents_a365/observability/core/inference_scope.py +140 -0
  12. microsoft_agents_a365/observability/core/invoke_agent_details.py +17 -0
  13. microsoft_agents_a365/observability/core/invoke_agent_scope.py +166 -0
  14. microsoft_agents_a365/observability/core/middleware/__init__.py +7 -0
  15. microsoft_agents_a365/observability/core/middleware/baggage_builder.py +319 -0
  16. microsoft_agents_a365/observability/core/middleware/turn_context_baggage.py +193 -0
  17. microsoft_agents_a365/observability/core/models/__init__.py +2 -0
  18. microsoft_agents_a365/observability/core/models/agent_type.py +25 -0
  19. microsoft_agents_a365/observability/core/models/caller_details.py +25 -0
  20. microsoft_agents_a365/observability/core/opentelemetry_scope.py +250 -0
  21. microsoft_agents_a365/observability/core/request.py +19 -0
  22. microsoft_agents_a365/observability/core/source_metadata.py +15 -0
  23. microsoft_agents_a365/observability/core/tenant_details.py +11 -0
  24. microsoft_agents_a365/observability/core/tool_call_details.py +18 -0
  25. microsoft_agents_a365/observability/core/tool_type.py +13 -0
  26. microsoft_agents_a365/observability/core/trace_processor/__init__.py +13 -0
  27. microsoft_agents_a365/observability/core/trace_processor/span_processor.py +75 -0
  28. microsoft_agents_a365/observability/core/trace_processor/util.py +44 -0
  29. microsoft_agents_a365/observability/core/utils.py +151 -0
  30. microsoft_agents_a365_observability_core-0.1.0.dist-info/METADATA +78 -0
  31. microsoft_agents_a365_observability_core-0.1.0.dist-info/RECORD +33 -0
  32. microsoft_agents_a365_observability_core-0.1.0.dist-info/WHEEL +5 -0
  33. microsoft_agents_a365_observability_core-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,166 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+
4
+ # Invoke agent scope for tracing agent invocation.
5
+
6
+ from .agent_details import AgentDetails
7
+ from .constants import (
8
+ GEN_AI_CALLER_AGENT_APPLICATION_ID_KEY,
9
+ GEN_AI_CALLER_AGENT_ID_KEY,
10
+ GEN_AI_CALLER_AGENT_NAME_KEY,
11
+ GEN_AI_CALLER_AGENT_TENANT_ID_KEY,
12
+ GEN_AI_CALLER_AGENT_UPN_KEY,
13
+ GEN_AI_CALLER_AGENT_USER_ID_KEY,
14
+ GEN_AI_CALLER_ID_KEY,
15
+ GEN_AI_CALLER_NAME_KEY,
16
+ GEN_AI_CALLER_TENANT_ID_KEY,
17
+ GEN_AI_CALLER_UPN_KEY,
18
+ GEN_AI_CALLER_USER_ID_KEY,
19
+ GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY,
20
+ GEN_AI_EXECUTION_SOURCE_ID_KEY,
21
+ GEN_AI_EXECUTION_SOURCE_NAME_KEY,
22
+ GEN_AI_EXECUTION_TYPE_KEY,
23
+ GEN_AI_INPUT_MESSAGES_KEY,
24
+ GEN_AI_OUTPUT_MESSAGES_KEY,
25
+ INVOKE_AGENT_OPERATION_NAME,
26
+ SERVER_ADDRESS_KEY,
27
+ SERVER_PORT_KEY,
28
+ SESSION_ID_KEY,
29
+ )
30
+ from .invoke_agent_details import InvokeAgentDetails
31
+ from .models.caller_details import CallerDetails
32
+ from .opentelemetry_scope import OpenTelemetryScope
33
+ from .request import Request
34
+ from .tenant_details import TenantDetails
35
+ from .utils import safe_json_dumps
36
+
37
+
38
+ class InvokeAgentScope(OpenTelemetryScope):
39
+ """Provides OpenTelemetry tracing scope for AI agent invocation operations."""
40
+
41
+ @staticmethod
42
+ def start(
43
+ invoke_agent_details: InvokeAgentDetails,
44
+ tenant_details: TenantDetails,
45
+ request: Request | None = None,
46
+ caller_agent_details: AgentDetails | None = None,
47
+ caller_details: CallerDetails | None = None,
48
+ ) -> "InvokeAgentScope":
49
+ """Create and start a new scope for agent invocation tracing.
50
+
51
+ Args:
52
+ invoke_agent_details: The details of the agent invocation including endpoint,
53
+ agent information, and session context
54
+ tenant_details: The details of the tenant
55
+ request: Optional request details for additional context
56
+ caller_agent_details: Optional details of the caller agent
57
+ caller_details: Optional details of the non-agentic caller
58
+
59
+ Returns:
60
+ A new InvokeAgentScope instance
61
+ """
62
+ return InvokeAgentScope(
63
+ invoke_agent_details, tenant_details, request, caller_agent_details, caller_details
64
+ )
65
+
66
+ def __init__(
67
+ self,
68
+ invoke_agent_details: InvokeAgentDetails,
69
+ tenant_details: TenantDetails,
70
+ request: Request | None = None,
71
+ caller_agent_details: AgentDetails | None = None,
72
+ caller_details: CallerDetails | None = None,
73
+ ):
74
+ """Initialize the agent invocation scope.
75
+
76
+ Args:
77
+ invoke_agent_details: The details of the agent invocation
78
+ tenant_details: The details of the tenant
79
+ request: Optional request details for additional context
80
+ caller_agent_details: Optional details of the caller agent
81
+ caller_details: Optional details of the non-agentic caller
82
+ """
83
+ activity_name = INVOKE_AGENT_OPERATION_NAME
84
+ if invoke_agent_details.details.agent_name:
85
+ activity_name = (
86
+ f"{INVOKE_AGENT_OPERATION_NAME} {invoke_agent_details.details.agent_name}"
87
+ )
88
+
89
+ super().__init__(
90
+ kind="Client",
91
+ operation_name=INVOKE_AGENT_OPERATION_NAME,
92
+ activity_name=activity_name,
93
+ agent_details=invoke_agent_details.details,
94
+ tenant_details=tenant_details,
95
+ )
96
+
97
+ endpoint, _, session_id = (
98
+ invoke_agent_details.endpoint,
99
+ invoke_agent_details.details,
100
+ invoke_agent_details.session_id,
101
+ )
102
+
103
+ self.set_tag_maybe(SESSION_ID_KEY, session_id)
104
+ if endpoint:
105
+ self.set_tag_maybe(SERVER_ADDRESS_KEY, endpoint.hostname)
106
+
107
+ # Only record port if it is different from 443
108
+ if endpoint.port and endpoint.port != 443:
109
+ self.set_tag_maybe(SERVER_PORT_KEY, endpoint.port)
110
+
111
+ # Set request metadata if provided
112
+ if request:
113
+ if request.source_metadata:
114
+ self.set_tag_maybe(GEN_AI_EXECUTION_SOURCE_ID_KEY, request.source_metadata.id)
115
+ self.set_tag_maybe(GEN_AI_EXECUTION_SOURCE_NAME_KEY, request.source_metadata.name)
116
+ self.set_tag_maybe(
117
+ GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY, request.source_metadata.description
118
+ )
119
+
120
+ self.set_tag_maybe(
121
+ GEN_AI_EXECUTION_TYPE_KEY,
122
+ request.execution_type.value if request.execution_type else None,
123
+ )
124
+
125
+ # Set caller details tags
126
+ if caller_details:
127
+ self.set_tag_maybe(GEN_AI_CALLER_ID_KEY, caller_details.caller_id)
128
+ self.set_tag_maybe(GEN_AI_CALLER_UPN_KEY, caller_details.caller_upn)
129
+ self.set_tag_maybe(GEN_AI_CALLER_NAME_KEY, caller_details.caller_name)
130
+ self.set_tag_maybe(GEN_AI_CALLER_USER_ID_KEY, caller_details.caller_user_id)
131
+ self.set_tag_maybe(GEN_AI_CALLER_TENANT_ID_KEY, caller_details.tenant_id)
132
+
133
+ # Set caller agent details tags
134
+ if caller_agent_details:
135
+ self.set_tag_maybe(GEN_AI_CALLER_AGENT_NAME_KEY, caller_agent_details.agent_name)
136
+ self.set_tag_maybe(GEN_AI_CALLER_AGENT_ID_KEY, caller_agent_details.agent_id)
137
+ self.set_tag_maybe(
138
+ GEN_AI_CALLER_AGENT_APPLICATION_ID_KEY, caller_agent_details.agent_blueprint_id
139
+ )
140
+ self.set_tag_maybe(GEN_AI_CALLER_AGENT_USER_ID_KEY, caller_agent_details.agent_auid)
141
+ self.set_tag_maybe(GEN_AI_CALLER_AGENT_UPN_KEY, caller_agent_details.agent_upn)
142
+ self.set_tag_maybe(GEN_AI_CALLER_AGENT_TENANT_ID_KEY, caller_agent_details.tenant_id)
143
+
144
+ def record_response(self, response: str) -> None:
145
+ """Record response information for telemetry tracking.
146
+
147
+ Args:
148
+ response: The response string to record
149
+ """
150
+ self.record_output_messages([response])
151
+
152
+ def record_input_messages(self, messages: list[str]) -> None:
153
+ """Record the input messages for telemetry tracking.
154
+
155
+ Args:
156
+ messages: List of input messages to record
157
+ """
158
+ self.set_tag_maybe(GEN_AI_INPUT_MESSAGES_KEY, safe_json_dumps(messages))
159
+
160
+ def record_output_messages(self, messages: list[str]) -> None:
161
+ """Record the output messages for telemetry tracking.
162
+
163
+ Args:
164
+ messages: List of output messages to record
165
+ """
166
+ self.set_tag_maybe(GEN_AI_OUTPUT_MESSAGES_KEY, safe_json_dumps(messages))
@@ -0,0 +1,7 @@
1
+ # Copyright (c) Microsoft. All rights reserved.
2
+
3
+ # Middleware components for Microsoft Agent 365 SDK.
4
+
5
+ from .baggage_builder import BaggageBuilder
6
+
7
+ __all__ = ["BaggageBuilder"]
@@ -0,0 +1,319 @@
1
+ # Copyright (c) Microsoft. All rights reserved.
2
+
3
+ # Per request baggage builder for OpenTelemetry context propagation.
4
+
5
+ from typing import Any
6
+
7
+ from opentelemetry import baggage, context
8
+
9
+ from ..constants import (
10
+ CORRELATION_ID_KEY,
11
+ GEN_AI_AGENT_AUID_KEY,
12
+ GEN_AI_AGENT_BLUEPRINT_ID_KEY,
13
+ GEN_AI_AGENT_DESCRIPTION_KEY,
14
+ GEN_AI_AGENT_ID_KEY,
15
+ GEN_AI_AGENT_NAME_KEY,
16
+ GEN_AI_AGENT_UPN_KEY,
17
+ GEN_AI_CALLER_ID_KEY,
18
+ GEN_AI_CALLER_NAME_KEY,
19
+ GEN_AI_CALLER_UPN_KEY,
20
+ GEN_AI_CONVERSATION_ID_KEY,
21
+ GEN_AI_CONVERSATION_ITEM_LINK_KEY,
22
+ GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY,
23
+ GEN_AI_EXECUTION_SOURCE_ID_KEY,
24
+ GEN_AI_EXECUTION_SOURCE_NAME_KEY,
25
+ HIRING_MANAGER_ID_KEY,
26
+ OPERATION_SOURCE_KEY,
27
+ TENANT_ID_KEY,
28
+ )
29
+ from .turn_context_baggage import from_turn_context
30
+
31
+
32
+ class BaggageBuilder:
33
+ """Per request baggage builder.
34
+
35
+ This class provides a fluent API for setting baggage values that will be
36
+ propagated in the OpenTelemetry context.
37
+
38
+ Example:
39
+ >>> with BaggageBuilder() \
40
+ ... .tenant_id("tenant-123") \
41
+ ... .agent_id("agent-456") \
42
+ ... .correlation_id("corr-789") \
43
+ ... .build():
44
+ ... # Baggage is set in this context
45
+ ... pass
46
+ >>> # Baggage is restored after exiting the context
47
+ """
48
+
49
+ def __init__(self):
50
+ """Initialize the baggage builder."""
51
+ self._pairs: dict[str, str] = {}
52
+
53
+ def operation_source(self, value: str | None) -> "BaggageBuilder":
54
+ """Set the operation source baggage value.
55
+
56
+ Args:
57
+ value: The operation source value
58
+
59
+ Returns:
60
+ Self for method chaining
61
+ """
62
+ self._set(OPERATION_SOURCE_KEY, value)
63
+ return self
64
+
65
+ def tenant_id(self, value: str | None) -> "BaggageBuilder":
66
+ """Set the tenant ID baggage value.
67
+
68
+ Args:
69
+ value: The tenant ID
70
+
71
+ Returns:
72
+ Self for method chaining
73
+ """
74
+ self._set(TENANT_ID_KEY, value)
75
+ return self
76
+
77
+ def agent_id(self, value: str | None) -> "BaggageBuilder":
78
+ """Set the agent ID baggage value.
79
+
80
+ Args:
81
+ value: The agent ID
82
+
83
+ Returns:
84
+ Self for method chaining
85
+ """
86
+ self._set(GEN_AI_AGENT_ID_KEY, value)
87
+ return self
88
+
89
+ def agent_auid(self, value: str | None) -> "BaggageBuilder":
90
+ """Set the agent AUID baggage value.
91
+
92
+ Args:
93
+ value: The agent AUID
94
+
95
+ Returns:
96
+ Self for method chaining
97
+ """
98
+ self._set(GEN_AI_AGENT_AUID_KEY, value)
99
+ return self
100
+
101
+ def agent_upn(self, value: str | None) -> "BaggageBuilder":
102
+ """Set the agent UPN baggage value.
103
+
104
+ Args:
105
+ value: The agent UPN
106
+
107
+ Returns:
108
+ Self for method chaining
109
+ """
110
+ self._set(GEN_AI_AGENT_UPN_KEY, value)
111
+ return self
112
+
113
+ def agent_blueprint_id(self, value: str | None) -> "BaggageBuilder":
114
+ """Set the agent blueprint ID baggage value.
115
+
116
+ Args:
117
+ value: The agent blueprint ID
118
+
119
+ Returns:
120
+ Self for method chaining
121
+ """
122
+ self._set(GEN_AI_AGENT_BLUEPRINT_ID_KEY, value)
123
+ return self
124
+
125
+ def correlation_id(self, value: str | None) -> "BaggageBuilder":
126
+ """Set the correlation ID baggage value.
127
+
128
+ Args:
129
+ value: The correlation ID
130
+
131
+ Returns:
132
+ Self for method chaining
133
+ """
134
+ self._set(CORRELATION_ID_KEY, value)
135
+ return self
136
+
137
+ def caller_id(self, value: str | None) -> "BaggageBuilder":
138
+ """Set the caller ID baggage value.
139
+
140
+ Args:
141
+ value: The caller ID
142
+
143
+ Returns:
144
+ Self for method chaining
145
+ """
146
+ self._set(GEN_AI_CALLER_ID_KEY, value)
147
+ return self
148
+
149
+ def hiring_manager_id(self, value: str | None) -> "BaggageBuilder":
150
+ """Set the hiring manager ID baggage value.
151
+
152
+ Args:
153
+ value: The hiring manager ID
154
+
155
+ Returns:
156
+ Self for method chaining
157
+ """
158
+ self._set(HIRING_MANAGER_ID_KEY, value)
159
+ return self
160
+
161
+ def agent_name(self, value: str | None) -> "BaggageBuilder":
162
+ """Set the agent name baggage value."""
163
+ self._set(GEN_AI_AGENT_NAME_KEY, value)
164
+ return self
165
+
166
+ def agent_description(self, value: str | None) -> "BaggageBuilder":
167
+ """Set the agent description baggage value."""
168
+ self._set(GEN_AI_AGENT_DESCRIPTION_KEY, value)
169
+ return self
170
+
171
+ def caller_name(self, value: str | None) -> "BaggageBuilder":
172
+ """Set the caller name baggage value."""
173
+ self._set(GEN_AI_CALLER_NAME_KEY, value)
174
+ return self
175
+
176
+ def caller_upn(self, value: str | None) -> "BaggageBuilder":
177
+ """Set the caller UPN baggage value."""
178
+ self._set(GEN_AI_CALLER_UPN_KEY, value)
179
+ return self
180
+
181
+ def conversation_id(self, value: str | None) -> "BaggageBuilder":
182
+ """Set the conversation ID baggage value."""
183
+ self._set(GEN_AI_CONVERSATION_ID_KEY, value)
184
+ return self
185
+
186
+ def conversation_item_link(self, value: str | None) -> "BaggageBuilder":
187
+ """Set the conversation item link baggage value."""
188
+ self._set(GEN_AI_CONVERSATION_ITEM_LINK_KEY, value)
189
+ return self
190
+
191
+ def source_metadata_id(self, value: str | None) -> "BaggageBuilder":
192
+ """Set the execution source metadata ID (e.g., channel ID)."""
193
+ self._set(GEN_AI_EXECUTION_SOURCE_ID_KEY, value)
194
+ return self
195
+
196
+ def source_metadata_name(self, value: str | None) -> "BaggageBuilder":
197
+ """Set the execution source metadata name (e.g., channel name)."""
198
+ self._set(GEN_AI_EXECUTION_SOURCE_NAME_KEY, value)
199
+ return self
200
+
201
+ def source_metadata_description(self, value: str | None) -> "BaggageBuilder":
202
+ """Set the execution source metadata description (e.g., channel description)."""
203
+ self._set(GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY, value)
204
+ return self
205
+
206
+ def from_turn_context(self, turn_context: Any) -> "BaggageBuilder":
207
+ """
208
+ Populate baggage from a turn_context (duck-typed).
209
+ Delegates to baggage_turn_context.from_turn_context.
210
+ """
211
+
212
+ return self.set_pairs(from_turn_context(turn_context))
213
+
214
+ def set_pairs(self, pairs: Any) -> "BaggageBuilder":
215
+ """
216
+ Accept dict or iterable of (k,v).
217
+ """
218
+ if not pairs:
219
+ return self
220
+ if isinstance(pairs, dict):
221
+ iterator = pairs.items()
222
+ else:
223
+ iterator = pairs
224
+ for k, v in iterator:
225
+ if v is None:
226
+ continue
227
+ self._set(str(k), str(v))
228
+ return self
229
+
230
+ def build(self) -> "BaggageScope":
231
+ """Apply the collected baggage to the current context.
232
+
233
+ Returns:
234
+ A context manager that restores the previous baggage on exit
235
+ """
236
+ return BaggageScope(self._pairs)
237
+
238
+ def _set(self, key: str, value: str | None) -> None:
239
+ """Add a baggage key/value if the value is not None or whitespace.
240
+
241
+ Args:
242
+ key: The baggage key
243
+ value: The baggage value
244
+ """
245
+ if value is not None and value.strip():
246
+ self._pairs[key] = value
247
+
248
+ @staticmethod
249
+ def set_request_context(
250
+ tenant_id: str | None = None,
251
+ agent_id: str | None = None,
252
+ correlation_id: str | None = None,
253
+ ) -> "BaggageScope":
254
+ """Convenience method to begin a request baggage scope with common fields.
255
+
256
+ Args:
257
+ tenant_id: The tenant ID
258
+ agent_id: The agent ID
259
+ correlation_id: The correlation ID
260
+
261
+ Returns:
262
+ A context manager that restores the previous baggage on exit
263
+ """
264
+ return (
265
+ BaggageBuilder()
266
+ .tenant_id(tenant_id)
267
+ .agent_id(agent_id)
268
+ .correlation_id(correlation_id)
269
+ .build()
270
+ )
271
+
272
+
273
+ class BaggageScope:
274
+ """Context manager for baggage scope.
275
+
276
+ This class manages the lifecycle of baggage values, setting them on enter
277
+ and restoring the previous context on exit.
278
+ """
279
+
280
+ def __init__(self, pairs: dict[str, str]):
281
+ """Initialize the baggage scope.
282
+
283
+ Args:
284
+ pairs: Dictionary of baggage key-value pairs to set
285
+ """
286
+ self._pairs = pairs
287
+ self._previous_context: Any = None
288
+ self._token: Any = None
289
+
290
+ def __enter__(self) -> "BaggageScope":
291
+ """Enter the context and set baggage values.
292
+
293
+ Returns:
294
+ Self
295
+ """
296
+ # Get the current context
297
+ self._previous_context = context.get_current()
298
+
299
+ # Set all baggage values in the new context
300
+ new_context = self._previous_context
301
+ for key, value in self._pairs.items():
302
+ if value and value.strip():
303
+ new_context = baggage.set_baggage(key, value, context=new_context)
304
+
305
+ # Attach the new context
306
+ self._token = context.attach(new_context)
307
+ return self
308
+
309
+ def __exit__(self, exc_type, exc_val, exc_tb):
310
+ """Exit the context and restore previous baggage.
311
+
312
+ Args:
313
+ exc_type: Exception type if an exception occurred
314
+ exc_val: Exception value if an exception occurred
315
+ exc_tb: Exception traceback if an exception occurred
316
+ """
317
+ # Detach and restore previous context
318
+ if self._token is not None:
319
+ context.detach(self._token)
@@ -0,0 +1,193 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any, Iterable, Iterator, Mapping
5
+
6
+ from ..constants import (
7
+ GEN_AI_AGENT_AUID_KEY,
8
+ GEN_AI_AGENT_DESCRIPTION_KEY,
9
+ GEN_AI_AGENT_ID_KEY,
10
+ GEN_AI_AGENT_NAME_KEY,
11
+ GEN_AI_AGENT_UPN_KEY,
12
+ GEN_AI_CALLER_ID_KEY,
13
+ GEN_AI_CALLER_NAME_KEY,
14
+ GEN_AI_CALLER_TENANT_ID_KEY,
15
+ GEN_AI_CALLER_UPN_KEY,
16
+ GEN_AI_CALLER_USER_ID_KEY,
17
+ GEN_AI_CONVERSATION_ID_KEY,
18
+ GEN_AI_CONVERSATION_ITEM_LINK_KEY,
19
+ GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY,
20
+ GEN_AI_EXECUTION_SOURCE_ID_KEY,
21
+ GEN_AI_EXECUTION_SOURCE_NAME_KEY,
22
+ GEN_AI_EXECUTION_TYPE_KEY,
23
+ TENANT_ID_KEY,
24
+ )
25
+ from ..execution_type import ExecutionType
26
+
27
+ AGENT_ROLE = "agenticUser"
28
+ CHANNEL_ID_AGENTS = "agents"
29
+ ENTITY_TYPE_WPX_COMMENT = "wpxcomment"
30
+ ENTITY_TYPE_EMAIL_NOTIFICATION = "emailNotification"
31
+ WPX_CONVERSATION_ID_FORMAT = "{document_id}_{parent_comment_id}"
32
+
33
+
34
+ def _safe_get(obj: Any, *names: str) -> Any:
35
+ """Attempt multiple attribute/dict keys; return first non-None."""
36
+ for n in names:
37
+ if obj is None:
38
+ continue
39
+ # dict-like
40
+ if isinstance(obj, Mapping) and n in obj:
41
+ return obj[n]
42
+ # attribute-like (support both camelCase and snake_case lookups)
43
+ if hasattr(obj, n):
44
+ return getattr(obj, n)
45
+ return None
46
+
47
+
48
+ def _extract_channel_data(activity: Any) -> Mapping[str, Any] | None:
49
+ cd = _safe_get(activity, "channel_data")
50
+ if cd is None:
51
+ return None
52
+ if isinstance(cd, Mapping):
53
+ return cd
54
+ if isinstance(cd, str):
55
+ try:
56
+ return json.loads(cd)
57
+ except Exception:
58
+ return None
59
+ return None
60
+
61
+
62
+ def _iter_caller_pairs(activity: Any) -> Iterator[tuple[str, Any]]:
63
+ frm = _safe_get(activity, "from")
64
+ if not frm:
65
+ return
66
+ yield GEN_AI_CALLER_ID_KEY, _safe_get(frm, "id")
67
+ name = _safe_get(frm, "name")
68
+ yield GEN_AI_CALLER_NAME_KEY, name
69
+ # Reuse 'name' as UPN if no separate field
70
+ upn = _safe_get(frm, "upn") or name
71
+ yield GEN_AI_CALLER_UPN_KEY, upn
72
+ user_id = _safe_get(frm, "agentic_user_id", "aad_object_id")
73
+ yield GEN_AI_CALLER_USER_ID_KEY, user_id
74
+ tenant_id = _safe_get(frm, "tenant_id")
75
+ yield GEN_AI_CALLER_TENANT_ID_KEY, tenant_id
76
+
77
+
78
+ def _is_agentic(entity: Any) -> bool:
79
+ return bool(
80
+ _safe_get(
81
+ entity,
82
+ "agentic_user_id",
83
+ )
84
+ or (
85
+ (role := _safe_get(entity, "role", "Role"))
86
+ and isinstance(role, str)
87
+ and role.lower() == AGENT_ROLE.lower()
88
+ )
89
+ )
90
+
91
+
92
+ def _iter_execution_type_pair(activity: Any) -> Iterator[tuple[str, Any]]:
93
+ frm = _safe_get(activity, "from")
94
+ rec = _safe_get(activity, "recipient")
95
+ is_agentic_caller = _is_agentic(frm)
96
+ is_agentic_recipient = _is_agentic(rec)
97
+ exec_type = (
98
+ ExecutionType.AGENT_TO_AGENT.value
99
+ if (is_agentic_caller and is_agentic_recipient)
100
+ else ExecutionType.HUMAN_TO_AGENT.value
101
+ )
102
+ yield GEN_AI_EXECUTION_TYPE_KEY, exec_type
103
+
104
+
105
+ def _iter_target_agent_pairs(activity: Any) -> Iterator[tuple[str, Any]]:
106
+ rec = _safe_get(activity, "recipient")
107
+ if not rec:
108
+ return
109
+ yield GEN_AI_AGENT_ID_KEY, _safe_get(rec, "agentic_app_id")
110
+ yield GEN_AI_AGENT_NAME_KEY, _safe_get(rec, "name")
111
+ auid = _safe_get(rec, "agentic_user_id", "aad_object_id")
112
+ yield GEN_AI_AGENT_AUID_KEY, auid
113
+ yield GEN_AI_AGENT_UPN_KEY, _safe_get(rec, "upn", "name")
114
+ yield (
115
+ GEN_AI_AGENT_DESCRIPTION_KEY,
116
+ _safe_get(rec, "role"),
117
+ )
118
+
119
+
120
+ def _iter_tenant_id_pair(activity: Any) -> Iterator[tuple[str, Any]]:
121
+ rec = _safe_get(activity, "recipient")
122
+ tenant_id = _safe_get(rec, "tenant_id")
123
+ if not tenant_id:
124
+ cd_dict = _extract_channel_data(activity)
125
+ # channelData.tenant.id
126
+ try:
127
+ tenant_id = (
128
+ cd_dict
129
+ and isinstance(cd_dict.get("tenant"), Mapping)
130
+ and cd_dict["tenant"].get("id")
131
+ )
132
+ except Exception:
133
+ tenant_id = None
134
+ yield TENANT_ID_KEY, tenant_id
135
+
136
+
137
+ def _iter_source_metadata_pairs(activity: Any) -> Iterator[tuple[str, Any]]:
138
+ channel_id = _safe_get(activity, "channel_id")
139
+ yield GEN_AI_EXECUTION_SOURCE_ID_KEY, channel_id
140
+ yield GEN_AI_EXECUTION_SOURCE_NAME_KEY, channel_id
141
+ yield GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY, _safe_get(activity, "type", "Type")
142
+
143
+
144
+ def _iter_conversation_pairs(activity: Any) -> Iterator[tuple[str, Any]]:
145
+ channel_id = _safe_get(activity, "channel_id")
146
+ entities = _safe_get(activity, "entities") or []
147
+ conversation_id = None
148
+
149
+ if channel_id == CHANNEL_ID_AGENTS and isinstance(entities, Iterable):
150
+ # search entities for wpxcomment or emailNotification
151
+ for e in entities:
152
+ etype = _safe_get(e, "type", "Type")
153
+ if etype == ENTITY_TYPE_WPX_COMMENT:
154
+ document_id = _safe_get(e, "documentId", "document_id")
155
+ parent_comment_id = _safe_get(e, "parentCommentId", "parent_comment_id")
156
+ if document_id and parent_comment_id:
157
+ conversation_id = WPX_CONVERSATION_ID_FORMAT.format(
158
+ document_id=document_id,
159
+ parent_comment_id=parent_comment_id,
160
+ )
161
+ break
162
+ elif etype == ENTITY_TYPE_EMAIL_NOTIFICATION:
163
+ conversation_id = _safe_get(e, "conversationId", "conversation_id")
164
+ if conversation_id:
165
+ break
166
+ if not conversation_id:
167
+ conv = _safe_get(activity, "conversation")
168
+ conversation_id = _safe_get(conv, "id", "Id")
169
+
170
+ item_link = _safe_get(activity, "service_url")
171
+
172
+ yield GEN_AI_CONVERSATION_ID_KEY, conversation_id
173
+ yield GEN_AI_CONVERSATION_ITEM_LINK_KEY, item_link
174
+
175
+
176
+ def _iter_all_pairs(turn_context: Any) -> Iterator[tuple[str, Any]]:
177
+ activity = _safe_get(
178
+ turn_context,
179
+ "activity",
180
+ )
181
+ if not activity:
182
+ return
183
+ yield from _iter_caller_pairs(activity)
184
+ yield from _iter_execution_type_pair(activity)
185
+ yield from _iter_target_agent_pairs(activity)
186
+ yield from _iter_tenant_id_pair(activity)
187
+ yield from _iter_source_metadata_pairs(activity)
188
+ yield from _iter_conversation_pairs(activity)
189
+
190
+
191
+ def from_turn_context(turn_context: Any) -> dict:
192
+ """Populate builder with baggage values extracted from a turn context."""
193
+ return dict(_iter_all_pairs(turn_context))
@@ -0,0 +1,2 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.