Modifica

Migrating from Teams SDK v1

This article is not available for the selected development language.

Migrating from Teams SDK v1

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 plugin.

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.

pip install 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.

    # highlight-error-start
-    # in api.py
-    from http import HTTPStatus
-
-    from aiohttp import web
-    from botbuilder.core.integration import aiohttp_error_middleware
-
-    from bot import app
-
-    routes = web.RouteTableDef()
-
-    @routes.post("/api/messages")
-    async def on_messages(req: web.Request) -> web.Response:
-        res = await app.process(req)
-        if res is not None:
-            return res
-        return web.Response(status=HTTPStatus.OK)
-
-    # in bot.py
-    import sys
-    import traceback
-
-    from botbuilder.core import TurnContext, MemoryStorage
-    from teams import Application, ApplicationOptions, TeamsAdapter
-    from teams.state import TurnState
-
-    config = Config()
-    storage = MemoryStorage()
-    app = Application[TurnState](
-        ApplicationOptions(
-            bot_app_id=config.APP_ID,
-            adapter=TeamsAdapter(config),
-            storage=storage
-        )
-    )
-
-    @app.activity("message")
-    async def on_message(context: TurnContext, _state: TurnState):
-        await context.send_activity(f"you said: {context.activity.text}")
-        return True
-
-    @app.error
-    async def on_error(context: TurnContext, error: Exception):
-        print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr)
-        traceback.print_exc()
-        await context.send_activity("The bot encountered an error or bug.")
    # highlight-error-end
    # highlight-success-start
+    # 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())
    # highlight-success-end
    ```

---

# [Teams SDK v2](#tab/v2)
```python
    # 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())
    ```

---

# [Teams SDK v1](#tab/v1)
```python
    # in api.py
    from http import HTTPStatus

    from aiohttp import web
    from botbuilder.core.integration import aiohttp_error_middleware

    from bot import app

    routes = web.RouteTableDef()

    @routes.post("/api/messages")
    async def on_messages(req: web.Request) -> web.Response:
        res = await app.process(req)

        if res is not None:
            return res

        return web.Response(status=HTTPStatus.OK)

    api = web.Application(middlewares=[aiohttp_error_middleware])
    api.add_routes(routes)

    # in app.py
    from aiohttp import web

    from api import api
    from config import Config

    if __name__ == "__main__":
        web.run_app(api, host="localhost", port=Config.PORT)

    # in bot.py
    import sys
    import traceback

    from botbuilder.core import TurnContext, MemoryStorage
    from teams import Application, ApplicationOptions, TeamsAdapter
    from teams.state import TurnState

    from config import Config

    config = Config()
    storage = MemoryStorage()
    app = Application[TurnState](
        ApplicationOptions(
            bot_app_id=config.APP_ID,
            adapter=TeamsAdapter(config),
            storage=storage
        )
    )

    @app.activity("message")
    async def on_message(context: TurnContext, _state: TurnState):
        await context.send_activity(f"you said: {context.activity.text}")
        return True

    @app.error
    async def on_error(context: TurnContext, error: Exception):
        # This check writes out errors to console log .vs. app insights.
        # NOTE: In production environment, you should consider logging this to Azure
        #       application insights.
        print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr)
        traceback.print_exc()

        # Send a message to the user
        await context.send_activity("The bot encountered an error or bug.")
    ```
    // highlight-error-start
-    import {
-      ConfigurationServiceClientCredentialFactory,
-      MemoryStorage,
-      TurnContext,
-    } from 'botbuilder';
-    import { Application, TeamsAdapter } from '@microsoft/teams-ai';
-    import * as restify from 'restify';
    // highlight-error-end
    // highlight-success-start
+    import { App } from '@microsoft/teams.apps';
+    import { LocalStorage } from '@microsoft/teams.common/storage';
    // highlight-success-end

    // highlight-error-start

- // Create adapter.
- const adapter = new TeamsAdapter(
-        {},
-        new ConfigurationServiceClientCredentialFactory({
-            MicrosoftAppId: process.env.ENTRA_APP_CLIENT_ID,
-            MicrosoftAppPassword: process.env.ENTRA_APP_CLIENT_SECRET,
-            MicrosoftAppType: 'SingleTenant',
-            MicrosoftAppTenantId: process.env.ENTRA_APP_TENANT_ID
-        })
- );

- // Catch-all for errors.
- const onTurnErrorHandler = async (context: TurnContext, error: any) => {
-        console.error(`\n [onTurnError] unhandled error: ${error}`);
-        // Send a message to the user
-        await context.sendActivity('The bot encountered an error or bug.');
- };

- // Set the onTurnError for the singleton CloudAdapter.
- adapter.onTurnError = onTurnErrorHandler;

- // Create HTTP server.
- const server = restify.createServer();
- server.use(restify.plugins.bodyParser());

- server.listen(process.env.port || process.env.PORT || 3978, () => {
-        console.log(`\n${server.name} listening to ${server.url}`);
- });

- // Define storage and application
- const app = new Application<ApplicationTurnState>({
-        storage: new MemoryStorage()
- });

- // Listen for incoming server requests.
- server.post('/api/messages', async (req, res) => {
-        // Route received a request to adapter for processing
-        await adapter.process(req, res, async (context) => {
-            // Dispatch to application for routing
-            await app.run(context);
-        });
- });
  // highlight-error-end
  // highlight-success-start

* // 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();
* })();
  // highlight-success-end

      ```

