Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 21 additions & 16 deletions examples/agent365/README.md
Original file line number Diff line number Diff line change
@@ -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=<tenant-id>
AGENT365_BLUEPRINT_CLIENT_ID=<agent-identity-blueprint-app-id>
AGENT365_BLUEPRINT_CLIENT_SECRET=<agent-identity-blueprint-secret>
AGENT365_AGENT_IDENTITY_APP_ID=<agent-identity-app-id>
AGENT365_AGENT_USER_ID=<agent-user-object-id>
```

Then run:
export CLIENT_ID=<agent-identity-blueprint-app-id>
export CLIENT_SECRET=<agent-identity-blueprint-secret>
export TENANT_ID=<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=<agent-identity-blueprint-app-id>
export CLIENT_SECRET=<agent-identity-blueprint-secret>
export TENANT_ID=<tenant-id>

uv run --project examples/agent365 python src/proactive.py \
<conversation-id> \
<agent-identity-app-id> \
<agent-user-id>
```

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.
78 changes: 43 additions & 35 deletions examples/agent365/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
102 changes: 102 additions & 0 deletions examples/agent365/src/proactive.py
Original file line number Diff line number Diff line change
@@ -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())
13 changes: 13 additions & 0 deletions packages/api/src/microsoft_teams/api/models/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .custom_base_model import CustomBaseModel

AccountType = Literal["person", "tag", "channel", "team", "bot"]
AccountRole = Literal["agenticUser"]


class Account(CustomBaseModel):
Expand Down Expand Up @@ -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):
Expand Down
2 changes: 2 additions & 0 deletions packages/apps/src/microsoft_teams/apps/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -23,6 +24,7 @@
__all__: list[str] = [
"App",
"AppOptions",
"AgentUserIdentity",
"HttpServer",
"HttpServerAdapter",
"FastAPIAdapter",
Expand Down
4 changes: 2 additions & 2 deletions packages/apps/src/microsoft_teams/apps/activity_sender.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions packages/apps/src/microsoft_teams/apps/agent_user.py
Original file line number Diff line number Diff line change
@@ -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"]
Loading
Loading