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.
- microsoft_agents_a365/observability/core/__init__.py +61 -0
- microsoft_agents_a365/observability/core/agent_details.py +42 -0
- microsoft_agents_a365/observability/core/config.py +246 -0
- microsoft_agents_a365/observability/core/constants.py +107 -0
- microsoft_agents_a365/observability/core/execute_tool_scope.py +88 -0
- microsoft_agents_a365/observability/core/execution_type.py +13 -0
- microsoft_agents_a365/observability/core/exporters/agent365_exporter.py +310 -0
- microsoft_agents_a365/observability/core/exporters/utils.py +72 -0
- microsoft_agents_a365/observability/core/inference_call_details.py +18 -0
- microsoft_agents_a365/observability/core/inference_operation_type.py +11 -0
- microsoft_agents_a365/observability/core/inference_scope.py +140 -0
- microsoft_agents_a365/observability/core/invoke_agent_details.py +17 -0
- microsoft_agents_a365/observability/core/invoke_agent_scope.py +166 -0
- microsoft_agents_a365/observability/core/middleware/__init__.py +7 -0
- microsoft_agents_a365/observability/core/middleware/baggage_builder.py +319 -0
- microsoft_agents_a365/observability/core/middleware/turn_context_baggage.py +193 -0
- microsoft_agents_a365/observability/core/models/__init__.py +2 -0
- microsoft_agents_a365/observability/core/models/agent_type.py +25 -0
- microsoft_agents_a365/observability/core/models/caller_details.py +25 -0
- microsoft_agents_a365/observability/core/opentelemetry_scope.py +250 -0
- microsoft_agents_a365/observability/core/request.py +19 -0
- microsoft_agents_a365/observability/core/source_metadata.py +15 -0
- microsoft_agents_a365/observability/core/tenant_details.py +11 -0
- microsoft_agents_a365/observability/core/tool_call_details.py +18 -0
- microsoft_agents_a365/observability/core/tool_type.py +13 -0
- microsoft_agents_a365/observability/core/trace_processor/__init__.py +13 -0
- microsoft_agents_a365/observability/core/trace_processor/span_processor.py +75 -0
- microsoft_agents_a365/observability/core/trace_processor/util.py +44 -0
- microsoft_agents_a365/observability/core/utils.py +151 -0
- microsoft_agents_a365_observability_core-0.1.0.dist-info/METADATA +78 -0
- microsoft_agents_a365_observability_core-0.1.0.dist-info/RECORD +33 -0
- microsoft_agents_a365_observability_core-0.1.0.dist-info/WHEEL +5 -0
- 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,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))
|