---

# [Teams SDK v2](#tab/v2)
```ts
      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();
      })();
      ```

---

# [Teams SDK v1](#tab/v1)
```ts
      import {
        ConfigurationServiceClientCredentialFactory,
        MemoryStorage,
        TurnContext,
      } from 'botbuilder';
      import { Application, TeamsAdapter } from '@microsoft/teams-ai';
      import * as restify from 'restify';

      // Create adapter.
      const adapter = new TeamsAdapter(
          {},
          new ConfigurationServiceClientCredentialFactory({
              MicrosoftAppId: process.env.ENTRA_APP_CLIENT_ID,
              MicrosoftAppPassword: process.env.ENTRA_APP_CLIENT_SECRET,
              MicrosoftAppType: 'SingleTenant',
              MicrosoftAppTenantId: process.env.ENTRA_APP_TENANT_ID
          })
      );

      // Catch-all for errors.
      const onTurnErrorHandler = async (context: TurnContext, error: any) => {
          console.error(`\n [onTurnError] unhandled error: ${error}`);
          // Send a message to the user
          await context.sendActivity('The bot encountered an error or bug.');
      };

      // Set the onTurnError for the singleton CloudAdapter.
      adapter.onTurnError = onTurnErrorHandler;

      // Create HTTP server.
      const server = restify.createServer();
      server.use(restify.plugins.bodyParser());

      server.listen(process.env.port || process.env.PORT || 3978, () => {
          console.log(`\n${server.name} listening to ${server.url}`);
      });

      // Define storage and application
      const app = new Application<ApplicationTurnState>({
          storage: new MemoryStorage()
      });

      // Listen for incoming server requests.
      server.post('/api/messages', async (req, res) => {
          // Route received a request to adapter for processing
          await adapter.process(req, res, async (context) => {
              // Dispatch to application for routing
              await app.run(context);
          });
      });
      ```

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 Activity handlers in Teams SDK v2 work slightly differently than in v1, with a more streamlined event-based approach. between the v1 and v2 versions of our SDK.

Message handlers

    # highlight-error-start
-    # Triggered when user sends "hi"
-    @app.message(re.compile(r"hi", re.IGNORECASE))
-    async def greeting(context: TurnContext, _state: AppTurnState) -> bool:
-        await context.send_activity("Hi there!")
-        return True
-
-    # Listens for ANY message received
-    @app.activity("message")
-    async def on_message(context: TurnContext, _state: TurnState):
-        # Echoes back what user said
-        await context.send_activity(f"you said: {context.activity.text}")
-        return True
    # highlight-error-end
    # highlight-success-start
+    # 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}'")
    # highlight-success-end
    ```

---

# [Teams SDK v2](#tab/v2)
```python
    # 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}'")
    ```

---

# [Teams SDK v1](#tab/v1)
```python
    # Triggered when user sends "hi"
    @app.message(re.compile(r"hi", re.IGNORECASE))
    async def greeting(context: TurnContext, _state: AppTurnState) -> bool:
        await context.send_activity("Hi there!")
        return True

    # Listens for ANY message received
    @app.activity("message")
    async def on_message(context: TurnContext, _state: TurnState):
        # Echoes back what user said
        await context.send_activity(f"you said: {context.activity.text}")
        return True
    ```
    // triggers when user sends "/hi" or "@bot /hi"
    // highlight-error-start
-    app.message("/hi", async (context) => {
-      await context.sendActivity("Hi!");
-    });
    // highlight-error-end
    // highlight-success-start
+    app.message('/hi', async (client) => {
+      // SDK does not auto send typing indicators
+      await client.send({ type: 'typing' });
+      await client.send("Hi!");
+    });
    // highlight-success-end
    // listen for ANY message to be received
    // highlight-error-start
-    app.activity(
-      ActivityTypes.Message,
-      async (context) => {
-        // echo back users request
-        await context.sendActivity(
-          `you said: ${context.activity.text}`
-        );
-      }
-    );
    // highlight-error-end
    // highlight-success-start
+    app.on('message', async (client) => {
+      await client.send({ type: 'typing' });
+      await client.send(
+        `you said "${client.activity.text}"`
+      );
+    });
    // highlight-success-end
    ```

---

# [Teams SDK v2](#tab/v2)
```ts
    // 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}"`
      );
    });
    ```

---

# [Teams SDK v1](#tab/v1)
```ts
    // triggers when user sends "/hi" or "@bot /hi"
    app.message("/hi", async (context) => {
      await context.sendActivity("Hi!");
    });
    // listen for ANY message to be received
    app.activity(
      ActivityTypes.Message,
      async (context) => {
        // echo back users request
        await context.sendActivity(
          `you said: ${context.activity.text}`
        );
      }
    );
    ```

Task modules

Note that on Microsoft Teams, task modules have been renamed to dialogs.

    # highlight-error-start
-    @app.task_module.fetch("connect-account")
-    async def on_connect_account(context: TurnContext, _state: TurnState):
-        return TaskModuleTaskInfo(
-            title="Connect your Microsoft 365 account",
-            height="medium",
-            width="medium",
-            url=f"https://{config.NEXT_PUBLIC_BOT_DOMAIN}/connections",
-            fallbackUrl=f"https://{config.NEXT_PUBLIC_BOT_DOMAIN}/connections",
-            completionBotId=config.NEXT_PUBLIC_BOT_ID,
-        )
-
-    @app.task_modules.submit("connect-account")
-    async def on_submit_connect_account(context: TurnContext, _state: TurnState, data: Dict[str, Any]):
-        print(json.dumps(data))
-        await context.send_activity("You are all set! Now, how can I help you today?")
-        return None
    # highlight-error-end
    # highlight-success-start
+    @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"))
    # highlight-success-end
    ```

