diff --git a/examples/agent365/README.md b/examples/agent365/README.md index c3164c58a..ddd6c41f7 100644 --- a/examples/agent365/README.md +++ b/examples/agent365/README.md @@ -1,31 +1,36 @@ # agent365 -Acquire an Agent 365 agent user token using an agent identity blueprint, an agent identity app ID, and an agent user. +Demonstrates Agent 365 `AgentUserIdentity` support in reactive and proactive modes. -## Run +## Reactive Echo -Set these environment variables or add them to `.env`: +`src/main.py` mimics the echo example. Incoming messages are handled normally, but when the inbound activity recipient has `role="agenticUser"`, `ctx.send()` and `ctx.reply()` send from that concrete `AgentUserIdentity` using the inbound activity's service URL. ```bash -AGENT365_TENANT_ID= -AGENT365_BLUEPRINT_CLIENT_ID= -AGENT365_BLUEPRINT_CLIENT_SECRET= -AGENT365_AGENT_IDENTITY_APP_ID= -AGENT365_AGENT_USER_ID= -``` - -Then run: +export CLIENT_ID= +export CLIENT_SECRET= +export TENANT_ID= -```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`. +## Proactive AgentUserIdentity Send -To request another resource, set `AGENT365_SCOPE`, for example: +`src/proactive.py` mimics the proactive messaging example, but sends from a specific AgentUserIdentity. Supply the concrete agent identity app ID and agent user ID. ```bash -AGENT365_SCOPE=https://graph.microsoft.com/.default +export CLIENT_ID= +export CLIENT_SECRET= +export TENANT_ID= + +uv run --project examples/agent365 python src/proactive.py \ + \ + \ + ``` -You can use `AGENT365_AGENT_USER_UPN` instead of `AGENT365_AGENT_USER_ID`. +## Identity Model + +- `CLIENT_ID`: blueprint client/app ID. +- `agent_identity_app_id`: concrete agent identity app/client ID. +- `agent_user_id`: user-shaped account/persona object ID for the agent. diff --git a/examples/agent365/src/main.py b/examples/agent365/src/main.py index 98ddca0d2..261d74acd 100644 --- a/examples/agent365/src/main.py +++ b/examples/agent365/src/main.py @@ -3,51 +3,59 @@ Licensed under the MIT License. """ +# Agent 365 Reactive Example +# ========================== +# This example echoes messages back from the concrete AgentUserIdentity in the inbound activity. + import asyncio -import os +import logging +import re -from dotenv import load_dotenv -from microsoft_teams.api import ClientCredentials -from microsoft_teams.apps.token_manager import AGENT_BOT_API_SCOPE, TokenManager +from microsoft_teams.api import AdaptiveCardInvokeActivity, MessageActivity +from microsoft_teams.api.activities.typing import TypingActivityInput +from microsoft_teams.api.models.adaptive_card import AdaptiveCardActionMessageResponse +from microsoft_teams.api.models.invoke_response import AdaptiveCardInvokeResponse +from microsoft_teams.apps import ActivityContext, App +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) -def get_required_env(name: str) -> str: - value = os.getenv(name) - if not value: - raise ValueError(f"{name} must be set") +app = App() - return value +@app.on_message_pattern(re.compile(r"hello|hi|greetings")) +async def handle_greeting(ctx: ActivityContext[MessageActivity]) -> None: + """Handle greeting messages as the inbound agent user.""" + await ctx.reply("Hello! How can I assist you today?") -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") - agent_identity_app_id = get_required_env("AGENT365_AGENT_IDENTITY_APP_ID") - agent_user_id = os.getenv("AGENT365_AGENT_USER_ID") - agent_user_upn = os.getenv("AGENT365_AGENT_USER_UPN") - scope = os.getenv("AGENT365_SCOPE", AGENT_BOT_API_SCOPE) +@app.on_message +async def handle_message(ctx: ActivityContext[MessageActivity]): + """Echo incoming messages as the inbound agent user.""" + logger.info(f"[Agent365 onMessage] Message received: {ctx.activity.text}") + logger.info(f"[Agent365 onMessage] From: {ctx.activity.from_}") + logger.info(f"[Agent365 onMessage] Agent user identity: {ctx.agent_user_identity}") - credentials = ClientCredentials( - client_id=blueprint_client_id, - client_secret=blueprint_client_secret, - tenant_id=tenant_id, - ) - token_manager = TokenManager(credentials=credentials) - - token = await token_manager.get_agent_user_token( - tenant_id, - agent_identity_app_id, - scope, - agent_user_id=agent_user_id, - agent_user_upn=agent_user_upn, - ) + await ctx.reply(TypingActivityInput()) - print(f"Acquired agent user token for {scope}") - print(f"Token preview: {str(token)[:20]}...") + 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}'") + + +@app.on_card_action_execute("ack_agent365_card") +async def handle_agent365_card_ack(ctx: ActivityContext[AdaptiveCardInvokeActivity]) -> AdaptiveCardInvokeResponse: + """Handle the Action.Execute button from the proactive AgentUserIdentity card.""" + data = ctx.activity.value.action.data + logger.info(f"[Agent365 card] Acknowledged with data: {data}") + await ctx.send(f"Acknowledged Agent 365 card. Data: {data}") + return AdaptiveCardActionMessageResponse( + status_code=200, + type="application/vnd.microsoft.activity.message", + value="Acknowledged", + ) 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..d7812df56 --- /dev/null +++ b/examples/agent365/src/proactive.py @@ -0,0 +1,102 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +# Agent 365 Proactive Example +# =========================== +# This example sends proactive messages from a specific AgentUserIdentity. + +import argparse +import asyncio +import logging + +from microsoft_teams.apps import App +from microsoft_teams.cards import ActionSet, AdaptiveCard, ExecuteAction, SubmitData, TextBlock + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +async def send_proactive_message( + app: App, + conversation_id: str, + agent_identity_app_id: str, + agent_user_id: str, + message: str, +) -> None: + """Send a proactive message from an AgentUserIdentity.""" + agent_user_identity = app.get_agent_user_identity(agent_identity_app_id, agent_user_id) + logger.info(f"Sending proactive message as agent user: {agent_user_identity.id}") + logger.info(f"Message: {message}") + result = await app.send(conversation_id, agent_user_identity, message) + + logger.info(f"Message sent successfully. Activity ID: {result.id}") + + +async def send_proactive_card( + app: App, + conversation_id: str, + agent_identity_app_id: str, + agent_user_id: str, +) -> None: + """Send a proactive Adaptive Card from an AgentUserIdentity.""" + agent_user_identity = app.get_agent_user_identity(agent_identity_app_id, agent_user_id) + card = AdaptiveCard( + schema="http://adaptivecards.io/schemas/adaptive-card.json", + body=[ + TextBlock(text="Agent 365 Notification", size="Large", weight="Bolder"), + TextBlock(text="This message was sent proactively from an AgentUserIdentity.", wrap=True), + TextBlock(text=f"Agent user: {agent_user_identity.id}", wrap=True, is_subtle=True), + ActionSet( + actions=[ + ExecuteAction(title="Acknowledge") + .with_data(SubmitData("ack_agent365_card", {"agent_user_id": agent_user_identity.id})) + .with_associated_inputs("auto") + ] + ), + ], + ) + + logger.info(f"Sending proactive card as agent user: {agent_user_identity.id}") + + result = await app.send(conversation_id, agent_user_identity, card) + + logger.info(f"Card sent successfully. Activity ID: {result.id}") + + +async def main(): + parser = argparse.ArgumentParser(description="Send proactive messages from an Agent 365 AgentUserIdentity") + parser.add_argument("conversation_id", help="The Teams conversation ID to send messages to") + parser.add_argument("agent_identity_app_id", help="The concrete agent identity app/client ID") + parser.add_argument("agent_user_id", help="The agent user object ID") + args = parser.parse_args() + + app = App() + + logger.info("Initializing app without starting server...") + await app.initialize() + logger.info("App initialized") + + await send_proactive_message( + app, + args.conversation_id, + args.agent_identity_app_id, + args.agent_user_id, + "Hello! This is a proactive message sent from an AgentUserIdentity.", + ) + + await asyncio.sleep(2) + + await send_proactive_card( + app, + args.conversation_id, + args.agent_identity_app_id, + args.agent_user_id, + ) + + logger.info("All proactive AgentUserIdentity messages sent successfully") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/packages/api/src/microsoft_teams/api/models/account.py b/packages/api/src/microsoft_teams/api/models/account.py index 9c58c8f2b..2489bd111 100644 --- a/packages/api/src/microsoft_teams/api/models/account.py +++ b/packages/api/src/microsoft_teams/api/models/account.py @@ -10,6 +10,7 @@ from .custom_base_model import CustomBaseModel AccountType = Literal["person", "tag", "channel", "team", "bot"] +AccountRole = Literal["agenticUser"] class Account(CustomBaseModel): @@ -44,6 +45,18 @@ class Account(CustomBaseModel): """ The name of the account. """ + role: Optional[AccountRole | str] = None + """The role of the account in the activity.""" + agentic_user_id: Optional[str] = None + """The Agent ID user-shaped identity object ID.""" + agentic_app_id: Optional[str] = None + """The Agent ID app/client ID for the concrete agent identity.""" + agentic_app_blueprint_id: Optional[str] = None + """The Agent ID blueprint app/client ID.""" + callback_uri: Optional[str] = None + """The callback URI associated with the agent identity.""" + tenant_id: Optional[str] = None + """The tenant ID associated with the account.""" class TeamsChannelAccount(CustomBaseModel): diff --git a/packages/apps/src/microsoft_teams/apps/__init__.py b/packages/apps/src/microsoft_teams/apps/__init__.py index 54ad5b922..8181a9df5 100644 --- a/packages/apps/src/microsoft_teams/apps/__init__.py +++ b/packages/apps/src/microsoft_teams/apps/__init__.py @@ -6,6 +6,7 @@ import logging from . import auth, contexts, events, plugins +from .agent_user import AgentUserIdentity from .app import App from .auth import * # noqa: F403 from .contexts import * # noqa: F403 @@ -23,6 +24,7 @@ __all__: list[str] = [ "App", "AppOptions", + "AgentUserIdentity", "HttpServer", "HttpServerAdapter", "FastAPIAdapter", diff --git a/packages/apps/src/microsoft_teams/apps/activity_sender.py b/packages/apps/src/microsoft_teams/apps/activity_sender.py index 1ab97c96c..18c4835a1 100644 --- a/packages/apps/src/microsoft_teams/apps/activity_sender.py +++ b/packages/apps/src/microsoft_teams/apps/activity_sender.py @@ -36,13 +36,13 @@ def __init__(self, client: Client): """ self._client = client - async def send(self, activity: ActivityParams, ref: ConversationReference) -> SentActivity: + async def send(self, ref: ConversationReference, activity: ActivityParams) -> SentActivity: """ Send an activity to the Bot Framework. Args: - activity: The activity to send ref: The conversation reference + activity: The activity to send Returns: The sent activity with id and other server-populated fields diff --git a/packages/apps/src/microsoft_teams/apps/agent_user.py b/packages/apps/src/microsoft_teams/apps/agent_user.py new file mode 100644 index 000000000..41b62c387 --- /dev/null +++ b/packages/apps/src/microsoft_teams/apps/agent_user.py @@ -0,0 +1,18 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class AgentUserIdentity: + """Identifies an Agent ID user-shaped identity and its backing agent app.""" + + id: str + agent_identity_app_id: str + tenant_id: str + + +__all__ = ["AgentUserIdentity"] diff --git a/packages/apps/src/microsoft_teams/apps/app.py b/packages/apps/src/microsoft_teams/apps/app.py index c8e93be73..d913409a6 100644 --- a/packages/apps/src/microsoft_teams/apps/app.py +++ b/packages/apps/src/microsoft_teams/apps/app.py @@ -36,6 +36,7 @@ from msgraph.graph_service_client import GraphServiceClient from .activity_sender import ActivitySender +from .agent_user import AgentUserIdentity from .app_events import EventManager from .app_oauth import OauthHandlers from .app_plugins import PluginProcessor @@ -290,7 +291,27 @@ 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): + @overload + async def send( + self, + conversation_id: str, + activity: str | ActivityParams | AdaptiveCard, + ) -> SentActivity: ... + + @overload + async def send( + self, + conversation_id: str, + from_: AgentUserIdentity, + activity: str | ActivityParams | AdaptiveCard, + ) -> SentActivity: ... + + async def send( # type: ignore[reportInconsistentOverload] + self, + conversation_id: str, + from_or_activity: AgentUserIdentity | str | ActivityParams | AdaptiveCard, + activity: str | ActivityParams | AdaptiveCard | None = None, + ) -> SentActivity: """Send an activity proactively to a conversation. Sends to the exact conversation ID provided. For channel threads, @@ -301,29 +322,113 @@ async def send(self, conversation_id: str, activity: str | ActivityParams | Adap if not self._initialized: raise ValueError("app not initialized - call app.initialize() or app.start() first") - if self.id is None: - raise ValueError("app credentials not configured") + if isinstance(from_or_activity, AgentUserIdentity): + from_ = from_or_activity + activity_params = activity + if activity_params is None: + raise TypeError("activity is required when sending as an agent user") + else: + from_ = None + activity_params = from_or_activity conversation_ref = ConversationReference( channel_id="msteams", service_url=self.api.service_url, - bot=Account(id=self.id), + bot=self._get_outbound_account(from_), conversation=ConversationAccount(id=conversation_id), ) - if isinstance(activity, str): - activity = MessageActivityInput(text=activity) - elif isinstance(activity, AdaptiveCard): - activity = MessageActivityInput().add_card(activity) + if isinstance(activity_params, str): + activity_params = MessageActivityInput(text=activity_params) + elif isinstance(activity_params, AdaptiveCard): + activity_params = MessageActivityInput().add_card(activity_params) else: - activity = activity + activity_params = activity_params + + return await self._get_activity_sender(from_).send(conversation_ref, activity_params) + + def get_agent_user_identity( + self, + agent_identity_app_id: str, + agent_user_id: str, + *, + tenant_id: Optional[str] = None, + ) -> AgentUserIdentity: + """Get an Agent ID user identity.""" + 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 agent user") + + return AgentUserIdentity( + id=agent_user_id, + agent_identity_app_id=agent_identity_app_id, + tenant_id=resolved_tenant_id, + ) + + def get_scoped_api(self, agent_user: AgentUserIdentity, service_url: Optional[str] = None) -> ApiClient: + """Get a Teams API client scoped to an Agent ID user identity.""" + return ApiClient( + service_url or self.api.service_url, + self.http_client.clone(ClientOptions(token=lambda: self._get_agent_bot_token(agent_user))), + self.options.api_client_settings, + cloud=self.cloud, + ) + + def get_scoped_graph(self, agent_user: AgentUserIdentity) -> "GraphServiceClient": + """Get a Graph client scoped to an Agent ID user identity.""" + return create_graph_client(lambda: self._get_agent_graph_token(agent_user), cloud=self.cloud) - return await self.activity_sender.send(activity, conversation_ref) + def _get_outbound_account(self, from_: AgentUserIdentity | None = None) -> Account: + if from_ is None: + if self.id is None: + raise ValueError("app credentials not configured") + return Account(id=self.id) + + return Account(id=from_.id if from_.id.startswith("8:") else f"8:orgid:{from_.id}") + + def _get_activity_sender(self, from_: AgentUserIdentity | None = None) -> ActivitySender: + if from_ is None: + return self.activity_sender + + return ActivitySender(self.http_client.clone(ClientOptions(token=lambda: self._get_agent_bot_token(from_)))) + + async def _get_agent_bot_token(self, agent_user: AgentUserIdentity) -> Optional[TokenProtocol]: + return await self._token_manager.get_agent_bot_token( + agent_user.tenant_id, + agent_user.agent_identity_app_id, + agent_user_id=agent_user.id, + caller_name="get_agent_bot_token", + ) + + async def _get_agent_graph_token(self, agent_user: AgentUserIdentity) -> Optional[TokenProtocol]: + return await self._token_manager.get_agent_user_token( + agent_user.tenant_id, + agent_user.agent_identity_app_id, + self.cloud.graph_scope, + agent_user_id=agent_user.id, + caller_name="get_agent_graph_token", + ) + + @overload + async def reply( + self, + conversation_id: str, + message_id: str, + activity: str | ActivityParams | AdaptiveCard, + ) -> SentActivity: ... @overload async def reply( self, conversation_id: str, + message_id: str | ActivityParams | AdaptiveCard, + ) -> SentActivity: ... + + @overload + async def reply( + self, + conversation_id: str, + from_: AgentUserIdentity, message_id: str, activity: str | ActivityParams | AdaptiveCard, ) -> SentActivity: ... @@ -332,13 +437,15 @@ async def reply( async def reply( self, conversation_id: str, + from_: AgentUserIdentity, message_id: str | ActivityParams | AdaptiveCard, ) -> SentActivity: ... async def reply( # type: ignore[reportInconsistentOverload] self, conversation_id: str, - message_id: str | ActivityParams | AdaptiveCard = "", + from_or_message_id: AgentUserIdentity | str | ActivityParams | AdaptiveCard = "", + message_id_or_activity: str | ActivityParams | AdaptiveCard | None = None, activity: str | ActivityParams | AdaptiveCard | None = None, ) -> SentActivity: """Send an activity proactively to a conversation, optionally as a threaded reply. @@ -357,12 +464,26 @@ async def reply( # type: ignore[reportInconsistentOverload] message_id: The thread root message ID (3-arg form) or the activity (2-arg form) activity: The activity to send (only in 3-arg form) """ - if activity is not None: - if not isinstance(message_id, str): + if isinstance(from_or_message_id, AgentUserIdentity): + if activity is not None: + if not isinstance(message_id_or_activity, 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_or_activity), from_or_message_id, activity + ) + + if message_id_or_activity is None: + raise TypeError("activity is required when replying as an agent user") + return await self.send(conversation_id, from_or_message_id, message_id_or_activity) + + if message_id_or_activity is not None: + if not isinstance(from_or_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, from_or_message_id), message_id_or_activity + ) - return await self.send(conversation_id, message_id) + return await self.send(conversation_id, from_or_message_id) 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 1b2e9f27a..5f6d3a686 100644 --- a/packages/apps/src/microsoft_teams/apps/app_process.py +++ b/packages/apps/src/microsoft_teams/apps/app_process.py @@ -129,6 +129,7 @@ async def _build_context( self.default_connection_name, activity_sender=self.activity_sender, app_token=lambda: self.token_manager.get_graph_token(tenant_id), + token_manager=self.token_manager, cloud=self.cloud, ) diff --git a/packages/apps/src/microsoft_teams/apps/contexts/function_context.py b/packages/apps/src/microsoft_teams/apps/contexts/function_context.py index 4ce90e9e3..f34a2825c 100644 --- a/packages/apps/src/microsoft_teams/apps/contexts/function_context.py +++ b/packages/apps/src/microsoft_teams/apps/contexts/function_context.py @@ -79,7 +79,7 @@ async def send(self, activity: str | ActivityParams | AdaptiveCard) -> Optional[ else: activity = activity - return await self.activity_sender.send(activity, conversation_ref) + return await self.activity_sender.send(conversation_ref, activity) async def _resolve_conversation_id(self, activity: str | ActivityParams | AdaptiveCard) -> Optional[str]: """Resolve or create a conversation ID for the current user/context. 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 6c70ceecd..10006d1b7 100644 --- a/packages/apps/src/microsoft_teams/apps/routing/activity_context.py +++ b/packages/apps/src/microsoft_teams/apps/routing/activity_context.py @@ -37,11 +37,13 @@ ) from microsoft_teams.api.models.oauth import OAuthCard from microsoft_teams.cards import AdaptiveCard -from microsoft_teams.common import Storage +from microsoft_teams.common import ClientOptions, Storage from microsoft_teams.common.experimental import ExperimentalWarning, experimental from microsoft_teams.common.http.client_token import Token from ..activity_sender import ActivitySender +from ..agent_user import AgentUserIdentity +from ..token_manager import TokenManager from ..utils import create_graph_client if TYPE_CHECKING: @@ -88,6 +90,7 @@ def __init__( connection_name: str, activity_sender: ActivitySender, app_token: Token, + token_manager: Optional[TokenManager] = None, cloud: CloudEnvironment = PUBLIC, ): self.activity = activity @@ -102,6 +105,8 @@ def __init__( self.cloud = cloud self._activity_sender = activity_sender self._app_token = app_token + self._token_manager = token_manager + self.agent_user_identity = self._get_agent_user_identity() self.stream = activity_sender.create_stream(conversation_ref) self._next_handler: Optional[Callable[[], Awaitable[None]]] = None @@ -191,7 +196,12 @@ async def send( self._add_targeted_message_info_entity(activity) ref = conversation_ref or self.conversation_ref - res = await self._activity_sender.send(activity, ref) + if self.agent_user_identity is not None: + ref = ref.model_copy(deep=True) + ref.bot = self._get_agent_user_account(self.agent_user_identity) + return await self._get_agent_user_sender(self.agent_user_identity).send(ref, activity) + + res = await self._activity_sender.send(ref, activity) return res async def reply(self, input: str | ActivityParams) -> SentActivity: @@ -246,6 +256,44 @@ def _incoming_targeted_sender(self) -> Optional[Account]: return self.activity.from_ + def _get_agent_user_identity(self) -> Optional[AgentUserIdentity]: + recipient = getattr(self.activity, "recipient", None) + if recipient is None or recipient.role != "agenticUser": + return None + + if not recipient.agentic_user_id: + raise ValueError("agenticUser recipient is missing agenticUserId") + if not recipient.agentic_app_id: + raise ValueError("agenticUser recipient is missing agenticAppId") + + tenant_id = recipient.tenant_id or getattr(self.activity.conversation, "tenant_id", None) + if not tenant_id: + raise ValueError("agenticUser recipient is missing tenantId") + + return AgentUserIdentity( + id=recipient.agentic_user_id, + agent_identity_app_id=recipient.agentic_app_id, + tenant_id=tenant_id, + ) + + def _get_agent_user_account(self, agent_user: AgentUserIdentity) -> Account: + return Account(id=agent_user.id if agent_user.id.startswith("8:") else f"8:orgid:{agent_user.id}") + + def _get_agent_user_sender(self, agent_user: AgentUserIdentity) -> ActivitySender: + if self._token_manager is None: + raise ValueError("token_manager is required for agenticUser activities") + token_manager = self._token_manager + + async def get_token(): + return await token_manager.get_agent_bot_token( + agent_user.tenant_id, + agent_user.agent_identity_app_id, + agent_user_id=agent_user.id, + caller_name="get_agent_bot_token", + ) + + return ActivitySender(self.api.http.clone(ClientOptions(token=get_token))) + def _should_outbound_be_auto_targeted( self, activity: ActivityParams,