microsoft-agents-a365-observability-hosting 0.2.1.dev0__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.
@@ -0,0 +1,6 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+
4
+ """
5
+ Microsoft Agent 365 Observability Hosting Library.
6
+ """
@@ -0,0 +1,2 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
@@ -0,0 +1,45 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+
4
+ from __future__ import annotations
5
+
6
+ from collections.abc import Iterator
7
+ from typing import Any
8
+
9
+ from microsoft_agents.hosting.core.turn_context import TurnContext
10
+ from microsoft_agents_a365.observability.core.middleware.baggage_builder import BaggageBuilder
11
+
12
+ from .utils import (
13
+ get_caller_pairs,
14
+ get_conversation_pairs,
15
+ get_execution_type_pair,
16
+ get_source_metadata_pairs,
17
+ get_target_agent_pairs,
18
+ get_tenant_id_pair,
19
+ )
20
+
21
+
22
+ def _iter_all_pairs(turn_context: TurnContext) -> Iterator[tuple[str, Any]]:
23
+ activity = turn_context.activity
24
+ if not activity:
25
+ return
26
+ yield from get_caller_pairs(activity)
27
+ yield from get_execution_type_pair(activity)
28
+ yield from get_target_agent_pairs(activity)
29
+ yield from get_tenant_id_pair(activity)
30
+ yield from get_source_metadata_pairs(activity)
31
+ yield from get_conversation_pairs(activity)
32
+
33
+
34
+ def populate(builder: BaggageBuilder, turn_context: TurnContext) -> BaggageBuilder:
35
+ """Populate BaggageBuilder with baggage values extracted from a turn context.
36
+
37
+ Args:
38
+ builder: The BaggageBuilder instance to populate
39
+ turn_context: The TurnContext containing activity information
40
+
41
+ Returns:
42
+ The updated BaggageBuilder instance (for method chaining)
43
+ """
44
+ builder.set_pairs(_iter_all_pairs(turn_context))
45
+ return builder
@@ -0,0 +1,48 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import TYPE_CHECKING
7
+
8
+ from microsoft_agents_a365.observability.core.invoke_agent_scope import InvokeAgentScope
9
+
10
+ from .utils import (
11
+ get_caller_pairs,
12
+ get_conversation_pairs,
13
+ get_execution_type_pair,
14
+ get_source_metadata_pairs,
15
+ get_target_agent_pairs,
16
+ get_tenant_id_pair,
17
+ )
18
+
19
+ if TYPE_CHECKING:
20
+ from microsoft_agents.hosting.core.turn_context import TurnContext
21
+
22
+
23
+ def populate(scope: InvokeAgentScope, turn_context: TurnContext) -> InvokeAgentScope:
24
+ """
25
+ Populate all supported InvokeAgentScope tags from the provided TurnContext.
26
+ :param scope: The InvokeAgentScope instance to populate.
27
+ :param turn_context: The TurnContext containing activity information.
28
+ :return: The updated InvokeAgentScope instance.
29
+ """
30
+ if not turn_context:
31
+ raise ValueError("turn_context is required")
32
+
33
+ if not turn_context.activity:
34
+ return scope
35
+
36
+ activity = turn_context.activity
37
+
38
+ scope.record_attributes(get_caller_pairs(activity))
39
+ scope.record_attributes(get_execution_type_pair(activity))
40
+ scope.record_attributes(get_target_agent_pairs(activity))
41
+ scope.record_attributes(get_tenant_id_pair(activity))
42
+ scope.record_attributes(get_source_metadata_pairs(activity))
43
+ scope.record_attributes(get_conversation_pairs(activity))
44
+
45
+ if activity.text:
46
+ scope.record_input_messages([activity.text])
47
+
48
+ return scope
@@ -0,0 +1,114 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+ from collections.abc import Iterator
4
+ from typing import Any
5
+
6
+ from microsoft_agents.activity import Activity
7
+ from microsoft_agents_a365.observability.core.constants import (
8
+ GEN_AI_AGENT_AUID_KEY,
9
+ GEN_AI_AGENT_DESCRIPTION_KEY,
10
+ GEN_AI_AGENT_ID_KEY,
11
+ GEN_AI_AGENT_NAME_KEY,
12
+ GEN_AI_AGENT_UPN_KEY,
13
+ GEN_AI_CALLER_ID_KEY,
14
+ GEN_AI_CALLER_NAME_KEY,
15
+ GEN_AI_CALLER_TENANT_ID_KEY,
16
+ GEN_AI_CALLER_UPN_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 microsoft_agents_a365.observability.core.execution_type import ExecutionType
25
+
26
+ AGENT_ROLE = "agenticUser"
27
+
28
+
29
+ def _is_agentic(entity: Any) -> bool:
30
+ if not entity:
31
+ return False
32
+ return bool(
33
+ entity.agentic_user_id
34
+ or ((role := entity.role) and isinstance(role, str) and role.lower() == AGENT_ROLE.lower())
35
+ )
36
+
37
+
38
+ def get_caller_pairs(activity: Activity) -> Iterator[tuple[str, Any]]:
39
+ frm = activity.from_property
40
+ if not frm:
41
+ return
42
+ yield GEN_AI_CALLER_ID_KEY, frm.aad_object_id
43
+ yield GEN_AI_CALLER_NAME_KEY, frm.name
44
+ yield GEN_AI_CALLER_UPN_KEY, frm.agentic_user_id
45
+ yield GEN_AI_CALLER_TENANT_ID_KEY, frm.tenant_id
46
+
47
+
48
+ def get_execution_type_pair(activity: Activity) -> Iterator[tuple[str, Any]]:
49
+ frm = activity.from_property
50
+ rec = activity.recipient
51
+ is_agentic_caller = _is_agentic(frm)
52
+ is_agentic_recipient = _is_agentic(rec)
53
+ exec_type = (
54
+ ExecutionType.AGENT_TO_AGENT.value
55
+ if (is_agentic_caller and is_agentic_recipient)
56
+ else ExecutionType.HUMAN_TO_AGENT.value
57
+ )
58
+ yield GEN_AI_EXECUTION_TYPE_KEY, exec_type
59
+
60
+
61
+ def get_target_agent_pairs(activity: Activity) -> Iterator[tuple[str, Any]]:
62
+ rec = activity.recipient
63
+ if not rec:
64
+ return
65
+ yield GEN_AI_AGENT_ID_KEY, rec.agentic_app_id
66
+ yield GEN_AI_AGENT_NAME_KEY, rec.name
67
+ yield GEN_AI_AGENT_AUID_KEY, rec.aad_object_id
68
+ yield GEN_AI_AGENT_UPN_KEY, rec.agentic_user_id
69
+ yield (
70
+ GEN_AI_AGENT_DESCRIPTION_KEY,
71
+ rec.role,
72
+ )
73
+
74
+
75
+ def get_tenant_id_pair(activity: Activity) -> Iterator[tuple[str, Any]]:
76
+ yield TENANT_ID_KEY, activity.recipient.tenant_id
77
+
78
+
79
+ def get_source_metadata_pairs(activity: Activity) -> Iterator[tuple[str, Any]]:
80
+ """
81
+ Generate source metadata pairs from activity, handling both string and ChannelId object cases.
82
+
83
+ :param activity: The activity object (Activity instance or dict)
84
+ :return: Iterator of (key, value) tuples for source metadata
85
+ """
86
+ # Handle channel_id (can be string or ChannelId object)
87
+ channel_id = activity.channel_id
88
+
89
+ # Extract channel name from either string or ChannelId object
90
+ channel_name = None
91
+ sub_channel = None
92
+
93
+ if channel_id is not None:
94
+ if isinstance(channel_id, str):
95
+ # Direct string value
96
+ channel_name = channel_id
97
+ elif hasattr(channel_id, "channel"):
98
+ # ChannelId object
99
+ channel_name = channel_id.channel
100
+ sub_channel = channel_id.sub_channel
101
+
102
+ # Yield channel name as source name
103
+ yield GEN_AI_EXECUTION_SOURCE_NAME_KEY, channel_name
104
+ yield GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY, sub_channel
105
+
106
+
107
+ def get_conversation_pairs(activity: Activity) -> Iterator[tuple[str, Any]]:
108
+ conv = activity.conversation
109
+ conversation_id = conv.id if conv else None
110
+
111
+ item_link = activity.service_url
112
+
113
+ yield GEN_AI_CONVERSATION_ID_KEY, conversation_id
114
+ yield GEN_AI_CONVERSATION_ITEM_LINK_KEY, item_link
@@ -0,0 +1,7 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+ """Token cache helpers for observability."""
4
+
5
+ from .agent_token_cache import AgenticTokenCache, AgenticTokenStruct
6
+
7
+ __all__ = ["AgenticTokenCache", "AgenticTokenStruct"]
@@ -0,0 +1,137 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+
4
+ """
5
+ Token cache for observability tokens per (agentId, tenantId).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from dataclasses import dataclass
12
+ from threading import Lock
13
+
14
+ from microsoft_agents.hosting.core.app.oauth.authorization import Authorization
15
+ from microsoft_agents.hosting.core.turn_context import TurnContext
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ @dataclass
21
+ class AgenticTokenStruct:
22
+ """Structure containing the token generation components."""
23
+
24
+ authorization: Authorization
25
+ """The user authorization object for token exchange."""
26
+
27
+ turn_context: TurnContext
28
+ """The turn context for the current conversation."""
29
+
30
+ auth_handler_name: str | None = "AGENTIC"
31
+ """The name of the authentication handler."""
32
+
33
+
34
+ class AgenticTokenCache:
35
+ """
36
+ Caches observability tokens per (agentId, tenantId) using the provided
37
+ UserAuthorization and TurnContext.
38
+ """
39
+
40
+ @dataclass
41
+ class _Entry:
42
+ """Internal entry structure for cache storage."""
43
+
44
+ agentic_token_struct: AgenticTokenStruct
45
+ """The token generation structure."""
46
+
47
+ scopes: list[str]
48
+ """The observability scopes for token requests."""
49
+
50
+ def __init__(self) -> None:
51
+ """Initialize the token cache."""
52
+ self._map: dict[str, AgenticTokenCache._Entry] = {}
53
+ self._lock = Lock()
54
+
55
+ def register_observability(
56
+ self,
57
+ agent_id: str,
58
+ tenant_id: str,
59
+ token_generator: AgenticTokenStruct,
60
+ observability_scopes: list[str],
61
+ ) -> None:
62
+ """
63
+ Register observability for the specified agent and tenant.
64
+
65
+ Args:
66
+ agent_id: The agent identifier.
67
+ tenant_id: The tenant identifier.
68
+ token_generator: The token generator structure.
69
+ observability_scopes: The observability scopes.
70
+
71
+ Raises:
72
+ ValueError: If agent_id or tenant_id is empty or None.
73
+ TypeError: If token_generator is None.
74
+ """
75
+ if not agent_id or not agent_id.strip():
76
+ raise ValueError("agent_id cannot be None or whitespace")
77
+
78
+ if not tenant_id or not tenant_id.strip():
79
+ raise ValueError("tenant_id cannot be None or whitespace")
80
+
81
+ if token_generator is None:
82
+ raise TypeError("token_generator cannot be None")
83
+
84
+ key = f"{agent_id}:{tenant_id}"
85
+
86
+ # First registration wins; subsequent calls ignored (idempotent)
87
+ with self._lock:
88
+ if key not in self._map:
89
+ self._map[key] = AgenticTokenCache._Entry(
90
+ agentic_token_struct=token_generator, scopes=observability_scopes
91
+ )
92
+ logger.debug(f"Registered observability for {key}")
93
+ else:
94
+ logger.debug(f"Observability already registered for {key}, ignoring")
95
+
96
+ async def get_observability_token(self, agent_id: str, tenant_id: str) -> str | None:
97
+ """
98
+ Get the observability token for the specified agent and tenant.
99
+
100
+ Args:
101
+ agent_id: The agent identifier.
102
+ tenant_id: The tenant identifier.
103
+
104
+ Returns:
105
+ The observability token if available; otherwise, None.
106
+ """
107
+ key = f"{agent_id}:{tenant_id}"
108
+
109
+ logger.debug(f"Cache lookup for {key}")
110
+
111
+ with self._lock:
112
+ entry = self._map.get(key)
113
+
114
+ if entry is None:
115
+ logger.debug(f"Cache miss for {key}")
116
+ return None
117
+
118
+ logger.debug(f"Cache hit for {key}, exchanging token")
119
+
120
+ try:
121
+ authorization = entry.agentic_token_struct.authorization
122
+ turn_context = entry.agentic_token_struct.turn_context
123
+ auth_handler_id = entry.agentic_token_struct.auth_handler_name
124
+
125
+ # Exchange the turn token for an observability token
126
+ token = await authorization.exchange_token(
127
+ context=turn_context,
128
+ scopes=entry.scopes,
129
+ auth_handler_id=auth_handler_id,
130
+ )
131
+
132
+ logger.info(f"Successfully exchanged token for {key}")
133
+ return token
134
+ except Exception as e:
135
+ # Return None if token generation fails
136
+ logger.error(f"Token exchange failed for {key}: {type(e).__name__}")
137
+ return None
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.4
2
+ Name: microsoft-agents-a365-observability-hosting
3
+ Version: 0.2.1.dev0
4
+ Summary: Hosting components for Agent 365 observability
5
+ Author-email: Microsoft <support@microsoft.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/microsoft/Agent365-python
8
+ Project-URL: Repository, https://github.com/microsoft/Agent365-python
9
+ Project-URL: Issues, https://github.com/microsoft/Agent365-python/issues
10
+ Project-URL: Documentation, https://github.com/microsoft/Agent365-python/tree/main/libraries/microsoft-agents-a365-observability-hosting
11
+ Keywords: observability,telemetry,tracing,opentelemetry,monitoring,ai,agents,hosting
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
+ Classifier: Topic :: System :: Monitoring
21
+ Requires-Python: >=3.11
22
+ Description-Content-Type: text/markdown
23
+ Requires-Dist: microsoft-agents-hosting-core<0.6.0,>=0.4.0
24
+ Requires-Dist: microsoft-agents-a365-observability-core>=0.0.0
25
+ Requires-Dist: opentelemetry-api>=1.36.0
26
+
27
+ # Microsoft Agent 365 Observability Hosting Library
28
+
29
+ This library provides hosting components for Agent 365 observability.
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install microsoft-agents-a365-observability-hosting
35
+ ```
@@ -0,0 +1,11 @@
1
+ microsoft_agents_a365/observability/hosting/__init__.py,sha256=94JvLF3ymPp0EfJMRPEhWWsBljFlAM1O5aXFImFgpqw,133
2
+ microsoft_agents_a365/observability/hosting/scope_helpers/__init__.py,sha256=HIog-luBqQnLkza1Q8b34Rr1QVu6tuiAy5sOry0vIPg,73
3
+ microsoft_agents_a365/observability/hosting/scope_helpers/populate_baggage.py,sha256=y39TwqC0RDQkuDf1nyRSn9Z6yX3e8h_rjjCzOLKOuNI,1419
4
+ microsoft_agents_a365/observability/hosting/scope_helpers/populate_invoke_agent_scope.py,sha256=h2d9kJy-fprnilmWXOC7m9OX-66dOupcrHJNay0QSQI,1513
5
+ microsoft_agents_a365/observability/hosting/scope_helpers/utils.py,sha256=Ic1rOxCIT08K-UCwkwjcVCbF_hxysMoCg7a_AgpCJfM,3694
6
+ microsoft_agents_a365/observability/hosting/token_cache_helpers/__init__.py,sha256=cLrxkhOtq7WXyhdAfn_vCdJpbf6K0y283MGcfQIqZq0,243
7
+ microsoft_agents_a365/observability/hosting/token_cache_helpers/agent_token_cache.py,sha256=iVbhYpnLLMx-PSM_jlcMTkVDAz4J8KgWWUQwYkZoxRQ,4380
8
+ microsoft_agents_a365_observability_hosting-0.2.1.dev0.dist-info/METADATA,sha256=g7tF4cVcL2yjyG1lDf5LfxvKk_9mlhos_A-2rC6pQZo,1550
9
+ microsoft_agents_a365_observability_hosting-0.2.1.dev0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
10
+ microsoft_agents_a365_observability_hosting-0.2.1.dev0.dist-info/top_level.txt,sha256=G3c2_4sy5_EM_BWO67SbK2tKj4G8XFn-QXRbh8g9Lgk,22
11
+ microsoft_agents_a365_observability_hosting-0.2.1.dev0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+