---

# [Teams SDK v2](#tab/v2)
```python
    @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"))
    ```

---

# [Teams SDK v1](#tab/v1)
```python
    @app.task_module.fetch("connect-account")
    async def on_connect_account(context: TurnContext, _state: TurnState):
        return TaskModuleTaskInfo(
            title="Connect your Microsoft 365 account",
            height="medium",
            width="medium",
            url=f"https://{config.NEXT_PUBLIC_BOT_DOMAIN}/connections",
            fallbackUrl=f"https://{config.NEXT_PUBLIC_BOT_DOMAIN}/connections",
            completionBotId=config.NEXT_PUBLIC_BOT_ID,
        )

    @app.task_modules.submit("connect-account")
    async def on_submit_connect_account(context: TurnContext, _state: TurnState, data: Dict[str, Any]):
        print(json.dumps(data))
        await context.send_activity("You are all set! Now, how can I help you today?")
        return None
    ```
    // highlight-error-start
-    app.taskModules.fetch('connect-account', async (context, state, data) => {
-      const taskInfo: TaskModuleTaskInfo = {
-        title: 'Connect your Microsoft 365 account',
-        height: 'medium',
-        width: 'medium',
-        url: `https://${process.env.NEXT_PUBLIC_BOT_DOMAIN}/connections`,
-        fallbackUrl: `https://${process.env.NEXT_PUBLIC_BOT_DOMAIN}/connections`,
-        completionBotId: process.env.NEXT_PUBLIC_BOT_ID,
-      };
-      return taskInfo;
-    });
-    app.taskModules.submit('connect-account', async (context, state, data) => {
-      console.log(
-        `bot-app.ts taskModules.submit("connect-account"): data`,
-        JSON.stringify(data, null, 4)
-      );
-      await context.sendActivity('You are all set! Now, how can I help you today?');
-      return undefined;
-    });
    // highlight-error-end
    // highlight-success-start
