Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
This page isn't available for C#.
Welcome, fellow agent developer! You've made it through a full major release of Teams SDK, and now you want to take the plunge into v2. In this guide, we'll walk you through everything you need to know, from migrating core features like message handlers and auth, to optional AI features like ActionPlanner.
Welcome, fellow agent developer! You've made it through a full major release of Teams SDK, and now you want to take the plunge into v2. In this guide, we'll walk you through everything you need to know, from migrating core features like message handlers and auth, to optional AI features like ActionPlanner. We'll also discuss how you can migrate features over incrementally via the botbuilder adapter.
Installing Teams SDK
First, let's install Teams SDK into your project. Notably, this won't replace any existing installation of Teams SDK. When you've completed your migration, you can safely remove the teams-ai dependency from your pyproject.toml file.
uv add microsoft-teams-apps
First, let's install Teams SDK into your project. Notably, this won't replace any existing installation of Teams SDK. When you've completed your migration, you can safely remove the @microsoft/teams-ai dependency from your package.json file.
npm install @microsoft/teams.apps
Migrate Application class
First, migrate your Application class from v1 to the new App class.
# in main.py
import asyncio
import logging
from microsoft.teams.api import MessageActivity
from microsoft.teams.apps import ActivityContext, App, ErrorEvent
from microsoft.teams.common import LocalStorage
logger = logging.getLogger(__name__)
# Define the app
app = App()
# Optionally create local storage
storage: LocalStorage[str] = LocalStorage()
@app.on_message
async def handle_message(ctx: ActivityContext[MessageActivity]):
await ctx.send(f"You said '{ctx.activity.text}'")
# Listen for errors
@app.event("error")
async def handle_error(event: ErrorEvent) -> None:
"""Handle errors."""
logger.error(f"Error occurred: {event.error}")
if event.context:
logger.warning(f"Context: {event.context}")
if __name__ == "__main__":
asyncio.run(app.start())
import { App } from '@microsoft/teams.apps';
import { LocalStorage } from '@microsoft/teams.common/storage';
// Define app
const app = new App({
clientId: process.env.ENTRA_APP_CLIENT_ID!,
clientSecret: process.env.ENTRA_APP_CLIENT_SECRET!,
tenantId: process.env.ENTRA_TENANT_ID!,
});
// Optionally create local storage
const storage = new LocalStorage();
// Listen for errors
app.event('error', async (client) => {
console.error('Error event received:', client.error);
if (client.activity) {
await app.send(
client.activity.conversation.id,
'An error occurred while processing your message.',
);
}
});
// App creates local server with route for /api/messages
// To reuse your restify or other server,
// create a custom `HttpPlugin`.
(async () => {
// starts the server
await app.start();
})();
Migrate activity handlers
Both v1 and v2 are built atop incoming Activity requests, which trigger handlers in your code when specific type of activities are received. The syntax for how you register different types of Activity handlers differs slightly between the v1 and v2 versions of our SDK.
Both v1 and v2 are built atop incoming Activity requests, which trigger handlers in your code when specific type of activities are received. The syntax for how you register different types of Activity handlers differs between the v1 and v2 versions of our SDK.
Message handlers
# Triggered when user sends "hi", "hello", or "greetings"
@app.on_message_pattern(re.compile(r"hello|hi|greetings"))
async def handle_greeting(ctx: ActivityContext[MessageActivity]) -> None:
await ctx.reply("Hello! How can I assist you today?")
# Listens for ANY message received
@app.on_message
async def handle_message(ctx: ActivityContext[MessageActivity]):
# Sends a typing indicator
await ctx.reply(TypingActivityInput())
await ctx.send(f"You said '{ctx.activity.text}'")
// triggers when user sends "/hi" or "@bot /hi"
app.message('/hi', async (client) => {
// SDK does not auto send typing indicators
await client.send({ type: 'typing' });
await client.send("Hi!");
});
// listen for ANY message to be received
app.on('message', async (client) => {
await client.send({ type: 'typing' });
await client.send(
`you said "${client.activity.text}"`
);
});
Task modules
Note that on Microsoft Teams, task modules have been renamed to dialogs.
@app.on_dialog_open
async def handle_dialog_open(ctx: ActivityContext[TaskFetchInvokeActivity]):
data: Optional[Any] = ctx.activity.value.data
dialog_type = data.get("opendialogtype") if data else None
if dialog_type == "some_type":
return InvokeResponse(
body=TaskModuleResponse(
task=TaskModuleContinueResponse(
value=UrlTaskModuleTaskInfo(
title="Dialog title",
height="medium",
width="medium",
url= f"https://${os.getenv("YOUR_WEBSITE_DOMAIN")}/some-path",
fallback_url= f"https://${os.getenv("YOUR_WEBSITE_DOMAIN")}/fallback-path-for-web",
completion_bot_id= os.getenv("ENTRA_APP_CLIENT_ID"),
)
)
)
)
@app.on_dialog_submit
async def handle_dialog_submit(ctx: ActivityContext[TaskSubmitInvokeActivity]):
data: Optional[Any] = ctx.activity.value.data
dialog_type = data.get("submissiondialogtype") if data else None
if dialog_type == "some_type":
await ctx.send(json.dumps(ctx.activity.value))
return TaskModuleResponse(task=TaskModuleMessageResponse(value="Received submit"))
app.on('dialog.open', (client) => {
const dialogType = client.activity.value.data?.opendialogtype;
if (dialogType === 'some-type') {
return {
task: {
type: 'continue',
value: {
title: 'Dialog title',
height: 'medium',
width: 'medium',
url: `https://${process.env.YOUR_WEBSITE_DOMAIN}/some-path`,
fallbackUrl: `https://${process.env.YOUR_WEBSITE_DOMAIN}/fallback-path-for-web`,
completionBotId: process.env.ENTRA_APP_CLIENT_ID!,
},
},
};
}
});
app.on('dialog.submit', async (client) => {
const dialogType = client.activity.value.data?.submissiondialogtype;
if (dialogType === 'some-type') {
const { data } = client.activity.value;
await client.send(JSON.stringify(data));
}
return undefined;
});
Learn more in the Dialogs guide.
Adaptive cards
In Teams SDK v2, cards have much more rich type validation than existed in v1. However, assuming your cards were valid, it should be easy to migrate to v2.
Option 1
For existing cards like this, the simplest way to convert that to Teams SDK is this:
@app.on_message_pattern("/card")
async def handle_card_message(ctx: ActivityContext[MessageActivity]):
print(f"[CARD] Card requested by: {ctx.activity.from_}")
card = AdaptiveCard.model_validate(
{
"schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.6",
"type": "AdaptiveCard",
"body": [
{
"text": "Hello, world!",
"wrap": True,
"type": "TextBlock",
},
],
"msteams": {
"width": "Full"
}
}
)
await ctx.send(card)
Option 2
For a more thorough port, you could also do the following:
@app.on_message_pattern("/card")
async def handle_card_message(ctx: ActivityContext[MessageActivity]):
card = AdaptiveCard(
schema="http://adaptivecards.io/schemas/adaptive-card.json",
body=[
TextBlock(text="Hello, world", wrap=True, weight="Bolder"),
],
ms_teams=TeamsCardProperties(width='full'),
)
await ctx.send(card)
Option 1
For existing cards like this, the simplest way to convert that to Teams SDK is this:
app.message('/card', async (client) => {
await client.send({
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
version: '1.5',
type: 'AdaptiveCard',
body: [
{
type: 'TextBlock',
text: 'Hello, world!',
wrap: true,
isSubtle: false,
},
],
msteams: {
width: 'Full',
},
});
});
Option 2
For a more thorough port, you could also do the following:
import { Card, TextBlock } from '@microsoft/teams.cards';
app.message('/card', async (client) => {
await client.send(
new Card(new TextBlock('Hello, world!', { wrap: true, isSubtle: false })).withOptions({
width: 'Full',
})
);
});
Learn more in the Adaptive Cards guide.
Authentication
Most agents feature authentication for user identification, interacting with APIs, etc. Whether your Teams SDK app used Entra SSO or custom OAuth, porting to v2 should be simple.
app = App()
@app.on_message
async def handle_message(ctx: ActivityContext[MessageActivity]):
ctx.logger.info("User requested sign-in.")
if ctx.is_signed_in:
await ctx.send("You are already signed in.")
else:
await ctx.sign_in()
@app.on_message_pattern("/signout")
async def handle_sign_out(ctx: ActivityContext[MessageActivity]):
await ctx.sign_out()
await ctx.send("You have been signed out.")
@app.event("sign_in")
async def handle_sign_in(event: SignInEvent):
"""Handle sign-in events."""
await event.activity_ctx.send("You are now signed in!")
@app.event("error")
async def handle_error(event: ErrorEvent):
"""Handle error events."""
print(f"Error occurred: {event.error}")
if event.context:
print(f"Context: {event.context}")
const app = new App({
oauth: {
// oauth configurations
/**
* The name of the auth connection to use.
* It should be the same as the OAuth connection name defined in the Azure Bot configuration.
*/
defaultConnectionName: 'graph',
},
logger: new ConsoleLogger('@tests/auth', { level: 'debug' }),
});
app.message('/signout', async (client) => {
if (!client.isSignedIn) return;
await client.signout(); // call signout for your auth connection...
await client.send('you have been signed out!');
});
app.message('/help', async (client) => {
await client.send('your help text');
});
app.on('message', async (client) => {
if (!client.isSignedIn) {
await client.signin({
// Customize the OAuth card text (only renders in OAuth flow, not SSO)
oauthCardText: 'Sign in to your account',
signInButtonText: 'Sign in',
}); // call signin for your auth connection...
return;
}
const me = await client.userGraph.me.get();
log.info(`user "${me.displayName}" already signed in!`);
});
app.event('signin', async (client) => {
const me = await client.userGraph.me.get();
await client.send(`user "${me.displayName}" signed in.`);
await client.send(`Token string length: ${client.token.token.length}`);
});
AI
Action planner
When we created Teams SDK, LLM's didn't natively support tool calling or orchestration. A lot has changed since then, which is why we decided to deprecate ActionPlanner from Teams SDK, and replace it with something a bit more lightweight. Notably, Teams SDK had two similar concepts: functions and actions. In Teams SDK, these are consolidated into functions.
In Teams SDK, there is no actions.json file. Instead, function prompts, parameters, etc. are declared in your code.
import '@azure/openai/types';
import { ChatPrompt, Message } from '@microsoft/teams.ai';
import { MessageActivity } from '@microsoft/teams.api';
import { App } from '@microsoft/teams.apps';
import { LocalStorage } from '@microsoft/teams.common/storage';
import { OpenAIChatModel } from '@microsoft/teams.openai';
interface IStorageState {
status: boolean;
messages: Message[];
}
const storage = new LocalStorage<IStorageState>();
const app = new App();
app.on('message', async (client) => {
let state = storage.get(client.activity.from.id);
if (!state) {
state = {
status: false,
messages: [],
};
storage.set(client.activity.from.id, state);
}
const prompt = new ChatPrompt({
messages: state.messages,
instructions: `The following is a conversation with an AI assistant.
The assistant can turn a light on or off.
The lights are currently off.`,
model: new OpenAIChatModel({
model: 'gpt-4o-mini',
apiKey: process.env.OPENAI_API_KEY,
}),
})
.function('get_light_status', 'get the current light status', () => {
return state.status;
})
.function('toggle_lights', 'toggles the lights on/off', () => {
state.status = !state.status;
storage.set(client.activity.from.id, state);
})
.function(
'pause',
'delays for a period of time',
{
type: 'object',
properties: {
time: {
type: 'number',
description: 'the amount of time to delay in milliseconds',
},
},
required: ['time'],
},
async ({ time }: { time: number }) => {
await new Promise((resolve) => setTimeout(resolve, time));
}
);
await prompt.send(client.activity.text, {
onChunk: (chunk) => {
client.stream.emit(new MessageActivity(chunk));
},
});
});
(async () => {
await app.start();
})();
Supporting feedback
If you supported feedback for AI generated messages, migrating is simple.
# Reply with message including feedback buttons
@app.on_message
async def handle_feedback(ctx: ActivityContext[MessageActivity]):
await ctx.send(MessageActivityInput(text="Hey, give me feedback!").add_ai_generated().add_feedback())
@app.on_message_submit_feedback
async def handle_message_feedback(ctx: ActivityContext[MessageSubmitActionInvokeActivity]):
# Custom logic here..
Note: In Teams SDK, you do not need to opt into feedback at the App level.
import { MessageActivity } from '@microsoft/teams.api';
// Reply with message including feedback buttons
app.on('message', async (client) => {
await client.send(
new MessageActivity('Hey, give me feedback!')
.addAiGenerated() // AI generated label
.addFeedback() // Feedback buttons
);
});
// Listen for feedback submissions
app.on('message.submit.feedback', async ({ activity, log }) => {
// custom logic here...
});
Note: In Teams SDK, you do not need to opt into feedback at the App level.
You can learn more about feedback in Teams SDK in the Feedback guide.
Incrementally migrating code via botbuilder plugin
Note
Comparison code coming soon!
If you aren't ready to migrate all of your code, you can run your existing Teams SDK code in parallel with Teams SDK. Learn more here.