From 5e880f11179e95e8fe50091f2044f8093340ed96 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Fri, 19 Jun 2026 12:48:59 -0700 Subject: [PATCH 1/7] Add app-level agentic send helpers --- examples/agent365/README.md | 35 ++++------- examples/agent365/src/main.py | 56 ++++++++--------- packages/apps/src/microsoft_teams/apps/app.py | 62 +++++++++++++++++-- .../src/microsoft_teams/apps/app_process.py | 7 ++- 4 files changed, 98 insertions(+), 62 deletions(-) diff --git a/examples/agent365/README.md b/examples/agent365/README.md index c3164c58a..194a66a7d 100644 --- a/examples/agent365/README.md +++ b/examples/agent365/README.md @@ -1,31 +1,18 @@ # agent365 -Acquire an Agent 365 agent user token using an agent identity blueprint, an agent identity app ID, and an agent user. +Demonstrates passing `AgenticIdentity` directly to Teams API surfaces. -## Run +## Proactive API Send -Set these environment variables or add them to `.env`: +`src/main.py` shows both `app.send(..., agentic_identity=...)` and the lower-level conversation activity API. In both cases the API layer asks the auth provider for the right Agent ID token and uses it in the request header. ```bash -AGENT365_TENANT_ID= -AGENT365_BLUEPRINT_CLIENT_ID= -AGENT365_BLUEPRINT_CLIENT_SECRET= -AGENT365_AGENT_IDENTITY_APP_ID= -AGENT365_AGENT_USER_ID= +export CLIENT_ID= +export CLIENT_SECRET= +export TENANT_ID= + +uv run --project examples/agent365 python src/main.py \ + \ + \ + ``` - -Then run: - -```bash -uv run --project examples/agent365 python src/main.py -``` - -By default this requests a Teams bot API token for `https://botapi.skype.com/.default`. - -To request another resource, set `AGENT365_SCOPE`, for example: - -```bash -AGENT365_SCOPE=https://graph.microsoft.com/.default -``` - -You can use `AGENT365_AGENT_USER_UPN` instead of `AGENT365_AGENT_USER_ID`. diff --git a/examples/agent365/src/main.py b/examples/agent365/src/main.py index 153e79f14..28403d350 100644 --- a/examples/agent365/src/main.py +++ b/examples/agent365/src/main.py @@ -3,46 +3,40 @@ Licensed under the MIT License. """ +import argparse import asyncio -import os +import logging -from dotenv import load_dotenv -from microsoft_teams.api import AgenticIdentity, ClientCredentials -from microsoft_teams.apps.token_manager import AGENT_BOT_API_SCOPE, TokenManager +from microsoft_teams.api import MessageActivityInput +from microsoft_teams.apps import App - -def get_required_env(name: str) -> str: - value = os.getenv(name) - if not value: - raise ValueError(f"{name} must be set") - - return value +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) async def main(): - load_dotenv() - - tenant_id = get_required_env("AGENT365_TENANT_ID") - blueprint_client_id = get_required_env("AGENT365_BLUEPRINT_CLIENT_ID") - blueprint_client_secret = get_required_env("AGENT365_BLUEPRINT_CLIENT_SECRET") - agentic_app_id = get_required_env("AGENT365_AGENTIC_APP_ID") - agentic_user_id = get_required_env("AGENT365_AGENTIC_USER_ID") - scope = os.getenv("AGENT365_SCOPE", AGENT_BOT_API_SCOPE) - - credentials = ClientCredentials( - client_id=blueprint_client_id, - client_secret=blueprint_client_secret, - tenant_id=tenant_id, + parser = argparse.ArgumentParser(description="Send a proactive message using API-level AgenticIdentity") + parser.add_argument("conversation_id", help="The Teams conversation ID to send messages to") + parser.add_argument("agentic_app_id", help="The concrete agent identity app/client ID") + parser.add_argument("agentic_user_id", help="The agent user object ID") + args = parser.parse_args() + + app = App() + await app.initialize() + + agentic_identity = app.get_agentic_identity(args.agentic_app_id, args.agentic_user_id) + sent = await app.send( + args.conversation_id, + "Hello from app.send with an AgenticIdentity.", + agentic_identity=agentic_identity, ) - token_manager = TokenManager(credentials=credentials) + logger.info("Sent activity through app.send. Activity ID: %s", sent.id) - token = await token_manager.get_agentic_token( - scope, - AgenticIdentity(agentic_app_id=agentic_app_id, agentic_user_id=agentic_user_id, tenant_id=tenant_id), + api_sent = await app.api.conversations.activities(args.conversation_id).create( + MessageActivityInput(text="Hello from the conversation activity API with an AgenticIdentity."), + agentic_identity=agentic_identity, ) - - print(f"Acquired agent user token for {scope}") - print(f"Token preview: {str(token)[:20]}...") + logger.info("Sent activity through app.api. Activity ID: %s", api_sent.id) if __name__ == "__main__": diff --git a/packages/apps/src/microsoft_teams/apps/app.py b/packages/apps/src/microsoft_teams/apps/app.py index 82c2ec6c7..37ad0e25f 100644 --- a/packages/apps/src/microsoft_teams/apps/app.py +++ b/packages/apps/src/microsoft_teams/apps/app.py @@ -15,6 +15,7 @@ Account, ActivityBase, ActivityParams, + AgenticIdentity, ApiClient, ClientCredentials, ConversationAccount, @@ -289,7 +290,14 @@ async def stop(self) -> None: self._events.emit("error", ErrorEvent(error, context={"method": "stop"})) raise - async def send(self, conversation_id: str, activity: str | ActivityParams | AdaptiveCard): + async def send( + self, + conversation_id: str, + activity: str | ActivityParams | AdaptiveCard, + *, + service_url: Optional[str] = None, + agentic_identity: Optional[AgenticIdentity] = None, + ) -> SentActivity: """Send an activity proactively to a conversation. Sends to the exact conversation ID provided. For channel threads, @@ -305,7 +313,7 @@ async def send(self, conversation_id: str, activity: str | ActivityParams | Adap conversation_ref = ConversationReference( channel_id="msteams", - service_url=self.api.service_url, + service_url=service_url or self.api.service_url, bot=Account(id=self.id), conversation=ConversationAccount(id=conversation_id), ) @@ -317,7 +325,32 @@ async def send(self, conversation_id: str, activity: str | ActivityParams | Adap else: activity = activity - return await send_or_update_activity(self.api, activity, conversation_ref) + return await send_or_update_activity( + self.api, + activity, + conversation_ref, + agentic_identity=agentic_identity, + ) + + def get_agentic_identity( + self, + agentic_app_id: str, + agentic_user_id: str, + *, + tenant_id: Optional[str] = None, + agentic_app_blueprint_id: Optional[str] = None, + ) -> AgenticIdentity: + """Get an Agent ID identity for API calls.""" + resolved_tenant_id = tenant_id or (self.credentials.tenant_id if self.credentials else None) + if resolved_tenant_id is None: + raise ValueError("tenant_id is required to get an agentic identity") + + return AgenticIdentity( + agentic_app_id=agentic_app_id, + agentic_user_id=agentic_user_id, + tenant_id=resolved_tenant_id, + agentic_app_blueprint_id=agentic_app_blueprint_id, + ) @overload async def reply( @@ -325,6 +358,9 @@ async def reply( conversation_id: str, message_id: str, activity: str | ActivityParams | AdaptiveCard, + *, + service_url: Optional[str] = None, + agentic_identity: Optional[AgenticIdentity] = None, ) -> SentActivity: ... @overload @@ -332,6 +368,9 @@ async def reply( self, conversation_id: str, message_id: str | ActivityParams | AdaptiveCard, + *, + service_url: Optional[str] = None, + agentic_identity: Optional[AgenticIdentity] = None, ) -> SentActivity: ... async def reply( # type: ignore[reportInconsistentOverload] @@ -339,6 +378,9 @@ async def reply( # type: ignore[reportInconsistentOverload] conversation_id: str, message_id: str | ActivityParams | AdaptiveCard = "", activity: str | ActivityParams | AdaptiveCard | None = None, + *, + service_url: Optional[str] = None, + agentic_identity: Optional[AgenticIdentity] = None, ) -> SentActivity: """Send an activity proactively to a conversation, optionally as a threaded reply. @@ -359,9 +401,19 @@ async def reply( # type: ignore[reportInconsistentOverload] if activity is not None: if not isinstance(message_id, str): raise TypeError("message_id must be a string when activity is provided") - return await self.send(to_threaded_conversation_id(conversation_id, message_id), activity) + return await self.send( + to_threaded_conversation_id(conversation_id, message_id), + activity, + service_url=service_url, + agentic_identity=agentic_identity, + ) - return await self.send(conversation_id, message_id) + return await self.send( + conversation_id, + message_id, + service_url=service_url, + agentic_identity=agentic_identity, + ) def use(self, middleware: Callable[[ActivityContext[ActivityBase]], Awaitable[None]]) -> None: """Add middleware to run on all activities.""" diff --git a/packages/apps/src/microsoft_teams/apps/app_process.py b/packages/apps/src/microsoft_teams/apps/app_process.py index fc9b69d39..48a05c7ea 100644 --- a/packages/apps/src/microsoft_teams/apps/app_process.py +++ b/packages/apps/src/microsoft_teams/apps/app_process.py @@ -21,11 +21,12 @@ from microsoft_teams.api.auth.cloud_environment import PUBLIC, CloudEnvironment from microsoft_teams.api.clients.user.params import GetUserTokenParams from microsoft_teams.cards import AdaptiveCard -from microsoft_teams.common import Client, ClientOptions, LocalStorage, Storage +from microsoft_teams.common import Client, LocalStorage, Storage if TYPE_CHECKING: from .app_events import EventManager +from .auth_provider import AppAuthProvider from .events import ActivityEvent, ActivityResponseEvent, ActivitySentEvent, ErrorEvent from .plugins import PluginActivityEvent, PluginBase, StreamCancelledError from .routing.activity_context import ActivityContext @@ -56,6 +57,7 @@ def __init__( self.default_connection_name = default_connection_name self.http_client = http_client self.token_manager = token_manager + self.auth_provider = AppAuthProvider(token_manager) self.api_client_settings = api_client_settings self.cloud = cloud @@ -90,8 +92,9 @@ async def _build_context( ) api_client = ApiClient( service_url, - self.http_client.clone(ClientOptions(token=self.token_manager.get_bot_token)), + self.http_client, self.api_client_settings, + auth_provider=self.auth_provider, ) # Check if user is signed in From acfe7f32ffe2f35ded2c4272ddd344080233f13d Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Wed, 17 Jun 2026 17:55:23 -0700 Subject: [PATCH 2/7] Split Agent 365 reactive and proactive examples --- examples/agent365/README.md | 16 ++++++++-- examples/agent365/src/main.py | 47 +++++++++++++++--------------- examples/agent365/src/proactive.py | 43 +++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 26 deletions(-) create mode 100644 examples/agent365/src/proactive.py diff --git a/examples/agent365/README.md b/examples/agent365/README.md index 194a66a7d..21bf73109 100644 --- a/examples/agent365/README.md +++ b/examples/agent365/README.md @@ -2,16 +2,28 @@ Demonstrates passing `AgenticIdentity` directly to Teams API surfaces. +## Reactive Echo + +`src/main.py` mimics the echo example. Incoming messages are handled normally; the inbound service URL and agentic identity are carried by the context/API layer. + +```bash +export CLIENT_ID= +export CLIENT_SECRET= +export TENANT_ID= + +uv run --project examples/agent365 python src/main.py +``` + ## Proactive API Send -`src/main.py` shows both `app.send(..., agentic_identity=...)` and the lower-level conversation activity API. In both cases the API layer asks the auth provider for the right Agent ID token and uses it in the request header. +`src/proactive.py` shows both `app.send(..., agentic_identity=...)` and the lower-level conversation activity API. In both cases the API layer asks the auth provider for the right Agent ID token and uses it in the request header. ```bash export CLIENT_ID= export CLIENT_SECRET= export TENANT_ID= -uv run --project examples/agent365 python src/main.py \ +uv run --project examples/agent365 python src/proactive.py \ \ \ diff --git a/examples/agent365/src/main.py b/examples/agent365/src/main.py index 28403d350..e3284754a 100644 --- a/examples/agent365/src/main.py +++ b/examples/agent365/src/main.py @@ -3,41 +3,40 @@ Licensed under the MIT License. """ -import argparse import asyncio import logging +import re -from microsoft_teams.api import MessageActivityInput -from microsoft_teams.apps import App +from microsoft_teams.api import MessageActivity +from microsoft_teams.api.activities.typing import TypingActivityInput +from microsoft_teams.apps import ActivityContext, App logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) +app = App() -async def main(): - parser = argparse.ArgumentParser(description="Send a proactive message using API-level AgenticIdentity") - parser.add_argument("conversation_id", help="The Teams conversation ID to send messages to") - parser.add_argument("agentic_app_id", help="The concrete agent identity app/client ID") - parser.add_argument("agentic_user_id", help="The agent user object ID") - args = parser.parse_args() - app = App() - await app.initialize() +@app.on_message_pattern(re.compile(r"hello|hi|greetings")) +async def handle_greeting(ctx: ActivityContext[MessageActivity]) -> None: + """Handle greeting messages using the inbound AgenticIdentity when present.""" + await ctx.reply("Hello! How can I assist you today?") - agentic_identity = app.get_agentic_identity(args.agentic_app_id, args.agentic_user_id) - sent = await app.send( - args.conversation_id, - "Hello from app.send with an AgenticIdentity.", - agentic_identity=agentic_identity, - ) - logger.info("Sent activity through app.send. Activity ID: %s", sent.id) - api_sent = await app.api.conversations.activities(args.conversation_id).create( - MessageActivityInput(text="Hello from the conversation activity API with an AgenticIdentity."), - agentic_identity=agentic_identity, - ) - logger.info("Sent activity through app.api. Activity ID: %s", api_sent.id) +@app.on_message +async def handle_message(ctx: ActivityContext[MessageActivity]): + """Echo incoming messages using the inbound AgenticIdentity when present.""" + logger.info("[Agent365 reactive] Message received: %s", ctx.activity.text) + logger.info("[Agent365 reactive] From: %s", ctx.activity.from_) + logger.info("[Agent365 reactive] Agentic identity: %s", ctx.activity.recipient.agentic_identity) + + await ctx.reply(TypingActivityInput()) + + if "reply" in ctx.activity.text.lower(): + await ctx.reply("Hello! How can I assist you today?") + else: + await ctx.send(f"You said '{ctx.activity.text}'") if __name__ == "__main__": - asyncio.run(main()) + asyncio.run(app.start()) diff --git a/examples/agent365/src/proactive.py b/examples/agent365/src/proactive.py new file mode 100644 index 000000000..4736bdab6 --- /dev/null +++ b/examples/agent365/src/proactive.py @@ -0,0 +1,43 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +import argparse +import asyncio +import logging + +from microsoft_teams.api import MessageActivityInput +from microsoft_teams.apps import App + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +async def main(): + parser = argparse.ArgumentParser(description="Send proactive messages using AgenticIdentity") + parser.add_argument("conversation_id", help="The Teams conversation ID to send messages to") + parser.add_argument("agentic_app_id", help="The concrete agent identity app/client ID") + parser.add_argument("agentic_user_id", help="The agent user object ID") + args = parser.parse_args() + + app = App() + await app.initialize() + + agentic_identity = app.get_agentic_identity(args.agentic_app_id, args.agentic_user_id) + sent = await app.send( + args.conversation_id, + "Hello from app.send with an AgenticIdentity.", + agentic_identity=agentic_identity, + ) + logger.info("Sent activity through app.send. Activity ID: %s", sent.id) + + api_sent = await app.api.conversations.activities(args.conversation_id).create( + MessageActivityInput(text="Hello from the conversation activity API with an AgenticIdentity."), + agentic_identity=agentic_identity, + ) + logger.info("Sent activity through app.api. Activity ID: %s", api_sent.id) + + +if __name__ == "__main__": + asyncio.run(main()) From 74247735198aad1aad0bdf59fc5b1ea60d573ece Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Wed, 17 Jun 2026 18:01:30 -0700 Subject: [PATCH 3/7] Show reactive Agent 365 reaction flow --- examples/agent365/src/main.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/examples/agent365/src/main.py b/examples/agent365/src/main.py index e3284754a..355b3d1f8 100644 --- a/examples/agent365/src/main.py +++ b/examples/agent365/src/main.py @@ -32,6 +32,15 @@ async def handle_message(ctx: ActivityContext[MessageActivity]): await ctx.reply(TypingActivityInput()) + if "react" in ctx.activity.text.lower(): + await ctx.api.reactions.add( + conversation_id=ctx.activity.conversation.id, + activity_id=ctx.activity.id, + reaction_type="like", + ) + await ctx.reply("Added a like reaction to your message.") + return + if "reply" in ctx.activity.text.lower(): await ctx.reply("Hello! How can I assist you today?") else: From b9b343719cb58d7c57db1c7b33ae2f3670c0b0df Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Thu, 18 Jun 2026 12:11:37 -0700 Subject: [PATCH 4/7] Test app agentic send helpers --- packages/apps/tests/test_app.py | 75 +++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/packages/apps/tests/test_app.py b/packages/apps/tests/test_app.py index 4c6d57fac..8a27eb15e 100644 --- a/packages/apps/tests/test_app.py +++ b/packages/apps/tests/test_app.py @@ -15,6 +15,7 @@ import pytest from microsoft_teams.api import ( Account, + AgenticIdentity, ConversationAccount, FederatedIdentityCredentials, InvokeActivity, @@ -802,6 +803,37 @@ async def test_proactive_targeted_with_explicit_recipient_succeeds(self, mock_st assert sent_activity.from_.id == "test-client-id" assert sent_activity.conversation.id == "conv-123" + @pytest.mark.asyncio + async def test_send_passes_agentic_identity_and_service_url(self, mock_storage) -> None: + options = AppOptions(storage=mock_storage, client_id="test-client-id", client_secret="test-secret") + app = App(**options) + app._initialized = True + create = AsyncMock( + return_value=SentActivity(id="sent-activity-id", activity_params=MessageActivityInput(text="sent")) + ) + activities = MagicMock() + activities.create = create + app.api.conversations.activities = MagicMock(return_value=activities) + agentic_identity = AgenticIdentity("agentic-app-id", "agentic-user-id", tenant_id="tenant-id") + + result = await app.send( + "conv-123", + "Hello", + service_url="https://override.service.url", + agentic_identity=agentic_identity, + ) + + app.api.conversations.activities.assert_called_once_with("conv-123") + create.assert_called_once() + activity = create.call_args.args[0] + assert isinstance(activity, MessageActivityInput) + assert activity.text == "Hello" + assert create.call_args.kwargs == { + "service_url": "https://override.service.url", + "agentic_identity": agentic_identity, + } + assert result.id == "sent-activity-id" + class TestAppInitialize: """Test cases for App.initialize() method.""" @@ -881,12 +913,55 @@ async def test_reply_with_three_args_constructs_threaded_id(self, started_app): started_app.api.conversations.activities.assert_called_once_with("19:abc@thread.skype;messageid=1680000000000") + @pytest.mark.asyncio + async def test_reply_with_three_args_passes_agentic_identity_and_service_url(self, started_app): + agentic_identity = AgenticIdentity("agentic-app-id", "agentic-user-id", tenant_id="tenant-id") + + await started_app.reply( + "19:abc@thread.skype", + "1680000000000", + "Hello thread", + service_url="https://override.service.url", + agentic_identity=agentic_identity, + ) + + started_app.api.conversations.activities.assert_called_once_with("19:abc@thread.skype;messageid=1680000000000") + create = started_app.api.conversations.activities.return_value.create + activity = create.call_args.args[0] + assert isinstance(activity, MessageActivityInput) + assert activity.text == "Hello thread" + assert create.call_args.kwargs == { + "service_url": "https://override.service.url", + "agentic_identity": agentic_identity, + } + @pytest.mark.asyncio async def test_reply_with_two_args_passes_conversation_id_as_is(self, started_app): await started_app.reply("19:abc@thread.skype", "Hello flat") started_app.api.conversations.activities.assert_called_once_with("19:abc@thread.skype") + @pytest.mark.asyncio + async def test_reply_with_two_args_passes_agentic_identity_and_service_url(self, started_app): + agentic_identity = AgenticIdentity("agentic-app-id", "agentic-user-id", tenant_id="tenant-id") + + await started_app.reply( + "19:abc@thread.skype", + "Hello flat", + service_url="https://override.service.url", + agentic_identity=agentic_identity, + ) + + started_app.api.conversations.activities.assert_called_once_with("19:abc@thread.skype") + create = started_app.api.conversations.activities.return_value.create + activity = create.call_args.args[0] + assert isinstance(activity, MessageActivityInput) + assert activity.text == "Hello flat" + assert create.call_args.kwargs == { + "service_url": "https://override.service.url", + "agentic_identity": agentic_identity, + } + @pytest.mark.asyncio async def test_reply_with_pre_constructed_threaded_id(self, started_app): await started_app.reply("19:abc@thread.skype;messageid=123", "Hello") From d0a93fe2a2884f945bcd076b91e575b6b123e632 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Fri, 19 Jun 2026 12:49:35 -0700 Subject: [PATCH 5/7] Use inbound agentic identity for reactive sends # Conflicts: # packages/apps/src/microsoft_teams/apps/routing/activity_context.py --- .../apps/routing/activity_context.py | 7 ++- packages/apps/tests/test_activity_context.py | 48 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/packages/apps/src/microsoft_teams/apps/routing/activity_context.py b/packages/apps/src/microsoft_teams/apps/routing/activity_context.py index ee9e372cd..9c05bc0eb 100644 --- a/packages/apps/src/microsoft_teams/apps/routing/activity_context.py +++ b/packages/apps/src/microsoft_teams/apps/routing/activity_context.py @@ -197,7 +197,12 @@ async def send( self._add_targeted_message_info_entity(activity) ref = conversation_ref or self.conversation_ref - return await send_or_update_activity(self.api, activity, ref) + return await send_or_update_activity( + self.api, + activity, + ref, + agentic_identity=self.activity.recipient.agentic_identity, + ) async def reply(self, input: str | ActivityParams) -> SentActivity: """Send a message in the current conversation with a visual quote of the inbound message. diff --git a/packages/apps/tests/test_activity_context.py b/packages/apps/tests/test_activity_context.py index fc0c856e8..db18e431f 100644 --- a/packages/apps/tests/test_activity_context.py +++ b/packages/apps/tests/test_activity_context.py @@ -380,6 +380,30 @@ async def test_send_with_adaptive_card(self) -> None: assert len(sent_activity.attachments) > 0 assert isinstance(result, SentActivity) + @pytest.mark.asyncio + async def test_send_passes_inbound_agentic_identity(self) -> None: + """Sending from an Agent ID activity uses the inbound agentic identity.""" + recipient = Account( + id="bot-id", + name="Test Bot", + agentic_app_id="agentic-app-id", + agentic_user_id="agentic-user-id", + tenant_id="tenant-id", + ) + activity = MessageActivity( + id="incoming-activity-id", + text="Incoming message", + from_=Account(id="user-id", name="Test User"), + recipient=recipient, + conversation=ConversationAccount(id="test-conversation"), + ) + ctx, mock_sender = _create_activity_context(activity=activity) + + await ctx.send("Hello") + + mock_sender.send.assert_called_once() + assert mock_sender.send.call_args.kwargs["agentic_identity"] == recipient.agentic_identity + class TestActivityContextReply: """Tests for ActivityContext.reply().""" @@ -424,6 +448,30 @@ async def test_reply_with_activity_params(self) -> None: assert isinstance(sent_activity.entities[0], QuotedReplyEntity) assert sent_activity.entities[0].quoted_reply.message_id == "evt-id-999" + @pytest.mark.asyncio + async def test_reply_passes_inbound_agentic_identity(self) -> None: + """Replying to an Agent ID activity uses the inbound agentic identity.""" + recipient = Account( + id="bot-id", + name="Test Bot", + agentic_app_id="agentic-app-id", + agentic_user_id="agentic-user-id", + tenant_id="tenant-id", + ) + activity = MessageActivity( + id="incoming-activity-id", + text="Incoming message", + from_=Account(id="user-id", name="Test User"), + recipient=recipient, + conversation=ConversationAccount(id="test-conversation"), + ) + ctx, mock_sender = _create_activity_context(activity=activity) + + await ctx.reply("Hello") + + mock_sender.send.assert_called_once() + assert mock_sender.send.call_args.kwargs["agentic_identity"] == recipient.agentic_identity + class TestActivityContextUserGraph: """Tests for ActivityContext.user_graph property.""" From 4718542b4ab512660309691432bc2afaa0c1c81a Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Thu, 18 Jun 2026 14:11:31 -0700 Subject: [PATCH 6/7] Scope reactive API to agentic identity --- .../src/microsoft_teams/apps/app_process.py | 1 + packages/apps/tests/test_app_process.py | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/packages/apps/src/microsoft_teams/apps/app_process.py b/packages/apps/src/microsoft_teams/apps/app_process.py index 48a05c7ea..9036984d1 100644 --- a/packages/apps/src/microsoft_teams/apps/app_process.py +++ b/packages/apps/src/microsoft_teams/apps/app_process.py @@ -95,6 +95,7 @@ async def _build_context( self.http_client, self.api_client_settings, auth_provider=self.auth_provider, + agentic_identity=activity.recipient.agentic_identity, ) # Check if user is signed in diff --git a/packages/apps/tests/test_app_process.py b/packages/apps/tests/test_app_process.py index f946b00f2..252651579 100644 --- a/packages/apps/tests/test_app_process.py +++ b/packages/apps/tests/test_app_process.py @@ -305,6 +305,45 @@ async def test_build_context_marks_signed_in_when_token_available(self, activity mock_api_client.users.token.get.assert_called_once() + @pytest.mark.asyncio + async def test_build_context_scopes_api_to_inbound_agentic_identity(self, activity_processor): + """Inbound Agent ID activities scope ctx.api with the inbound agentic identity.""" + core_activity = CoreActivity( + type="message", + id="activity-agentic", + service_url="https://service.url", + **{ + "from": {"id": "user-1", "name": "Test User"}, + "conversation": {"id": "conv-1"}, + "recipient": { + "id": "bot-1", + "name": "Test Bot", + "agenticAppId": "agentic-app-id", + "agenticUserId": "agentic-user-id", + "tenantId": "tenant-id", + }, + "channelId": "msteams", + }, + ) + mock_token = MagicMock(spec=TokenProtocol) + mock_token.service_url = "https://service.url" + mock_activity_event = ActivityEvent(body=core_activity, token=mock_token) + mock_api_client = MagicMock() + mock_api_client.users.token.get = AsyncMock(side_effect=Exception("no token")) + + activity_processor.router.select_handlers = MagicMock(return_value=[]) + activity_processor.event_manager = MagicMock() + activity_processor.event_manager.on_activity_response = AsyncMock() + activity_processor.event_manager.on_error = AsyncMock() + + with patch("microsoft_teams.apps.app_process.ApiClient", return_value=mock_api_client) as mock_api_client_type: + await activity_processor.process_activity([], mock_activity_event) + + agentic_identity = mock_api_client_type.call_args.kwargs["agentic_identity"] + assert agentic_identity.agentic_app_id == "agentic-app-id" + assert agentic_identity.agentic_user_id == "agentic-user-id" + assert agentic_identity.tenant_id == "tenant-id" + @pytest.mark.asyncio async def test_process_activity_raises_when_event_manager_missing(self, activity_processor): """process_activity raises ValueError if event_manager was never initialized.""" From 49b98f7e3d9fee8bead8c7cffc292857f74c3f4b Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Fri, 19 Jun 2026 13:10:02 -0700 Subject: [PATCH 7/7] Pass app auth provider to activity processor --- packages/apps/src/microsoft_teams/apps/app.py | 1 + packages/apps/src/microsoft_teams/apps/app_process.py | 3 ++- packages/apps/tests/test_app_oauth.py | 2 ++ packages/apps/tests/test_app_process.py | 4 ++++ 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/apps/src/microsoft_teams/apps/app.py b/packages/apps/src/microsoft_teams/apps/app.py index 37ad0e25f..1af304b19 100644 --- a/packages/apps/src/microsoft_teams/apps/app.py +++ b/packages/apps/src/microsoft_teams/apps/app.py @@ -137,6 +137,7 @@ def __init__(self, **options: Unpack[AppOptions]): self.options.default_connection_name, self.http_client, self._token_manager, + self._auth_provider, self.options.api_client_settings, self.cloud, ) diff --git a/packages/apps/src/microsoft_teams/apps/app_process.py b/packages/apps/src/microsoft_teams/apps/app_process.py index 9036984d1..aad80d1e0 100644 --- a/packages/apps/src/microsoft_teams/apps/app_process.py +++ b/packages/apps/src/microsoft_teams/apps/app_process.py @@ -48,6 +48,7 @@ def __init__( default_connection_name: str, http_client: Client, token_manager: TokenManager, + auth_provider: AppAuthProvider, api_client_settings: Optional[ApiClientSettings], cloud: CloudEnvironment = PUBLIC, ) -> None: @@ -57,7 +58,7 @@ def __init__( self.default_connection_name = default_connection_name self.http_client = http_client self.token_manager = token_manager - self.auth_provider = AppAuthProvider(token_manager) + self.auth_provider = auth_provider self.api_client_settings = api_client_settings self.cloud = cloud diff --git a/packages/apps/tests/test_app_oauth.py b/packages/apps/tests/test_app_oauth.py index c685a40d8..f6034686a 100644 --- a/packages/apps/tests/test_app_oauth.py +++ b/packages/apps/tests/test_app_oauth.py @@ -27,6 +27,7 @@ ) from microsoft_teams.apps.app_oauth import OauthHandlers from microsoft_teams.apps.app_process import ActivityProcessor +from microsoft_teams.apps.auth_provider import AppAuthProvider from microsoft_teams.apps.events import ErrorEvent, SignInEvent from microsoft_teams.apps.routing import ActivityContext from microsoft_teams.apps.routing.activity_route_configs import ACTIVITY_ROUTES @@ -445,6 +446,7 @@ def processor(self, router): default_connection_name="graph", http_client=MagicMock(), token_manager=MagicMock(), + auth_provider=MagicMock(spec=AppAuthProvider), api_client_settings=None, cloud=PUBLIC, ) diff --git a/packages/apps/tests/test_app_process.py b/packages/apps/tests/test_app_process.py index 252651579..fb01ccbc6 100644 --- a/packages/apps/tests/test_app_process.py +++ b/packages/apps/tests/test_app_process.py @@ -19,6 +19,7 @@ from microsoft_teams.apps import ActivityContext, ActivityEvent from microsoft_teams.apps.app_events import EventManager from microsoft_teams.apps.app_process import ActivityProcessor +from microsoft_teams.apps.auth_provider import AppAuthProvider from microsoft_teams.apps.events import CoreActivity from microsoft_teams.apps.routing.router import ActivityHandler, ActivityRouter from microsoft_teams.apps.token_manager import TokenManager @@ -42,6 +43,7 @@ def activity_processor(self, mock_http_client): mock_storage = MagicMock(spec=LocalStorage) mock_activity_router = MagicMock(spec=ActivityRouter) mock_token_manager = MagicMock(spec=TokenManager) + mock_auth_provider = MagicMock(spec=AppAuthProvider) return ActivityProcessor( mock_activity_router, "id", @@ -49,6 +51,7 @@ def activity_processor(self, mock_http_client): "default_connection", mock_http_client, mock_token_manager, + mock_auth_provider, None, PUBLIC, ) @@ -339,6 +342,7 @@ async def test_build_context_scopes_api_to_inbound_agentic_identity(self, activi with patch("microsoft_teams.apps.app_process.ApiClient", return_value=mock_api_client) as mock_api_client_type: await activity_processor.process_activity([], mock_activity_event) + assert mock_api_client_type.call_args.kwargs["auth_provider"] is activity_processor.auth_provider agentic_identity = mock_api_client_type.call_args.kwargs["agentic_identity"] assert agentic_identity.agentic_app_id == "agentic-app-id" assert agentic_identity.agentic_user_id == "agentic-user-id"