+    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;
- });
  // highlight-success-end

      ```

---

# [Teams SDK v2](#tab/v2)
```ts
      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;
      });
      ```

---

# [Teams SDK v1](#tab/v1)
```ts
      app.taskModules.fetch('connect-account', async (context, state, data) => {
        const taskInfo: TaskModuleTaskInfo = {
          title: 'Connect your Microsoft 365 account',
          height: 'medium',
          width: 'medium',
          url: `https://${process.env.NEXT_PUBLIC_BOT_DOMAIN}/connections`,
          fallbackUrl: `https://${process.env.NEXT_PUBLIC_BOT_DOMAIN}/connections`,
          completionBotId: process.env.NEXT_PUBLIC_BOT_ID,
        };
        return taskInfo;
      });
      app.taskModules.submit('connect-account', async (context, state, data) => {
        console.log(
          `bot-app.ts taskModules.submit("connect-account"): data`,
          JSON.stringify(data, null, 4)
        );
        await context.sendActivity('You are all set! Now, how can I help you today?');
        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.

    # highlight-error-start
-    @app.message("/card")
-    async def adaptive_card(context: TurnContext, _state: AppTurnState) -> bool:
-        attachment = CardFactory.adaptive_card(
-             {
-                "$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 context.send_activity(Activity(attachments=[attachment]))
-        return True
    # highlight-error-end
    # highlight-success-start
+    @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)
    # highlight-success-end
    ```

---

# [Teams SDK v2 (Option 1)](#tab/v2-option1)
For existing cards like this, the simplest way to convert that to Teams SDK is this:

    ```python
    @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)
    ```

---

# [Teams SDK v2 (Option 2)](#tab/v2-option2)
For a more thorough port, you could also do the following:

    ```python
    @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)
    ```

---

# [Teams SDK v1](#tab/v1)
```python
    @app.message("/card")
    async def adaptive_card(context: TurnContext, _state: AppTurnState) -> bool:
        attachment = CardFactory.adaptive_card(
             {
                "$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 context.send_activity(Activity(attachments=[attachment]))
        return True
    ```
    // highlight-error-start
-    app.message('/card', async (context: TurnContext) => {
-      const card = CardFactory.adaptiveCard({
-        $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',
-        },
-      });
-      await context.sendActivity({
-        attachments: [card],
-      });
-    });
    // highlight-error-end
    // highlight-success-start
+    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',
+        },
+      });
+    });
    // highlight-success-end
    ```

---

# [Teams SDK v2 (Option 1)](#tab/v2-option1)
For existing cards like this, the simplest way to convert that to Teams SDK is this:

    ```ts
    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',
        },
      });
    });
    ```

---

# [Teams SDK v2 (Option 2)](#tab/v2-option2)
For a more thorough port, you could also do the following:

    ```ts
    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',
        })
      );
    });
    ```

---

# [Teams SDK v1](#tab/v1)
```ts
    app.message('/card', async (context: TurnContext) => {
      const card = CardFactory.adaptiveCard({
        $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',
        },
      });
      await context.sendActivity({
        attachments: [card],
      });
    });
    ```

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.

    # highlight-error-start
-    app = Application[TurnState[ConversationState, UserState, TempState]](
-        ApplicationOptions(
-            bot_app_id=config.APP_ID,
-            storage=MemoryStorage(),
-            adapter=TeamsAdapter(config),
-            auth=AuthOptions(
-                default="graph",
-                auto=True,
-                settings={
-                    "graph": OAuthOptions(
-                        connection_name=config.OAUTH_CONNECTION_NAME,
-                        title="Sign In",
-                        text="please sign in",
-                        end_on_invalid_message=True,
-                        enable_sso=True,
-                    ),
-                },
-            ),
-        )
-    )
-
-    auth = app.auth.get("graph")
-
-    @app.message("/signout")
-    async def on_sign_out(
-        context: TurnContext, state: TurnState[ConversationState, UserState, TempState]
-    ):
-        await auth.sign_out(context, state)
-        await context.send_activity("you are now signed out...👋")
-        return False
-
-    @auth.on_sign_in_success
-    async def on_sign_in_success(
-        context: TurnContext, state: TurnState[ConversationState, UserState, TempState]
-    ):
-        await context.send_activity("successfully logged in!")
-        await context.send_activity(f"token: {state.temp.auth_tokens['graph']}")
-
-    @auth.on_sign_in_failure
-    async def on_sign_in_failure(
-        context: TurnContext,
-        _state: TurnState[ConversationState, UserState, TempState],
-        _res: SignInResponse,
-    ):
-        await context.send_activity("failed to login...")
    # highlight-error-end
    # highlight-success-start
+    app = App()
+    graph = app.add_oauth_flow("graph")
+
+    @app.on_message
+    async def handle_message(ctx: ActivityContext[MessageActivity]):
+        ctx.logger.info("User requested sign-in.")
+        if await graph.is_signed_in(ctx):
+            await ctx.send("You are already signed in.")
+        else:
+            await graph.sign_in(ctx)
+
+    @app.on_message_pattern("/signout")
+    async def handle_sign_out(ctx: ActivityContext[MessageActivity]):
+        await graph.sign_out(ctx)
+        await ctx.send("You have been signed out.")
+
+    @graph.on_signin
+    async def handle_sign_in(event: SignInEvent):
+        """Handle sign-in for the graph connection."""
+        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}")
    # highlight-success-end
    ```

---

# [Teams SDK v2](#tab/v2)
```python
    app = App()
    graph = app.add_oauth_flow("graph")

    @app.on_message
    async def handle_message(ctx: ActivityContext[MessageActivity]):
        ctx.logger.info("User requested sign-in.")
        if await graph.is_signed_in(ctx):
            await ctx.send("You are already signed in.")
        else:
            await graph.sign_in(ctx)

    @app.on_message_pattern("/signout")
    async def handle_sign_out(ctx: ActivityContext[MessageActivity]):
        await graph.sign_out(ctx)
        await ctx.send("You have been signed out.")

    @graph.on_signin
    async def handle_sign_in(event: SignInEvent):
        """Handle sign-in for the graph connection."""
        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}")
    ```

---

# [Teams SDK v1](#tab/v1)
```python
    app = Application[TurnState[ConversationState, UserState, TempState]](
        ApplicationOptions(
            bot_app_id=config.APP_ID,
            storage=MemoryStorage(),
            adapter=TeamsAdapter(config),
            auth=AuthOptions(
                default="graph",
                auto=True,
                settings={
                    "graph": OAuthOptions(
                        connection_name=config.OAUTH_CONNECTION_NAME,
                        title="Sign In",
                        text="please sign in",
                        end_on_invalid_message=True,
                        enable_sso=True,
                    ),
                },
            ),
        )
    )

    auth = app.auth.get("graph")

    @app.message("/signout")
    async def on_sign_out(
        context: TurnContext, state: TurnState[ConversationState, UserState, TempState]
    ):
        await auth.sign_out(context, state)
        await context.send_activity("you are now signed out...👋")
        return False

    @auth.on_sign_in_success
    async def on_sign_in_success(
        context: TurnContext, state: TurnState[ConversationState, UserState, TempState]
    ):
        await context.send_activity("successfully logged in!")
        await context.send_activity(f"token: {state.temp.auth_tokens['graph']}")

    @auth.on_sign_in_failure
    async def on_sign_in_failure(
        context: TurnContext,
        _state: TurnState[ConversationState, UserState, TempState],
        _res: SignInResponse,
    ):
        await context.send_activity("failed to login...")
    ```
    // highlight-error-start
-    const storage = new MemoryStorage();
-    const app = new Application({
-      storage: new MemoryStorage(),
-      authentication: {
-        autoSignIn: (context) => {
-          const activity = context.activity;
-          // No auth when user wants to sign in
-          if (activity.text === '/signout') {
-            return Promise.resolve(false);
-          }
-          // No auth for "/help"
-          if (activity.text === '/help') {
-            return Promise.resolve(false);
-          }
-          // Manually sign in (for illustrative purposes)
-          if (activity.text === '/signin') {
-            return Promise.resolve(false);
-          }
-          // For all other messages, require sign in
-          return Promise.resolve(true);
-        },
-        settings: {
-          graph: {
-            connectionName: process.env.OAUTH_CONNECTION_NAME!,
-            title: 'Sign in',
-            text: 'Please sign in to use the bot.',
-            endOnInvalidMessage: true,
-            tokenExchangeUri: process.env.TOKEN_EXCHANGE_URI!,
-            enableSso: true,
-          },
-        },
-      },
-    });
-
-    app.message('/signout', async (context, state) => {
-      await app.authentication.signOutUser(context, state);
-      await context.sendActivity(`You have signed out`);
-    });
-
-    app.message('/help', async (context, state) => {
-      await context.sendActivity(`your help text`);
-    });
-
-    app.authentication.get('graph').onUserSignInSuccess(async (context, state) => {
-      await context.sendActivity('Successfully logged in');
-      await context.sendActivity(`Token string length: ${state.temp.authTokens['graph']!.length}`);
-    });
    // highlight-error-end
    // highlight-success-start
+    const app = new App({
+      logger: new ConsoleLogger('@tests/auth', { level: 'debug' }),
+    });
+
+    const graph = app.addOAuthFlow('graph');
+
+    app.message('/signout', async (ctx) => {
+      if (!(await graph.isSignedIn(ctx))) return;
+      await graph.signOut(ctx);
+      await ctx.send('you have been signed out!');
+    });
+
+    app.message('/help', async ({ send }) => {
+      await send('your help text');
+    });
+
+    app.on('message', async (ctx) => {
+      const token = await graph.signIn(ctx, {
+        oauthCardText: 'Sign in to your account',
+        signInButtonText: 'Sign in',
+      });
+      if (!token) return;
+      ctx.log.info('user already signed in!');
+    });
+
+    graph.onSignInComplete(async (ctx, token) => {
+      await ctx.send(`signed in. Token string length: ${token.token.length}`);
+    });
    // highlight-success-end
    ```

---

# [Teams SDK v2](#tab/v2)
```ts
    const app = new App({
      logger: new ConsoleLogger('@tests/auth', { level: 'debug' }),
    });

    /**
     * Register the auth connection.
     * The name should match the OAuth connection name defined in the Azure Bot configuration.
     */
    const graph = app.addOAuthFlow('graph');

    app.message('/signout', async (ctx) => {
      if (!(await graph.isSignedIn(ctx))) return;
      await graph.signOut(ctx); // call signOut for this auth connection...
      await ctx.send('you have been signed out!');
    });

    app.message('/help', async ({ send }) => {
      await send('your help text');
    });

    app.on('message', async (ctx) => {
      const token = await graph.signIn(ctx, {
        // Customize the OAuth card text (only renders in OAuth flow, not SSO)
        oauthCardText: 'Sign in to your account',
        signInButtonText: 'Sign in',
      });

      // An OAuth card was sent — the flow resumes on the callback turn.
      if (!token) return;

      ctx.log.info('user already signed in!');
    });

    graph.onSignInComplete(async (ctx, token) => {
      await ctx.send(`signed in. Token string length: ${token.token.length}`);
    });
    ```

---

# [Teams SDK v1](#tab/v1)
```ts
    const storage = new MemoryStorage();
    const app = new Application({
      storage: new MemoryStorage(),
      authentication: {
        autoSignIn: (context) => {
          const activity = context.activity;
          // No auth when user wants to sign in
          if (activity.text === '/signout') {
            return Promise.resolve(false);
          }
          // No auth for "/help"
          if (activity.text === '/help') {
            return Promise.resolve(false);
          }
          // Manually sign in (for illustrative purposes)
          if (activity.text === '/signin') {
            return Promise.resolve(false);
          }
          // For all other messages, require sign in
          return Promise.resolve(true);
        },
        settings: {
          graph: {
            connectionName: process.env.OAUTH_CONNECTION_NAME!,
            title: 'Sign in',
            text: 'Please sign in to use the bot.',
            endOnInvalidMessage: true,
            tokenExchangeUri: process.env.TOKEN_EXCHANGE_URI!,
            enableSso: true,
          },
        },
      },
    });

    app.message('/signout', async (context, state) => {
      await app.authentication.signOutUser(context, state);

      // Echo back users request
      await context.sendActivity(`You have signed out`);
    });

    app.message('/signin', async (context, state) => {
      let token = state.temp.authTokens['graph'];
      if (!token) {
        const res = await app.authentication.signInUser(context, state);
        if (res.error) {
          console.log(res.error);
          return;
        }
        token = state.temp.authTokens['graph'];
      }

      if (token) {
        // Echo back users request
        await context.sendActivity(`You are already authenticated!`);
        return;
      }

      // Sign in is pending...
    });

    app.message('/help', async (context, state) => {
      await context.sendActivity(`your help text`);
    });

    app.authentication.get('graph').onUserSignInSuccess(async (context, state) => {
      // Successfully logged in
      await context.sendActivity('Successfully logged in');
      await context.sendActivity(`Token string length: ${state.temp.authTokens['graph']!.length}`);
    });

    app.authentication.get('graph').onUserSignInFailure(async (context, _state, error) => {
      // Failed to login
      await context.sendActivity(`Failed to login with error: ${error.message}`);
    });
    ```

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.

    # highlight-error-start
-    # Create AI components
-    model = OpenAIModel(
-        OpenAIModelOptions(api_key=config.OPENAI_KEY, default_model="gpt-4o")
-    )
-
-    prompts = PromptManager(
-        PromptManagerOptions(prompts_folder=f"{os.path.dirname(__file__)}/prompts")
-    )
-
-    # Define a prompt function for getting the current status of the lights
-    @prompts.function("get_light_status")
-    async def on_get_light_status(context, state, functions, tokenizer, args):
-        return "on" if state.get("conversation.lightsOn") else "off"
-
-    planner = ActionPlanner(
-        ActionPlannerOptions(model=model, prompts=prompts, default_prompt="tools")
-    )
-
-    # Define storage and application
-    storage = MemoryStorage()
-    app = Application[AppTurnState](
-        ApplicationOptions(
-            bot_app_id=config.APP_ID,
-            storage=storage,
-            adapter=TeamsAdapter(config),
-            ai=AIOptions(planner=planner),
-        )
-    )
-
-    # Register action handlers
-    @app.ai.action("LightsOn")
-    async def on_lights_on(context: ActionTurnContext, state: AppTurnState):
-        state.conversation.lights_on = True
-        return "the lights are now on"
-
-    @app.ai.action("LightsOff")
-    async def on_lights_off(context: ActionTurnContext, state: AppTurnState):
-        state.conversation.lights_on = False
-        return "the lights are now off"
-
-    @app.ai.action("Pause")
-    async def on_pause(context: ActionTurnContext, state: AppTurnState):
-        time_ms = int(context.data["time"]) if context.data["time"] else 1000
-        time.sleep(time_ms / 1000)
-        return "done pausing"
    # highlight-error-end
    # highlight-success-start
+    import asyncio
+    from microsoft_teams.ai import ChatPrompt
+    from microsoft_teams.api import MessageActivity
+    from microsoft_teams.apps import ActivityContext, App
+    from microsoft_teams.common import LocalStorage
+    from microsoft_teams.openai import OpenAICompletionsAIModel
+
+    storage = LocalStorage()
+    app = App()
+
+    @app.on_message
+    async def handle_message(ctx: ActivityContext[MessageActivity]):
+        state = storage.get(ctx.activity.from_.id) or {"lights_on": False}
+
+        prompt = ChatPrompt(
+            instructions="You are a helpful assistant that can control lights.",
+            model=OpenAICompletionsAIModel(model="gpt-4o"),
+        )
+
+        # Define functions inline
+        prompt.function(
+            "get_light_status",
+            "Gets the current status of the lights",
+            lambda: "on" if state["lights_on"] else "off"
+        )
+
+        prompt.function(
+            "lights_on",
+            "Turns the lights on",
+            lambda: (state.update({"lights_on": True}), "the lights are now on")[1]
+        )
+
+        prompt.function(
+            "lights_off",
+            "Turns the lights off",
+            lambda: (state.update({"lights_on": False}), "the lights are now off")[1]
+        )
+
+        async def pause(time: int):
+            await asyncio.sleep(time / 1000)
+            return "done pausing"
+
+        prompt.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"]
+            },
+            pause
+        )
+
+        result = await prompt.send(ctx.activity.text)
+        storage.set(ctx.activity.from_.id, state)
+
+        if result.response.content:
+            await ctx.send(result.response.content)
    # highlight-success-end
    ```

---

# [Teams SDK v2](#tab/v2)
In Teams SDK, there is no `actions.json` file. Instead, function prompts, parameters, etc. are declared in your code.

    ```python
    import asyncio
    from microsoft_teams.ai import ChatPrompt
    from microsoft_teams.api import MessageActivity
    from microsoft_teams.apps import ActivityContext, App
    from microsoft_teams.common import LocalStorage
    from microsoft_teams.openai import OpenAICompletionsAIModel

    storage = LocalStorage()
    app = App()

    @app.on_message
    async def handle_message(ctx: ActivityContext[MessageActivity]):
        state = storage.get(ctx.activity.from_.id) or {"lights_on": False}

        prompt = ChatPrompt(
            instructions="You are a helpful assistant that can control lights.",
            model=OpenAICompletionsAIModel(model="gpt-4o"),
        )

        # Define functions inline - no separate actions.json needed
        prompt.function(
            "get_light_status",
            "Gets the current status of the lights",
            lambda: "on" if state["lights_on"] else "off"
        )

        prompt.function(
            "lights_on",
            "Turns the lights on",
            lambda: (state.update({"lights_on": True}), "the lights are now on")[1]
        )

        prompt.function(
            "lights_off",
            "Turns the lights off",
            lambda: (state.update({"lights_on": False}), "the lights are now off")[1]
        )

        async def pause(time: int):
            await asyncio.sleep(time / 1000)
            return "done pausing"

        prompt.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"]
            },
            pause
        )

        result = await prompt.send(ctx.activity.text)
        storage.set(ctx.activity.from_.id, state)

        if result.response.content:
            await ctx.send(result.response.content)
    ```

---

# [Teams SDK v1](#tab/v1)
```python
    # Create AI components
    model = OpenAIModel(
        OpenAIModelOptions(api_key=config.OPENAI_KEY, default_model="gpt-4o")
    )

    prompts = PromptManager(
        PromptManagerOptions(prompts_folder=f"{os.path.dirname(__file__)}/prompts")
    )

    # Define a prompt function for getting the current status of the lights
    @prompts.function("get_light_status")
    async def on_get_light_status(context, state, functions, tokenizer, args):
        return "on" if state.get("conversation.lightsOn") else "off"

    planner = ActionPlanner(
        ActionPlannerOptions(model=model, prompts=prompts, default_prompt="tools")
    )

    # Define storage and application
    storage = MemoryStorage()
    app = Application[AppTurnState](
        ApplicationOptions(
            bot_app_id=config.APP_ID,
            storage=storage,
            adapter=TeamsAdapter(config),
            ai=AIOptions(planner=planner),
        )
    )

    # Register action handlers
    @app.ai.action("LightsOn")
    async def on_lights_on(context: ActionTurnContext, state: AppTurnState):
        state.conversation.lights_on = True
        return "the lights are now on"

    @app.ai.action("LightsOff")
    async def on_lights_off(context: ActionTurnContext, state: AppTurnState):
        state.conversation.lights_on = False
        return "the lights are now off"

    @app.ai.action("Pause")
    async def on_pause(context: ActionTurnContext, state: AppTurnState):
        time_ms = int(context.data["time"]) if context.data["time"] else 1000
        time.sleep(time_ms / 1000)
        return "done pausing"

    @app.ai.action("LightStatus")
    async def on_lights_status(context: ActionTurnContext, state: AppTurnState):
        return "the lights are on" if state.conversation.lights_on else "the lights are off"
    ```

    And the corresponding `actions.json` file:

    ```json
    [
        {
            "name": "LightsOn",
            "description": "Turns on the lights"
        },
        {
            "name": "LightsOff",
            "description": "Turns off the lights"
        },
        {
            "name": "Pause",
            "description": "Delays for a period of time",
            "parameters": {
                "type": "object",
                "properties": {
                    "time": {
                        "type": "number",
                        "description": "The amount of time to delay in milliseconds"
                    }
                },
                "required": ["time"]
            }
        },
        {
            "name": "LightStatus",
            "description": "Gets the lights status"
        }
    ]
    ```

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.

    // highlight-error-start
-    // Create AI components
-    const model = new OpenAIModel({
-      apiKey: process.env.OPENAI_KEY!,
-      defaultModel: 'gpt-4o',
-      logRequests: true,
-    });
-
-    const prompts = new PromptManager({
-      promptsFolder: path.join(__dirname, '../src/prompts'),
-    });
-
-    // Define a prompt function for getting the current status of the lights
-    prompts.addFunction('getLightStatus', async (context, memory) => {
-      return memory.getValue('conversation.lightsOn') ? 'on' : 'off';
-    });
-
-    const planner = new ActionPlanner({
-      model,
-      prompts,
-      defaultPrompt: 'tools',
-    });
-
-    // Define storage and application
-    const storage = new MemoryStorage();
-    const app = new Application<ApplicationTurnState>({
-      storage,
-      ai: {
-        planner,
-      },
-    });
-
-    // Register action handlers
-    app.ai.action('ToggleLights', async (context, state) => {
-      state.conversation.lightsOn = !state.conversation.lightsOn;
-      const lightStatusText = state.conversation.lightsOn ? 'on' : 'off';
-      await context.sendActivity(`[lights ${lightStatusText}]`);
-      return `the lights are now ${lightStatusText}$`;
-    });
-
-    app.ai.action('Pause', async (context, state, parameters: PauseParameters) => {
-      await context.sendActivity(`[pausing for ${parameters.time / 1000} seconds]`);
-      await new Promise((resolve) => setTimeout(resolve, parameters.time));
-      return `done pausing`;
-    });
    // highlight-error-end
    // highlight-success-start
+    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 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 MessageActivityInput(chunk));
+        },
+      });
+    });
    // highlight-success-end
    ```

