microsoft-agents-a365-observability-core 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.
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/PKG-INFO +3 -1
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365/observability/core/__init__.py +2 -1
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365/observability/core/config.py +34 -20
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365/observability/core/constants.py +2 -1
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365/observability/core/execute_tool_scope.py +15 -1
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365/observability/core/execution_type.py +2 -1
- microsoft_agents_a365_observability_core-0.2.1.dev0/microsoft_agents_a365/observability/core/exporters/__init__.py +8 -0
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365/observability/core/exporters/agent365_exporter.py +50 -6
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365/observability/core/exporters/utils.py +56 -1
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365/observability/core/inference_call_details.py +2 -1
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365/observability/core/inference_operation_type.py +2 -1
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365/observability/core/inference_scope.py +9 -0
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365/observability/core/invoke_agent_details.py +2 -1
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365/observability/core/middleware/__init__.py +2 -1
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365/observability/core/middleware/baggage_builder.py +13 -18
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365/observability/core/opentelemetry_scope.py +2 -1
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365/observability/core/request.py +2 -1
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365/observability/core/source_metadata.py +2 -1
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365/observability/core/tenant_details.py +2 -1
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365/observability/core/tool_call_details.py +2 -1
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365/observability/core/tool_type.py +2 -1
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365/observability/core/trace_processor/__init__.py +2 -1
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365/observability/core/trace_processor/span_processor.py +3 -2
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365/observability/core/trace_processor/util.py +3 -1
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365_observability_core.egg-info/PKG-INFO +3 -1
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365_observability_core.egg-info/SOURCES.txt +4 -1
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365_observability_core.egg-info/top_level.txt +1 -0
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/pyproject.toml +4 -0
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/setup.py +1 -1
- microsoft_agents_a365_observability_core-0.2.0.dev5/microsoft_agents_a365/observability/core/middleware/turn_context_baggage.py +0 -192
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/README.md +0 -0
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365/observability/core/agent_details.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365/observability/core/exporters/agent365_exporter_options.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365/observability/core/invoke_agent_scope.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365/observability/core/models/__init__.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365/observability/core/models/agent_type.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365/observability/core/models/caller_details.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365/observability/core/models/operation_source.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365/observability/core/utils.py +0 -0
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365_observability_core.egg-info/dependency_links.txt +0 -0
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-0.2.1.dev0}/microsoft_agents_a365_observability_core.egg-info/requires.txt +0 -0
- {microsoft_agents_a365_observability_core-0.2.0.dev5 → microsoft_agents_a365_observability_core-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-core
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1.dev0
|
|
4
4
|
Summary: Telemetry, tracing, and monitoring components for AI agents
|
|
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: opentelemetry-api>=1.36.0
|
|
24
25
|
Requires-Dist: opentelemetry-sdk>=1.36.0
|
|
25
26
|
Requires-Dist: opentelemetry-exporter-otlp>=1.36.0
|
|
@@ -41,6 +42,7 @@ Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
|
41
42
|
Provides-Extra: test
|
|
42
43
|
Requires-Dist: pytest>=7.0.0; extra == "test"
|
|
43
44
|
Requires-Dist: pytest-asyncio>=0.21.0; extra == "test"
|
|
45
|
+
Dynamic: license-file
|
|
44
46
|
|
|
45
47
|
# microsoft-agents-a365-observability-core
|
|
46
48
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
# Copyright (c) Microsoft
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
2
3
|
|
|
3
4
|
import logging
|
|
4
5
|
import threading
|
|
@@ -10,7 +11,7 @@ from opentelemetry.sdk.resources import SERVICE_NAME, SERVICE_NAMESPACE, Resourc
|
|
|
10
11
|
from opentelemetry.sdk.trace import TracerProvider
|
|
11
12
|
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
|
|
12
13
|
|
|
13
|
-
from .exporters.agent365_exporter import
|
|
14
|
+
from .exporters.agent365_exporter import _Agent365Exporter
|
|
14
15
|
from .exporters.agent365_exporter_options import Agent365ExporterOptions
|
|
15
16
|
from .exporters.utils import is_agent365_exporter_enabled
|
|
16
17
|
from .trace_processor.span_processor import SpanProcessor
|
|
@@ -53,6 +54,7 @@ class TelemetryManager:
|
|
|
53
54
|
token_resolver: Callable[[str, str], str | None] | None = None,
|
|
54
55
|
cluster_category: str = "prod",
|
|
55
56
|
exporter_options: Optional[Agent365ExporterOptions] = None,
|
|
57
|
+
suppress_invoke_agent_input: bool = False,
|
|
56
58
|
**kwargs: Any,
|
|
57
59
|
) -> bool:
|
|
58
60
|
"""
|
|
@@ -67,6 +69,7 @@ class TelemetryManager:
|
|
|
67
69
|
Use exporter_options instead.
|
|
68
70
|
:param exporter_options: Agent365ExporterOptions instance for configuring the exporter.
|
|
69
71
|
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.
|
|
72
|
+
:param suppress_invoke_agent_input: If True, suppress input messages for spans that are children of InvokeAgent spans.
|
|
70
73
|
:return: True if configuration succeeded, False otherwise.
|
|
71
74
|
"""
|
|
72
75
|
try:
|
|
@@ -78,6 +81,7 @@ class TelemetryManager:
|
|
|
78
81
|
token_resolver,
|
|
79
82
|
cluster_category,
|
|
80
83
|
exporter_options,
|
|
84
|
+
suppress_invoke_agent_input,
|
|
81
85
|
**kwargs,
|
|
82
86
|
)
|
|
83
87
|
except Exception as e:
|
|
@@ -92,10 +96,18 @@ class TelemetryManager:
|
|
|
92
96
|
token_resolver: Callable[[str, str], str | None] | None = None,
|
|
93
97
|
cluster_category: str = "prod",
|
|
94
98
|
exporter_options: Optional[Agent365ExporterOptions] = None,
|
|
99
|
+
suppress_invoke_agent_input: bool = False,
|
|
95
100
|
**kwargs: Any,
|
|
96
101
|
) -> bool:
|
|
97
102
|
"""Internal configuration method - not thread-safe, must be called with lock."""
|
|
98
103
|
|
|
104
|
+
# Check if a365 observability is already configured
|
|
105
|
+
if self._tracer_provider is not None:
|
|
106
|
+
self._logger.warning(
|
|
107
|
+
"a365 observability already configured. Ignoring repeated configure() call."
|
|
108
|
+
)
|
|
109
|
+
return True
|
|
110
|
+
|
|
99
111
|
# Create resource with service information
|
|
100
112
|
resource = Resource.create(
|
|
101
113
|
{
|
|
@@ -104,23 +116,24 @@ class TelemetryManager:
|
|
|
104
116
|
}
|
|
105
117
|
)
|
|
106
118
|
|
|
107
|
-
#
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
119
|
+
# Check if there's an existing TracerProvider (from app's OTEL setup)
|
|
120
|
+
tracer_provider = trace.get_tracer_provider()
|
|
121
|
+
|
|
122
|
+
# Determine if we should use existing provider or create new one
|
|
123
|
+
# Check if it's a real TracerProvider with a resource (not a proxy/no-op)
|
|
124
|
+
if getattr(tracer_provider, "resource", None):
|
|
125
|
+
# Use existing provider from application's OTEL setup
|
|
126
|
+
self._logger.info(
|
|
127
|
+
"Detected existing TracerProvider with resource. "
|
|
128
|
+
"Adding a365 observability processors to it."
|
|
129
|
+
)
|
|
130
|
+
else:
|
|
131
|
+
# Create new TracerProvider with our resource
|
|
132
|
+
self._logger.info("Creating new TracerProvider for a365 observability.")
|
|
133
|
+
tracer_provider = TracerProvider(resource=resource)
|
|
134
|
+
trace.set_tracer_provider(tracer_provider)
|
|
135
|
+
|
|
136
|
+
# Store reference
|
|
124
137
|
self._tracer_provider = tracer_provider
|
|
125
138
|
|
|
126
139
|
# Use exporter_options if provided, otherwise create default options with legacy parameters
|
|
@@ -139,10 +152,11 @@ class TelemetryManager:
|
|
|
139
152
|
}
|
|
140
153
|
|
|
141
154
|
if is_agent365_exporter_enabled() and exporter_options.token_resolver is not None:
|
|
142
|
-
exporter =
|
|
155
|
+
exporter = _Agent365Exporter(
|
|
143
156
|
token_resolver=exporter_options.token_resolver,
|
|
144
157
|
cluster_category=exporter_options.cluster_category,
|
|
145
158
|
use_s2s_endpoint=exporter_options.use_s2s_endpoint,
|
|
159
|
+
suppress_invoke_agent_input=suppress_invoke_agent_input,
|
|
146
160
|
)
|
|
147
161
|
else:
|
|
148
162
|
exporter = ConsoleSpanExporter()
|
|
@@ -5,6 +5,8 @@ from .agent_details import AgentDetails
|
|
|
5
5
|
from .constants import (
|
|
6
6
|
EXECUTE_TOOL_OPERATION_NAME,
|
|
7
7
|
GEN_AI_EVENT_CONTENT,
|
|
8
|
+
GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY,
|
|
9
|
+
GEN_AI_EXECUTION_SOURCE_NAME_KEY,
|
|
8
10
|
GEN_AI_TOOL_ARGS_KEY,
|
|
9
11
|
GEN_AI_TOOL_CALL_ID_KEY,
|
|
10
12
|
GEN_AI_TOOL_DESCRIPTION_KEY,
|
|
@@ -14,6 +16,7 @@ from .constants import (
|
|
|
14
16
|
SERVER_PORT_KEY,
|
|
15
17
|
)
|
|
16
18
|
from .opentelemetry_scope import OpenTelemetryScope
|
|
19
|
+
from .request import Request
|
|
17
20
|
from .tenant_details import TenantDetails
|
|
18
21
|
from .tool_call_details import ToolCallDetails
|
|
19
22
|
|
|
@@ -26,6 +29,7 @@ class ExecuteToolScope(OpenTelemetryScope):
|
|
|
26
29
|
details: ToolCallDetails,
|
|
27
30
|
agent_details: AgentDetails,
|
|
28
31
|
tenant_details: TenantDetails,
|
|
32
|
+
request: Request | None = None,
|
|
29
33
|
) -> "ExecuteToolScope":
|
|
30
34
|
"""Creates and starts a new scope for tool execution tracing.
|
|
31
35
|
|
|
@@ -33,17 +37,19 @@ class ExecuteToolScope(OpenTelemetryScope):
|
|
|
33
37
|
details: The details of the tool call
|
|
34
38
|
agent_details: The details of the agent making the call
|
|
35
39
|
tenant_details: The details of the tenant
|
|
40
|
+
request: Optional request details for additional context
|
|
36
41
|
|
|
37
42
|
Returns:
|
|
38
43
|
A new ExecuteToolScope instance
|
|
39
44
|
"""
|
|
40
|
-
return ExecuteToolScope(details, agent_details, tenant_details)
|
|
45
|
+
return ExecuteToolScope(details, agent_details, tenant_details, request)
|
|
41
46
|
|
|
42
47
|
def __init__(
|
|
43
48
|
self,
|
|
44
49
|
details: ToolCallDetails,
|
|
45
50
|
agent_details: AgentDetails,
|
|
46
51
|
tenant_details: TenantDetails,
|
|
52
|
+
request: Request | None = None,
|
|
47
53
|
):
|
|
48
54
|
"""Initialize the tool execution scope.
|
|
49
55
|
|
|
@@ -51,6 +57,7 @@ class ExecuteToolScope(OpenTelemetryScope):
|
|
|
51
57
|
details: The details of the tool call
|
|
52
58
|
agent_details: The details of the agent making the call
|
|
53
59
|
tenant_details: The details of the tenant
|
|
60
|
+
request: Optional request details for additional context
|
|
54
61
|
"""
|
|
55
62
|
super().__init__(
|
|
56
63
|
kind="Internal",
|
|
@@ -79,6 +86,13 @@ class ExecuteToolScope(OpenTelemetryScope):
|
|
|
79
86
|
if endpoint.port and endpoint.port != 443:
|
|
80
87
|
self.set_tag_maybe(SERVER_PORT_KEY, endpoint.port)
|
|
81
88
|
|
|
89
|
+
# Set request metadata if provided
|
|
90
|
+
if request and request.source_metadata:
|
|
91
|
+
self.set_tag_maybe(GEN_AI_EXECUTION_SOURCE_NAME_KEY, request.source_metadata.name)
|
|
92
|
+
self.set_tag_maybe(
|
|
93
|
+
GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY, request.source_metadata.description
|
|
94
|
+
)
|
|
95
|
+
|
|
82
96
|
def record_response(self, response: str) -> None:
|
|
83
97
|
"""Records response information for telemetry tracking.
|
|
84
98
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
from .agent365_exporter_options import Agent365ExporterOptions
|
|
5
|
+
|
|
6
|
+
# Agent365Exporter is not exported intentionally.
|
|
7
|
+
# It should only be used internally by the observability core module.
|
|
8
|
+
__all__ = ["Agent365ExporterOptions"]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
# Copyright (c) Microsoft
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
2
3
|
|
|
3
4
|
# pip install opentelemetry-sdk opentelemetry-api requests
|
|
4
5
|
|
|
@@ -9,7 +10,8 @@ import logging
|
|
|
9
10
|
import threading
|
|
10
11
|
import time
|
|
11
12
|
from collections.abc import Callable, Sequence
|
|
12
|
-
from typing import Any
|
|
13
|
+
from typing import Any, final
|
|
14
|
+
from urllib.parse import urlparse
|
|
13
15
|
|
|
14
16
|
import requests
|
|
15
17
|
from microsoft_agents_a365.runtime.power_platform_api_discovery import PowerPlatformApiDiscovery
|
|
@@ -17,7 +19,13 @@ from opentelemetry.sdk.trace import ReadableSpan
|
|
|
17
19
|
from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
|
|
18
20
|
from opentelemetry.trace import StatusCode
|
|
19
21
|
|
|
22
|
+
from ..constants import (
|
|
23
|
+
GEN_AI_INPUT_MESSAGES_KEY,
|
|
24
|
+
GEN_AI_OPERATION_NAME_KEY,
|
|
25
|
+
INVOKE_AGENT_OPERATION_NAME,
|
|
26
|
+
)
|
|
20
27
|
from .utils import (
|
|
28
|
+
get_validated_domain_override,
|
|
21
29
|
hex_span_id,
|
|
22
30
|
hex_trace_id,
|
|
23
31
|
kind_name,
|
|
@@ -36,7 +44,8 @@ DEFAULT_MAX_RETRIES = 3
|
|
|
36
44
|
logger = logging.getLogger(__name__)
|
|
37
45
|
|
|
38
46
|
|
|
39
|
-
|
|
47
|
+
@final
|
|
48
|
+
class _Agent365Exporter(SpanExporter):
|
|
40
49
|
"""
|
|
41
50
|
Agent 365 span exporter for Agent 365:
|
|
42
51
|
* Partitions spans by (tenantId, agentId)
|
|
@@ -50,6 +59,7 @@ class Agent365Exporter(SpanExporter):
|
|
|
50
59
|
token_resolver: Callable[[str, str], str | None],
|
|
51
60
|
cluster_category: str = "prod",
|
|
52
61
|
use_s2s_endpoint: bool = False,
|
|
62
|
+
suppress_invoke_agent_input: bool = False,
|
|
53
63
|
):
|
|
54
64
|
if token_resolver is None:
|
|
55
65
|
raise ValueError("token_resolver must be provided.")
|
|
@@ -59,6 +69,9 @@ class Agent365Exporter(SpanExporter):
|
|
|
59
69
|
self._token_resolver = token_resolver
|
|
60
70
|
self._cluster_category = cluster_category
|
|
61
71
|
self._use_s2s_endpoint = use_s2s_endpoint
|
|
72
|
+
self._suppress_invoke_agent_input = suppress_invoke_agent_input
|
|
73
|
+
# Read domain override once at initialization
|
|
74
|
+
self._domain_override = get_validated_domain_override()
|
|
62
75
|
|
|
63
76
|
# ------------- SpanExporter API -----------------
|
|
64
77
|
|
|
@@ -85,14 +98,29 @@ class Agent365Exporter(SpanExporter):
|
|
|
85
98
|
body = json.dumps(payload, separators=(",", ":"), ensure_ascii=False)
|
|
86
99
|
|
|
87
100
|
# Resolve endpoint + token
|
|
88
|
-
|
|
89
|
-
|
|
101
|
+
if self._domain_override:
|
|
102
|
+
endpoint = self._domain_override
|
|
103
|
+
else:
|
|
104
|
+
discovery = PowerPlatformApiDiscovery(self._cluster_category)
|
|
105
|
+
endpoint = discovery.get_tenant_island_cluster_endpoint(tenant_id)
|
|
106
|
+
|
|
90
107
|
endpoint_path = (
|
|
91
108
|
f"/maven/agent365/service/agents/{agent_id}/traces"
|
|
92
109
|
if self._use_s2s_endpoint
|
|
93
110
|
else f"/maven/agent365/agents/{agent_id}/traces"
|
|
94
111
|
)
|
|
95
|
-
|
|
112
|
+
|
|
113
|
+
# Construct URL - if endpoint has a scheme (http:// or https://), use it as-is
|
|
114
|
+
# Otherwise, prepend https://
|
|
115
|
+
# Note: Check for "://" to distinguish between real protocols and domain:port format
|
|
116
|
+
# (urlparse treats "example.com:8080" as having scheme="example.com")
|
|
117
|
+
parsed = urlparse(endpoint)
|
|
118
|
+
if parsed.scheme and "://" in endpoint:
|
|
119
|
+
# Endpoint is a full URL, append path
|
|
120
|
+
url = f"{endpoint}{endpoint_path}?api-version=1"
|
|
121
|
+
else:
|
|
122
|
+
# Endpoint is just a domain (possibly with port), prepend https://
|
|
123
|
+
url = f"https://{endpoint}{endpoint_path}?api-version=1"
|
|
96
124
|
|
|
97
125
|
# Debug: Log endpoint being used
|
|
98
126
|
logger.info(
|
|
@@ -141,6 +169,8 @@ class Agent365Exporter(SpanExporter):
|
|
|
141
169
|
def force_flush(self, timeout_millis: int = 30000) -> bool:
|
|
142
170
|
return True
|
|
143
171
|
|
|
172
|
+
# ------------- Helper methods -------------------
|
|
173
|
+
|
|
144
174
|
# ------------- HTTP helper ----------------------
|
|
145
175
|
|
|
146
176
|
@staticmethod
|
|
@@ -257,6 +287,20 @@ class Agent365Exporter(SpanExporter):
|
|
|
257
287
|
|
|
258
288
|
# attributes
|
|
259
289
|
attrs = dict(sp.attributes or {})
|
|
290
|
+
|
|
291
|
+
# Suppress input messages if configured and current span is an InvokeAgent span
|
|
292
|
+
if self._suppress_invoke_agent_input:
|
|
293
|
+
# Check if current span is an InvokeAgent span by:
|
|
294
|
+
# 1. Span name starts with "invoke_agent"
|
|
295
|
+
# 2. Has attribute gen_ai.operation.name set to INVOKE_AGENT_OPERATION_NAME
|
|
296
|
+
operation_name = attrs.get(GEN_AI_OPERATION_NAME_KEY)
|
|
297
|
+
if (
|
|
298
|
+
sp.name.startswith(INVOKE_AGENT_OPERATION_NAME)
|
|
299
|
+
and operation_name == INVOKE_AGENT_OPERATION_NAME
|
|
300
|
+
):
|
|
301
|
+
# Remove input messages attribute
|
|
302
|
+
attrs.pop(GEN_AI_INPUT_MESSAGES_KEY, None)
|
|
303
|
+
|
|
260
304
|
# events
|
|
261
305
|
events = []
|
|
262
306
|
for ev in sp.events:
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
# Copyright (c) Microsoft
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
2
3
|
|
|
3
4
|
import json
|
|
4
5
|
import logging
|
|
5
6
|
import os
|
|
6
7
|
from collections.abc import Sequence
|
|
7
8
|
from typing import Any
|
|
9
|
+
from urllib.parse import urlparse
|
|
8
10
|
|
|
9
11
|
from opentelemetry.sdk.trace import ReadableSpan
|
|
10
12
|
from opentelemetry.trace import SpanKind, StatusCode
|
|
@@ -142,6 +144,59 @@ def partition_by_identity(
|
|
|
142
144
|
return groups
|
|
143
145
|
|
|
144
146
|
|
|
147
|
+
def get_validated_domain_override() -> str | None:
|
|
148
|
+
"""
|
|
149
|
+
Get and validate the domain override from environment variable.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
The validated domain override, or None if not set or invalid.
|
|
153
|
+
"""
|
|
154
|
+
domain_override = os.getenv("A365_OBSERVABILITY_DOMAIN_OVERRIDE", "").strip()
|
|
155
|
+
if not domain_override:
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
# Validate that it's a valid URL
|
|
159
|
+
try:
|
|
160
|
+
parsed = urlparse(domain_override)
|
|
161
|
+
|
|
162
|
+
# If scheme is present and looks like a protocol (contains //)
|
|
163
|
+
# Note: We check for "://" because urlparse treats "example.com:8080" as having
|
|
164
|
+
# scheme="example.com", but this is actually a domain with port, not a protocol.
|
|
165
|
+
if parsed.scheme and "://" in domain_override:
|
|
166
|
+
# Validate it's http or https
|
|
167
|
+
if parsed.scheme not in ("http", "https"):
|
|
168
|
+
logger.warning(
|
|
169
|
+
f"Invalid domain override '{domain_override}': "
|
|
170
|
+
f"scheme must be http or https, got '{parsed.scheme}'"
|
|
171
|
+
)
|
|
172
|
+
return None
|
|
173
|
+
# Must have a netloc (hostname) when scheme is present
|
|
174
|
+
if not parsed.netloc:
|
|
175
|
+
logger.warning(f"Invalid domain override '{domain_override}': missing hostname")
|
|
176
|
+
return None
|
|
177
|
+
else:
|
|
178
|
+
# If no scheme with ://, it should be a domain with optional port (no path)
|
|
179
|
+
# Note: domain can contain : for port (e.g., example.com:8080)
|
|
180
|
+
# Reject malformed URLs like "http:8080" that look like protocols but aren't
|
|
181
|
+
if domain_override.startswith(("http:", "https:")) and "://" not in domain_override:
|
|
182
|
+
logger.warning(
|
|
183
|
+
f"Invalid domain override '{domain_override}': "
|
|
184
|
+
"malformed URL - protocol requires '://'"
|
|
185
|
+
)
|
|
186
|
+
return None
|
|
187
|
+
if "/" in domain_override:
|
|
188
|
+
logger.warning(
|
|
189
|
+
f"Invalid domain override '{domain_override}': "
|
|
190
|
+
"domain without protocol should not contain path separators (/)"
|
|
191
|
+
)
|
|
192
|
+
return None
|
|
193
|
+
except Exception as e:
|
|
194
|
+
logger.warning(f"Invalid domain override '{domain_override}': {e}")
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
return domain_override
|
|
198
|
+
|
|
199
|
+
|
|
145
200
|
def is_agent365_exporter_enabled() -> bool:
|
|
146
201
|
"""Check if Agent 365 exporter is enabled."""
|
|
147
202
|
# Check environment variable
|
|
@@ -5,6 +5,8 @@ from typing import List
|
|
|
5
5
|
|
|
6
6
|
from .agent_details import AgentDetails
|
|
7
7
|
from .constants import (
|
|
8
|
+
GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY,
|
|
9
|
+
GEN_AI_EXECUTION_SOURCE_NAME_KEY,
|
|
8
10
|
GEN_AI_INPUT_MESSAGES_KEY,
|
|
9
11
|
GEN_AI_OPERATION_NAME_KEY,
|
|
10
12
|
GEN_AI_OUTPUT_MESSAGES_KEY,
|
|
@@ -90,6 +92,13 @@ class InferenceScope(OpenTelemetryScope):
|
|
|
90
92
|
)
|
|
91
93
|
self.set_tag_maybe(GEN_AI_RESPONSE_ID_KEY, details.responseId)
|
|
92
94
|
|
|
95
|
+
# Set request metadata if provided
|
|
96
|
+
if request and request.source_metadata:
|
|
97
|
+
self.set_tag_maybe(GEN_AI_EXECUTION_SOURCE_NAME_KEY, request.source_metadata.name)
|
|
98
|
+
self.set_tag_maybe(
|
|
99
|
+
GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY, request.source_metadata.description
|
|
100
|
+
)
|
|
101
|
+
|
|
93
102
|
def record_input_messages(self, messages: List[str]) -> None:
|
|
94
103
|
"""Records the input messages for telemetry tracking.
|
|
95
104
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
# Copyright (c) Microsoft
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
2
3
|
|
|
3
4
|
# Per request baggage builder for OpenTelemetry context propagation.
|
|
4
5
|
|
|
@@ -31,7 +32,6 @@ from ..constants import (
|
|
|
31
32
|
)
|
|
32
33
|
from ..models.operation_source import OperationSource
|
|
33
34
|
from ..utils import deprecated, validate_and_normalize_ip
|
|
34
|
-
from .turn_context_baggage import from_turn_context
|
|
35
35
|
|
|
36
36
|
logger = logging.getLogger(__name__)
|
|
37
37
|
|
|
@@ -43,14 +43,17 @@ class BaggageBuilder:
|
|
|
43
43
|
propagated in the OpenTelemetry context.
|
|
44
44
|
|
|
45
45
|
Example:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
.. code-block:: python
|
|
47
|
+
|
|
48
|
+
builder = (BaggageBuilder()
|
|
49
|
+
.tenant_id("tenant-123")
|
|
50
|
+
.agent_id("agent-456")
|
|
51
|
+
.correlation_id("corr-789"))
|
|
52
|
+
|
|
53
|
+
with builder.build():
|
|
54
|
+
# Baggage is set in this context
|
|
55
|
+
pass
|
|
56
|
+
# Baggage is restored after exiting the context
|
|
54
57
|
"""
|
|
55
58
|
|
|
56
59
|
def __init__(self):
|
|
@@ -232,14 +235,6 @@ class BaggageBuilder:
|
|
|
232
235
|
self._set(GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY, value)
|
|
233
236
|
return self
|
|
234
237
|
|
|
235
|
-
def from_turn_context(self, turn_context: Any) -> "BaggageBuilder":
|
|
236
|
-
"""
|
|
237
|
-
Populate baggage from a turn_context (duck-typed).
|
|
238
|
-
Delegates to baggage_turn_context.from_turn_context.
|
|
239
|
-
"""
|
|
240
|
-
|
|
241
|
-
return self.set_pairs(from_turn_context(turn_context))
|
|
242
|
-
|
|
243
238
|
def set_pairs(self, pairs: Any) -> "BaggageBuilder":
|
|
244
239
|
"""
|
|
245
240
|
Accept dict or iterable of (k,v).
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
2
3
|
|
|
3
|
-
Span processor for copying OpenTelemetry baggage entries onto spans.
|
|
4
|
+
"""Span processor for copying OpenTelemetry baggage entries onto spans.
|
|
4
5
|
|
|
5
6
|
This implementation assumes `opentelemetry.baggage.get_all` is available with the
|
|
6
7
|
signature `get_all(context: Context | None) -> Mapping[str, object]`.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
# Copyright (c) Microsoft
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
2
3
|
|
|
3
4
|
from .. import constants as consts
|
|
4
5
|
|
|
@@ -23,6 +24,7 @@ COMMON_ATTRIBUTES = [
|
|
|
23
24
|
consts.SESSION_ID_KEY,
|
|
24
25
|
consts.SESSION_DESCRIPTION_KEY,
|
|
25
26
|
consts.HIRING_MANAGER_ID_KEY,
|
|
27
|
+
consts.GEN_AI_CALLER_CLIENT_IP_KEY, # gen_ai.caller.client.ip
|
|
26
28
|
# Execution context
|
|
27
29
|
consts.GEN_AI_EXECUTION_SOURCE_NAME_KEY, # gen_ai.channel.name
|
|
28
30
|
consts.GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY, # gen_ai.channel.link
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: microsoft-agents-a365-observability-core
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1.dev0
|
|
4
4
|
Summary: Telemetry, tracing, and monitoring components for AI agents
|
|
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: opentelemetry-api>=1.36.0
|
|
24
25
|
Requires-Dist: opentelemetry-sdk>=1.36.0
|
|
25
26
|
Requires-Dist: opentelemetry-exporter-otlp>=1.36.0
|
|
@@ -41,6 +42,7 @@ Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
|
41
42
|
Provides-Extra: test
|
|
42
43
|
Requires-Dist: pytest>=7.0.0; extra == "test"
|
|
43
44
|
Requires-Dist: pytest-asyncio>=0.21.0; extra == "test"
|
|
45
|
+
Dynamic: license-file
|
|
44
46
|
|
|
45
47
|
# microsoft-agents-a365-observability-core
|
|
46
48
|
|
|
@@ -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/core/__init__.py
|
|
5
8
|
microsoft_agents_a365/observability/core/agent_details.py
|
|
6
9
|
microsoft_agents_a365/observability/core/config.py
|
|
@@ -19,12 +22,12 @@ microsoft_agents_a365/observability/core/tenant_details.py
|
|
|
19
22
|
microsoft_agents_a365/observability/core/tool_call_details.py
|
|
20
23
|
microsoft_agents_a365/observability/core/tool_type.py
|
|
21
24
|
microsoft_agents_a365/observability/core/utils.py
|
|
25
|
+
microsoft_agents_a365/observability/core/exporters/__init__.py
|
|
22
26
|
microsoft_agents_a365/observability/core/exporters/agent365_exporter.py
|
|
23
27
|
microsoft_agents_a365/observability/core/exporters/agent365_exporter_options.py
|
|
24
28
|
microsoft_agents_a365/observability/core/exporters/utils.py
|
|
25
29
|
microsoft_agents_a365/observability/core/middleware/__init__.py
|
|
26
30
|
microsoft_agents_a365/observability/core/middleware/baggage_builder.py
|
|
27
|
-
microsoft_agents_a365/observability/core/middleware/turn_context_baggage.py
|
|
28
31
|
microsoft_agents_a365/observability/core/models/__init__.py
|
|
29
32
|
microsoft_agents_a365/observability/core/models/agent_type.py
|
|
30
33
|
microsoft_agents_a365/observability/core/models/caller_details.py
|
|
@@ -80,6 +80,10 @@ target-version = ['py311']
|
|
|
80
80
|
line-length = 100
|
|
81
81
|
target-version = "py311"
|
|
82
82
|
|
|
83
|
+
[tool.ruff.lint.flake8-copyright]
|
|
84
|
+
notice-rgx = "# Copyright \\(c\\) Microsoft Corporation\\.\\r?\\n# Licensed under the MIT License\\."
|
|
85
|
+
min-file-size = 1
|
|
86
|
+
|
|
83
87
|
[tool.mypy]
|
|
84
88
|
python_version = "3.11"
|
|
85
89
|
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)
|
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
from typing import Any, 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_NAME_KEY,
|
|
21
|
-
GEN_AI_EXECUTION_TYPE_KEY,
|
|
22
|
-
TENANT_ID_KEY,
|
|
23
|
-
)
|
|
24
|
-
from ..execution_type import ExecutionType
|
|
25
|
-
|
|
26
|
-
AGENT_ROLE = "agenticUser"
|
|
27
|
-
CHANNEL_ID_AGENTS = "agents"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def _safe_get(obj: Any, *names: str) -> Any:
|
|
31
|
-
"""Attempt multiple attribute/dict keys; return first non-None."""
|
|
32
|
-
for n in names:
|
|
33
|
-
if obj is None:
|
|
34
|
-
continue
|
|
35
|
-
# dict-like
|
|
36
|
-
if isinstance(obj, Mapping) and n in obj:
|
|
37
|
-
return obj[n]
|
|
38
|
-
# attribute-like (support both camelCase and snake_case lookups)
|
|
39
|
-
if hasattr(obj, n):
|
|
40
|
-
return getattr(obj, n)
|
|
41
|
-
return None
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def _extract_channel_data(activity: Any) -> Mapping[str, Any] | None:
|
|
45
|
-
cd = _safe_get(activity, "channel_data")
|
|
46
|
-
if cd is None:
|
|
47
|
-
return None
|
|
48
|
-
if isinstance(cd, Mapping):
|
|
49
|
-
return cd
|
|
50
|
-
if isinstance(cd, str):
|
|
51
|
-
try:
|
|
52
|
-
return json.loads(cd)
|
|
53
|
-
except Exception:
|
|
54
|
-
return None
|
|
55
|
-
return None
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def _iter_caller_pairs(activity: Any) -> Iterator[tuple[str, Any]]:
|
|
59
|
-
frm = _safe_get(activity, "from")
|
|
60
|
-
if not frm:
|
|
61
|
-
return
|
|
62
|
-
yield GEN_AI_CALLER_ID_KEY, _safe_get(frm, "id")
|
|
63
|
-
name = _safe_get(frm, "name")
|
|
64
|
-
yield GEN_AI_CALLER_NAME_KEY, name
|
|
65
|
-
# Reuse 'name' as UPN if no separate field
|
|
66
|
-
upn = _safe_get(frm, "upn") or name
|
|
67
|
-
yield GEN_AI_CALLER_UPN_KEY, upn
|
|
68
|
-
user_id = _safe_get(frm, "agentic_user_id", "aad_object_id")
|
|
69
|
-
yield GEN_AI_CALLER_USER_ID_KEY, user_id
|
|
70
|
-
tenant_id = _safe_get(frm, "tenant_id")
|
|
71
|
-
yield GEN_AI_CALLER_TENANT_ID_KEY, tenant_id
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def _is_agentic(entity: Any) -> bool:
|
|
75
|
-
return bool(
|
|
76
|
-
_safe_get(
|
|
77
|
-
entity,
|
|
78
|
-
"agentic_user_id",
|
|
79
|
-
)
|
|
80
|
-
or (
|
|
81
|
-
(role := _safe_get(entity, "role", "Role"))
|
|
82
|
-
and isinstance(role, str)
|
|
83
|
-
and role.lower() == AGENT_ROLE.lower()
|
|
84
|
-
)
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def _iter_execution_type_pair(activity: Any) -> Iterator[tuple[str, Any]]:
|
|
89
|
-
frm = _safe_get(activity, "from")
|
|
90
|
-
rec = _safe_get(activity, "recipient")
|
|
91
|
-
is_agentic_caller = _is_agentic(frm)
|
|
92
|
-
is_agentic_recipient = _is_agentic(rec)
|
|
93
|
-
exec_type = (
|
|
94
|
-
ExecutionType.AGENT_TO_AGENT.value
|
|
95
|
-
if (is_agentic_caller and is_agentic_recipient)
|
|
96
|
-
else ExecutionType.HUMAN_TO_AGENT.value
|
|
97
|
-
)
|
|
98
|
-
yield GEN_AI_EXECUTION_TYPE_KEY, exec_type
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def _iter_target_agent_pairs(activity: Any) -> Iterator[tuple[str, Any]]:
|
|
102
|
-
rec = _safe_get(activity, "recipient")
|
|
103
|
-
if not rec:
|
|
104
|
-
return
|
|
105
|
-
yield GEN_AI_AGENT_ID_KEY, _safe_get(rec, "agentic_app_id")
|
|
106
|
-
yield GEN_AI_AGENT_NAME_KEY, _safe_get(rec, "name")
|
|
107
|
-
auid = _safe_get(rec, "agentic_user_id", "aad_object_id")
|
|
108
|
-
yield GEN_AI_AGENT_AUID_KEY, auid
|
|
109
|
-
yield GEN_AI_AGENT_UPN_KEY, _safe_get(rec, "upn", "name")
|
|
110
|
-
yield (
|
|
111
|
-
GEN_AI_AGENT_DESCRIPTION_KEY,
|
|
112
|
-
_safe_get(rec, "role"),
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def _iter_tenant_id_pair(activity: Any) -> Iterator[tuple[str, Any]]:
|
|
117
|
-
rec = _safe_get(activity, "recipient")
|
|
118
|
-
tenant_id = _safe_get(rec, "tenant_id")
|
|
119
|
-
if not tenant_id:
|
|
120
|
-
cd_dict = _extract_channel_data(activity)
|
|
121
|
-
# channelData.tenant.id
|
|
122
|
-
try:
|
|
123
|
-
tenant_id = (
|
|
124
|
-
cd_dict
|
|
125
|
-
and isinstance(cd_dict.get("tenant"), Mapping)
|
|
126
|
-
and cd_dict["tenant"].get("id")
|
|
127
|
-
)
|
|
128
|
-
except Exception:
|
|
129
|
-
tenant_id = None
|
|
130
|
-
yield TENANT_ID_KEY, tenant_id
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def _iter_source_metadata_pairs(activity: Any) -> Iterator[tuple[str, Any]]:
|
|
134
|
-
"""
|
|
135
|
-
Generate source metadata pairs from activity, handling both string and ChannelId object cases.
|
|
136
|
-
|
|
137
|
-
:param activity: The activity object (Activity instance or dict)
|
|
138
|
-
:return: Iterator of (key, value) tuples for source metadata
|
|
139
|
-
"""
|
|
140
|
-
# Handle channel_id (can be string or ChannelId object)
|
|
141
|
-
channel_id = _safe_get(activity, "channel_id")
|
|
142
|
-
|
|
143
|
-
# Extract channel name from either string or ChannelId object
|
|
144
|
-
channel_name = None
|
|
145
|
-
sub_channel = None
|
|
146
|
-
|
|
147
|
-
if channel_id is not None:
|
|
148
|
-
if isinstance(channel_id, str):
|
|
149
|
-
# Direct string value
|
|
150
|
-
channel_name = channel_id
|
|
151
|
-
elif hasattr(channel_id, "channel"):
|
|
152
|
-
# ChannelId object
|
|
153
|
-
channel_name = channel_id.channel
|
|
154
|
-
sub_channel = getattr(channel_id, "sub_channel", None)
|
|
155
|
-
elif isinstance(channel_id, dict):
|
|
156
|
-
# Serialized ChannelId as dict
|
|
157
|
-
channel_name = channel_id.get("channel")
|
|
158
|
-
sub_channel = channel_id.get("sub_channel")
|
|
159
|
-
|
|
160
|
-
# Yield channel name as source name
|
|
161
|
-
yield GEN_AI_EXECUTION_SOURCE_NAME_KEY, channel_name
|
|
162
|
-
yield GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY, sub_channel
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
def _iter_conversation_pairs(activity: Any) -> Iterator[tuple[str, Any]]:
|
|
166
|
-
conv = _safe_get(activity, "conversation")
|
|
167
|
-
conversation_id = _safe_get(conv, "id")
|
|
168
|
-
|
|
169
|
-
item_link = _safe_get(activity, "service_url")
|
|
170
|
-
|
|
171
|
-
yield GEN_AI_CONVERSATION_ID_KEY, conversation_id
|
|
172
|
-
yield GEN_AI_CONVERSATION_ITEM_LINK_KEY, item_link
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
def _iter_all_pairs(turn_context: Any) -> Iterator[tuple[str, Any]]:
|
|
176
|
-
activity = _safe_get(
|
|
177
|
-
turn_context,
|
|
178
|
-
"activity",
|
|
179
|
-
)
|
|
180
|
-
if not activity:
|
|
181
|
-
return
|
|
182
|
-
yield from _iter_caller_pairs(activity)
|
|
183
|
-
yield from _iter_execution_type_pair(activity)
|
|
184
|
-
yield from _iter_target_agent_pairs(activity)
|
|
185
|
-
yield from _iter_tenant_id_pair(activity)
|
|
186
|
-
yield from _iter_source_metadata_pairs(activity)
|
|
187
|
-
yield from _iter_conversation_pairs(activity)
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
def from_turn_context(turn_context: Any) -> dict:
|
|
191
|
-
"""Populate builder with baggage values extracted from a turn context."""
|
|
192
|
-
return dict(_iter_all_pairs(turn_context))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|