Skip to content

Commit 97612e7

Browse files
add token cache
1 parent b7b2e33 commit 97612e7

2 files changed

Lines changed: 144 additions & 0 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -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"]
Original file line numberDiff line numberDiff line change
@@ -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 commit comments

Comments
 (0)