---

# [Teams SDK v2](#tab/v2)
In Teams SDK, there is no `actions.json` file. Instead, function prompts, parameters, etc. are declared in your code.

    ```ts
    import '@azure/openai/types';
    import { ChatPrompt, Message } from '@microsoft/teams.ai';
    import { MessageActivityInput } 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 MessageActivityInput(chunk));
        },
      });
    });

    (async () => {
      await app.start();
    })();
    ```

---

# [Teams SDK v1](#tab/v1)
```ts
    // Create AI components
    const model = new OpenAIModel({
      // OpenAI Support
      apiKey: process.env.OPENAI_KEY!,
      defaultModel: 'gpt-4o',

      // Azure OpenAI Support
      azureApiKey: process.env.AZURE_OPENAI_KEY!,
      azureDefaultDeployment: 'gpt-4o',
      azureEndpoint: process.env.AZURE_OPENAI_ENDPOINT!,
      azureApiVersion: '2023-03-15-preview',

      // Request logging
      logRequests: true,
    });

    const prompts = new PromptManager({
      promptsFolder: path.join(__dirname, '../src/prompts'),
    });

    // Define a prompt function for getting the current status of the lights
    prompts.addFunction('getLightStatus', async (context, memory) => {
      return memory.getValue('conversation.lightsOn') ? 'on' : 'off';
    });

    const planner = new ActionPlanner({
      model,
      prompts,
      defaultPrompt: 'tools',
    });

    // Define storage and application
    const storage = new MemoryStorage();
    const app = new Application<ApplicationTurnState>({
      storage,
      ai: {
        planner,
      },
    });

    // Register action handlers
    app.ai.action('ToggleLights', async (context, state) => {
      state.conversation.lightsOn = !state.conversation.lightsOn;
      const lightStatusText = state.conversation.lightsOn ? 'on' : 'off';
      await context.sendActivity(`[lights ${lightStatusText}]`);
      return `the lights are now ${lightStatusText}$`;
    });

    interface PauseParameters {
      time: number;
    }

    app.ai.action('Pause', async (context, state, parameters: PauseParameters) => {
      await context.sendActivity(`[pausing for ${parameters.time / 1000} seconds]`);
      await new Promise((resolve) => setTimeout(resolve, parameters.time));
      return `done pausing`;
    });

    // Listen for incoming server requests.
    server.post('/api/messages', async (req, res) => {
      // Route received a request to adapter for processing
      await adapter.process(req, res as any, async (context) => {
        // Dispatch to application for routing
        await app.run(context);
      });
    });
    ```

    And the corresponding `actions.json` file:

    ```json
    [
      {
        "name": "ToggleLights",
        "description": "Turns on/off the lights"
      },
      {
        "name": "Pause",
        "description": "Delays for a period of time",
        "parameters": {
          "type": "object",
          "properties": {
            "time": {
              "type": "number",
              "description": "The amount of time to delay in milliseconds"
            }
          },
          "required": ["time"]
        }
      }
    ]
    ```

