Skip to content

Commit eb8f016

Browse files
End-to-end fixes for agentic identities (#145)
* Adding agentic test sample * Removed extra logic around Graph token exchange * Basics for closing the loop with agentic work * Basics for closing the loop with agentic work * Adding conversation id truncation to all conversation operations that depend on it to form the API endpoint * Reformatting * Logging in sample * Refactored _get_agentic_token * Cleaning up changes * Centralizing conversation id normalization code
1 parent 3a2f9b9 commit eb8f016

9 files changed

Lines changed: 253 additions & 81 deletions

File tree

libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -213,21 +213,21 @@ async def create_conversation( # pylint: disable=arguments-differ
213213
claims_identity = self.create_claims_identity(agent_app_id)
214214
claims_identity.claims[AuthenticationConstants.SERVICE_URL_CLAIM] = service_url
215215

216-
# Create a UserTokenClient instance for the application to use. (For example, in the OAuthPrompt.)
217-
user_token_client: UserTokenClient = (
218-
await self._channel_service_client_factory.create_user_token_client(
219-
claims_identity
220-
)
221-
)
222-
223216
# Create a turn context and run the pipeline.
224217
context = self._create_turn_context(
225218
claims_identity,
226219
None,
227-
user_token_client,
228220
callback,
229221
)
230222

223+
# Create a UserTokenClient instance for the application to use. (For example, in the OAuthPrompt.)
224+
user_token_client: UserTokenClient = (
225+
await self._channel_service_client_factory.create_user_token_client(
226+
context, claims_identity
227+
)
228+
)
229+
context.turn_state[self.USER_TOKEN_CLIENT_KEY] = user_token_client
230+
231231
# Create the connector client to use for outbound requests.
232232
connector_client: ConnectorClient = (
233233
await self._channel_service_client_factory.create_connector_client(
@@ -264,22 +264,21 @@ async def process_proactive(
264264
callback: Callable[[TurnContext], Awaitable],
265265
):
266266

267-
# Create a UserTokenClient instance for the application to use. (For example, in the OAuthPrompt.)
268-
user_token_client: UserTokenClient = (
269-
await self._channel_service_client_factory.create_user_token_client(
270-
claims_identity
271-
)
272-
)
273-
274267
# Create a turn context and run the pipeline.
275268
context = self._create_turn_context(
276269
claims_identity,
277270
audience,
278-
user_token_client,
279271
callback,
280272
activity=continuation_activity,
281273
)
282274

275+
user_token_client: UserTokenClient = (
276+
await self._channel_service_client_factory.create_user_token_client(
277+
context, claims_identity
278+
)
279+
)
280+
context.turn_state[self.USER_TOKEN_CLIENT_KEY] = user_token_client
281+
283282
# Create the connector client to use for outbound requests.
284283
connector_client: ConnectorClient = (
285284
await self._channel_service_client_factory.create_connector_client(
@@ -338,22 +337,22 @@ async def process_activity(
338337
):
339338
use_anonymous_auth_callback = True
340339

341-
# Create a UserTokenClient instance for the OAuth flow.
342-
user_token_client: UserTokenClient = (
343-
await self._channel_service_client_factory.create_user_token_client(
344-
claims_identity, use_anonymous_auth_callback
345-
)
346-
)
347-
348340
# Create a turn context and run the pipeline.
349341
context = self._create_turn_context(
350342
claims_identity,
351343
outgoing_audience,
352-
user_token_client,
353344
callback,
354345
activity=activity,
355346
)
356347

348+
# Create a UserTokenClient instance for the OAuth flow.
349+
user_token_client: UserTokenClient = (
350+
await self._channel_service_client_factory.create_user_token_client(
351+
context, claims_identity, use_anonymous_auth_callback
352+
)
353+
)
354+
context.turn_state[self.USER_TOKEN_CLIENT_KEY] = user_token_client
355+
357356
# Create the connector client to use for outbound requests.
358357
connector_client: ConnectorClient = (
359358
await self._channel_service_client_factory.create_connector_client(
@@ -425,14 +424,12 @@ def _create_turn_context(
425424
self,
426425
claims_identity: ClaimsIdentity,
427426
oauth_scope: str,
428-
user_token_client: UserTokenClientBase,
429427
callback: Callable[[TurnContext], Awaitable],
430428
activity: Optional[Activity] = None,
431429
) -> TurnContext:
432430
context = TurnContext(self, activity, claims_identity)
433431

434432
context.turn_state[self.AGENT_IDENTITY_KEY] = claims_identity
435-
context.turn_state[self.USER_TOKEN_CLIENT_KEY] = user_token_client
436433
context.turn_state[self.AGENT_CALLBACK_HANDLER_KEY] = callback
437434
context.turn_state[self.CHANNEL_SERVICE_FACTORY_KEY] = (
438435
self._channel_service_client_factory

libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_client_factory_base.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
ConnectorClientBase,
77
UserTokenClientBase,
88
)
9+
from microsoft_agents.hosting.core.turn_context import TurnContext
910

1011

1112
class ChannelServiceClientFactoryBase(Protocol):
1213
@abstractmethod
1314
async def create_connector_client(
1415
self,
16+
context: TurnContext,
1517
claims_identity: ClaimsIdentity,
1618
service_url: str,
1719
audience: str,
@@ -32,7 +34,10 @@ async def create_connector_client(
3234

3335
@abstractmethod
3436
async def create_user_token_client(
35-
self, claims_identity: ClaimsIdentity, use_anonymous: bool = False
37+
self,
38+
context: TurnContext,
39+
claims_identity: ClaimsIdentity,
40+
use_anonymous: bool = False,
3641
) -> UserTokenClientBase:
3742
"""
3843
Creates the appropriate UserTokenClientBase instance.

libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/client/connector_client.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,12 @@ async def get_attachment(self, attachment_id: str, view_id: str) -> BytesIO:
122122

123123
class ConversationsOperations(ConversationsBase):
124124

125-
def __init__(self, client: ClientSession):
125+
def __init__(self, client: ClientSession, **kwargs):
126126
self.client = client
127+
self._max_conversation_id_length = kwargs.get("max_conversation_id_length", 200)
128+
129+
def _normalize_conversation_id(self, conversation_id: str) -> str:
130+
return conversation_id[: self._max_conversation_id_length]
127131

128132
async def get_conversations(
129133
self, continuation_token: Optional[str] = None
@@ -193,11 +197,16 @@ async def reply_to_activity(
193197
)
194198
raise ValueError("conversationId and activityId are required")
195199

200+
print("\n*3")
201+
print(conversation_id)
202+
print("\n*3")
203+
conversation_id = self._normalize_conversation_id(conversation_id)
196204
url = f"v3/conversations/{conversation_id}/activities/{activity_id}"
197205

198206
logger.info(
199207
f"Replying to activity: {activity_id} in conversation: {conversation_id}. Activity type is {body.type}"
200208
)
209+
201210
async with self.client.post(
202211
url,
203212
json=body.model_dump(
@@ -216,7 +225,8 @@ async def reply_to_activity(
216225
logger.info(
217226
f"Reply to conversation/activity: {result.get('id')}, {activity_id}"
218227
)
219-
return ResourceResponse.model_validate(result)
228+
229+
return ResourceResponse.model_validate(result)
220230

221231
async def send_to_conversation(
222232
self, conversation_id: str, body: Activity
@@ -235,6 +245,7 @@ async def send_to_conversation(
235245
)
236246
raise ValueError("conversationId is required")
237247

248+
conversation_id = self._normalize_conversation_id(conversation_id)
238249
url = f"v3/conversations/{conversation_id}/activities"
239250

240251
logger.info(
@@ -271,6 +282,7 @@ async def update_activity(
271282
)
272283
raise ValueError("conversationId and activityId are required")
273284

285+
conversation_id = self._normalize_conversation_id(conversation_id)
274286
url = f"v3/conversations/{conversation_id}/activities/{activity_id}"
275287

276288
logger.info(
@@ -303,6 +315,7 @@ async def delete_activity(self, conversation_id: str, activity_id: str) -> None:
303315
)
304316
raise ValueError("conversationId and activityId are required")
305317

318+
conversation_id = self._normalize_conversation_id(conversation_id)
306319
url = f"v3/conversations/{conversation_id}/activities/{activity_id}"
307320

308321
logger.info(
@@ -332,6 +345,7 @@ async def upload_attachment(
332345
)
333346
raise ValueError("conversationId is required")
334347

348+
conversation_id = self._normalize_conversation_id(conversation_id)
335349
url = f"v3/conversations/{conversation_id}/attachments"
336350

337351
# Convert the AttachmentData to a dictionary
@@ -371,6 +385,7 @@ async def get_conversation_members(
371385
)
372386
raise ValueError("conversationId is required")
373387

388+
conversation_id = self._normalize_conversation_id(conversation_id)
374389
url = f"v3/conversations/{conversation_id}/members"
375390

376391
logger.info(f"Getting conversation members for conversation: {conversation_id}")
@@ -402,6 +417,7 @@ async def get_conversation_member(
402417
)
403418
raise ValueError("conversationId and memberId are required")
404419

420+
conversation_id = self._normalize_conversation_id(conversation_id)
405421
url = f"v3/conversations/{conversation_id}/members/{member_id}"
406422

407423
logger.info(
@@ -434,6 +450,7 @@ async def delete_conversation_member(
434450
)
435451
raise ValueError("conversationId and memberId are required")
436452

453+
conversation_id = self._normalize_conversation_id(conversation_id)
437454
url = f"v3/conversations/{conversation_id}/members/{member_id}"
438455

439456
logger.info(
@@ -464,6 +481,7 @@ async def get_activity_members(
464481
)
465482
raise ValueError("conversationId and activityId are required")
466483

484+
conversation_id = self._normalize_conversation_id(conversation_id)
467485
url = f"v3/conversations/{conversation_id}/activities/{activity_id}/members"
468486

469487
logger.info(
@@ -507,6 +525,7 @@ async def get_conversation_paged_members(
507525
if continuation_token is not None:
508526
params["continuationToken"] = continuation_token
509527

528+
conversation_id = self._normalize_conversation_id(conversation_id)
510529
url = f"v3/conversations/{conversation_id}/pagedmembers"
511530

512531
logger.info(
@@ -540,6 +559,7 @@ async def send_conversation_history(
540559
)
541560
raise ValueError("conversationId is required")
542561

562+
conversation_id = self._normalize_conversation_id(conversation_id)
543563
url = f"v3/conversations/{conversation_id}/activities/history"
544564

545565
logger.info(f"Sending conversation history to conversation: {conversation_id}")

0 commit comments

Comments
 (0)