Migration Feedback

If you supported feedback for AI generated messages, migrating is simple.

    # highlight-error-start
-    app = Application[AppTurnState](
-        ApplicationOptions(
-            # ... other options
-            ai=AIOptions(
-                enable_feedback_loop=enableFeedbackLoop
-            ),
-        )
-    )
-
-    @app.message()
-    async def on_message(context: TurnContext, state: AppTurnState):
-        await context.send_activity(Activity(text="Hey, give me feedback!", channel_data={"feedbackLoop": { "type": "custom"}}))
-
-    @app.feedback_loop()
-    async def feedback_loop(context: TurnContext, state: AppTurnState, feedback_data: FeedbackLoopData):
-        print("Feedback loop triggered")
    # highlight-error-end
    # highlight-success-start
+    # 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..
    # highlight-success-end
    ```

---

# [Teams SDK v2](#tab/v2)
```python
    # 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.

---

# [Teams SDK v1](#tab/v1)
```python
    app = Application[AppTurnState](
        ApplicationOptions(
            # ... other options
            ai=AIOptions(
                enable_feedback_loop=enableFeedbackLoop
            ),
        )
    )

    @app.message()
    async def on_message(context: TurnContext, state: AppTurnState):
        await context.send_activity(Activity(text="Hey, give me feedback!", channel_data={"feedbackLoop": { "type": "custom"}}))

    @app.feedback_loop()
    async def feedback_loop(context: TurnContext, state: AppTurnState, feedback_data: FeedbackLoopData):
        print("Feedback loop triggered")
    ```
    // highlight-error-start
-    export const app = new Application({
-      ai: {
-        // opts into feedback loop
-        enable_feedback_loop: true,
-      },
-    });
-
-    // Reply with message including feedback buttons
-    app.activity(ActivityTypes.Message, async (context) => {
-      await context.sendActivity({
-        type: ActivityTypes.Message,
-        text: `Hey, give me feedback!`,
-        channelData: {
-          feedbackLoop: {
-            type: 'custom',
-          },
-        },
-      });
-    });
-
-    // Handle feedback submit
-    app.feedbackLoop(async (context, state, feedbackLoopData) => {
-      // custom logic here...
-    });
    // highlight-error-end
    // highlight-success-start
+    // Reply with message including feedback buttons
+    app.on('message', async (client) => {
+      await client.send(
+        new MessageActivityInput('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...
+    });
    // highlight-success-end
    ```

---

# [Teams SDK v2](#tab/v2)
```ts
    import { MessageActivityInput } from '@microsoft/teams.api';

    // Reply with message including feedback buttons
    app.on('message', async (client) => {
      await client.send(
        new MessageActivityInput('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.

---

# [Teams SDK v1](#tab/v1)
```ts
    export const app = new Application({
      ai: {
        // opts into feedback loop
        enable_feedback_loop: true,
      },
    });

    // Reply with message including feedback buttons
    app.activity(ActivityTypes.Message, async (context) => {
      await context.sendActivity({
        type: ActivityTypes.Message,
        text: `Hey, give me feedback!`,
        channelData: {
          feedbackLoop: {
            type: 'custom',
          },
        },
      });
    });

    // Handle feedback submit
    app.feedbackLoop(async (context, state, feedbackLoopData) => {
      // custom logic here...
    });
    ```

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.