diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 000000000..4ccfae130
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,91 @@
+# Lines starting with '#' are comments.
+# Each line is a file pattern followed by one or more owners.
+
+# More details are here: https://help.github.com/articles/about-codeowners/
+
+# The '*' pattern is global owners.
+
+# Order is important. The last matching pattern has the most precedence.
+# The folders are ordered as follows:
+
+# In each subsection folders are ordered first by depth, then alphabetically.
+# This should make it easy to add new rules without breaking existing ones.
+
+# Global rule:
+* @microsoft/bb-python
+
+# Functional tests
+/libraries/functional-tests/**                                                      @tracyboehrer
+
+# Adapters
+/libraries/botbuilder-adapters-slack/**                                             @tracyboehrer @garypretty
+
+# Platform Integration Libaries (aiohttp)
+/libraries/botbuilder-integration-aiohttp/**                                        @microsoft/bb-python-integration
+/libraries/botbuilder-integration-applicationinsights-aiohttp/**                    @microsoft/bb-python-integration @garypretty
+
+# Application Insights/Telemetry
+/libraries/botbuilder-applicationinsights/**                                        @axelsrz @garypretty
+
+# AI: LUIS + QnA Maker
+/libraries/botbuilder-ai/**                                                         @microsoft/bf-cog-services
+
+# Azure (Storage)
+/libraries/botbuilder-azure/**                                                      @tracyboehrer @EricDahlvang
+
+# Adaptive Dialogs
+/libraries/botbuilder-dialogs-*/**                                                  @tracyboehrer @microsoft/bf-adaptive
+
+# AdaptiveExpressions & LanguageGeneration libraries
+/libraries/adaptive-expressions/**                                                  @axelsrz @microsoft/bf-adaptive
+/libraries/botbuilder-lg/**                                                         @axelsrz @microsoft/bf-adaptive
+
+# BotBuilder Testing
+/libraries/botbuilder-testing/**                                                    @axelsrz @gabog
+
+# Streaming library
+/libraries/botbuilder-streaming/**                                                  @microsoft/bf-streaming
+
+# BotBuilder library
+/libraries/botbuilder-core/**                                                       @axelsrz @gabog @johnataylor
+
+# BotBuilder Dialogs
+/libraries/botbuilder-dialogs/**                                                    @microsoft/bf-dialogs
+
+# Swagger
+/libraries/swagger/**                                                               @axelsrz @EricDahlvang
+
+# Bot Framework Schema
+/libraries/botbuilder-schema/**                                                     @EricDahlvang @johnataylor
+
+# Bot Framework connector
+libraries\botframework-connector/**                                                 @axelsrz @carlosscastro @johnataylor
+
+# Bot Framework Authentication
+/libraries/botbuilder-core/botbuilder/core/oauth/**                                 @microsoft/bf-auth
+/libraries/botframework-connector/botframework/connector/auth/**                    @microsoft/bf-auth
+
+# Bot Framework Skills
+/libraries/botbuilder-core/botbuilder/core/skills/**                                @microsoft/bf-skills
+/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/skills/**  @microsoft/bf-skills
+/tests/skills/**                                                                    @microsoft/bf-skills
+
+# Bot Framework & Microsoft Teams
+/libraries/botbuilder-core/botbuilder/core/teams/**                                 @microsoft/bf-teams
+/libraries/botbuilder-schema/botbuilder/schema/teams/**                             @microsoft/bf-teams
+/tests/teams/**                                                                     @microsoft/bf-teams
+
+# Ownership by specific files or file types
+# This section MUST stay at the bottom of the CODEOWNERS file. For more information, see
+# https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#example-of-a-codeowners-file
+
+# Shipped package files
+# e.g. READMEs, requirements.txt, setup.py, MANIFEST.in
+/libraries/**/README.rst                                                            @microsoft/bb-python
+/libraries/**/requirements.txt                                                      @microsoft/bb-python
+/libraries/**/setup.py                                                              @microsoft/bb-python
+/libraries/**/setup.cfg                                                             @microsoft/bb-python
+/libraries/**/MANIFEST.in                                                           @microsoft/bb-python
+
+# CODEOWNERS
+/.github/CODEOWNERS                                                                 @stevengum @cleemullins @microsoft/bb-python
diff --git a/.github/ISSUE_TEMPLATE/python-sdk-bug.md b/.github/ISSUE_TEMPLATE/python-sdk-bug.md
index 3fd6037d9..435fe4310 100644
--- a/.github/ISSUE_TEMPLATE/python-sdk-bug.md
+++ b/.github/ISSUE_TEMPLATE/python-sdk-bug.md
@@ -1,9 +1,13 @@
 ---
 name: Python SDK Bug
 about: Create a bug report for a bug you found in the Bot Builder Python SDK
-
+title: ""
+labels: "needs-triage, bug"
+assignees: ""
 ---
 
+### [Github issues](https://github.com/Microsoft/botbuilder-python) should be used for bugs and feature requests. Use [Stack Overflow](https://stackoverflow.com/questions/tagged/botframework) for general "how-to" questions. 
+
 ## Version
 What package version of the SDK are you using.
 
@@ -25,5 +29,3 @@ If applicable, add screenshots to help explain your problem.
 
 ## Additional context
 Add any other context about the problem here.
-
-[bug]
diff --git a/.github/ISSUE_TEMPLATE/python-sdk-feature-request.md b/.github/ISSUE_TEMPLATE/python-sdk-feature-request.md
index e3f0aad0e..d498599d9 100644
--- a/.github/ISSUE_TEMPLATE/python-sdk-feature-request.md
+++ b/.github/ISSUE_TEMPLATE/python-sdk-feature-request.md
@@ -1,9 +1,13 @@
 ---
 name: Python SDK Feature Request
 about: Suggest a feature for the Bot Builder Python SDK
-
+title: ""
+labels: "needs-triage, feature-request"
+assignees: ""
 ---
 
+### Use this [query](https://github.com/Microsoft/botbuilder-python/issues?q=is%3Aissue+is%3Aopen++label%3Afeature-request+) to search for the most popular feature requests.
+
 **Is your feature request related to a problem? Please describe.**
 A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
 
@@ -15,5 +19,3 @@ A clear and concise description of any alternative solutions or features you've
 
 **Additional context**
 Add any other context or screenshots about the feature request here.
-
-[enhancement]
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 000000000..e8870cd7e
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,14 @@
+Fixes #
+
+## Description
+
+
+## Specific Changes
+
+
+  - 
+  -
+  -
+
+## Testing
+
\ No newline at end of file
diff --git a/.github/workflows/create-parity-issue.yml b/.github/workflows/create-parity-issue.yml
new file mode 100644
index 000000000..51f47a190
--- /dev/null
+++ b/.github/workflows/create-parity-issue.yml
@@ -0,0 +1,43 @@
+name: create-parity-issue.yml
+
+on:
+  workflow_dispatch:
+    inputs:
+      prDescription:
+        description: PR description
+        default: 'No description provided'
+        required: true
+      prNumber:
+        description: PR number
+        required: true
+      prTitle:
+        description: PR title
+        required: true
+      sourceRepo:
+        description: repository PR is sourced from
+        required: true
+
+jobs:
+  createIssue:
+    name: create issue
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v2
+
+      - uses: joshgummersall/create-issue@main
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        with:
+          title: |
+            port: ${{ github.event.inputs.prTitle }} (#${{ github.event.inputs.prNumber }})
+          labels: |
+            ["parity", "needs-triage", "ExemptFromDailyDRIReport"]
+          body: |
+             The changes in [${{ github.event.inputs.prTitle }} (#${{ github.event.inputs.prNumber }})](https://github.com/${{ github.event.inputs.sourceRepo }}/pull/${{ github.event.inputs.prNumber }}) may need to be ported to maintain parity with `${{ github.event.inputs.sourceRepo }}`.
+
+             
+             ${{ github.event.inputs.prDescription }}
+             
+
+             Please review and, if necessary, port the changes.
diff --git a/.pylintrc b/.pylintrc
index 40a38eff1..a134068ff 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -7,7 +7,7 @@ extension-pkg-whitelist=
 
 # Add files or directories to the blacklist. They should be base names, not
 # paths.
-ignore=CVS,build,botbuilder-schema,samples,django_tests,Generator,operations,operations_async
+ignore=CVS,build,botbuilder-schema,samples,django_tests,Generator,operations,operations_async,schema
 
 # Add files or directories matching the regex patterns to the blacklist. The
 # regex matches against base names, not paths.
@@ -157,7 +157,8 @@ disable=print-statement,
         too-many-function-args,
         too-many-return-statements,
         import-error,
-        no-name-in-module
+        no-name-in-module,
+        too-many-branches
 
 # Enable the message, report, category or checker with the given id(s). You can
 # either give multiple identifier separated by comma (,) or put this option
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 000000000..d3ff17639
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,5 @@
+## Code of Conduct
+
+This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 
+For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact
+ [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
\ No newline at end of file
diff --git a/Contributing.md b/Contributing.md
new file mode 100644
index 000000000..41a2e6153
--- /dev/null
+++ b/Contributing.md
@@ -0,0 +1,23 @@
+# Instructions for Contributing Code
+
+## Contributing bug fixes and features
+
+The Bot Framework team is currently accepting contributions in the form of bug fixes and new 
+features. Any submission must have an issue tracking it in the issue tracker that has
+ been approved by the Bot Framework team. Your pull request should include a link to 
+ the bug that you are fixing. If you've submitted a PR for a bug, please post a 
+ comment in the bug to avoid duplication of effort.
+
+## Legal
+
+If your contribution is more than 15 lines of code, you will need to complete a Contributor 
+License Agreement (CLA). Briefly, this agreement testifies that you are granting us permission
+ to use the submitted change according to the terms of the project's license, and that the work
+  being submitted is under appropriate copyright.
+
+Please submit a Contributor License Agreement (CLA) before submitting a pull request. 
+You may visit https://cla.azure.com to sign digitally. Alternatively, download the 
+agreement ([Microsoft Contribution License Agreement.docx](https://www.codeplex.com/Download?ProjectName=typescript&DownloadId=822190) or
+ [Microsoft Contribution License Agreement.pdf](https://www.codeplex.com/Download?ProjectName=typescript&DownloadId=921298)), sign, scan, 
+ and email it back to . Be sure to include your github user name along with the agreement. Once we have received the 
+ signed CLA, we'll review the request. 
\ No newline at end of file
diff --git a/README.md b/README.md
index 5981ef94f..cbbb66577 100644
--- a/README.md
+++ b/README.md
@@ -1,43 +1,148 @@
 # 
 
-### [Click here to find out what's new with Bot Framework](https://github.com/Microsoft/botframework/blob/master/whats-new.md#whats-new)
+### [What's new with Bot Framework](https://docs.microsoft.com/en-us/azure/bot-service/what-is-new?view=azure-bot-service-4.0)
 
-# Bot Framework SDK v4 for Python (Preview)
-[](https://fuselabs.visualstudio.com/SDK_v4/_build/latest?definitionId=431)
-[](https://github.com/Microsoft/botbuilder-python/wiki/Roadmap)
-[](https://coveralls.io/github/microsoft/botbuilder-python?branch=HEAD)
-[](https://github.com/psf/black)
+This repository contains code for the Python version of the [Microsoft Bot Framework SDK](https://github.com/Microsoft/botframework-sdk), which is part of the Microsoft Bot Framework - a comprehensive framework for building enterprise-grade conversational AI experiences.
 
-This repository contains code for the Python version of the [Microsoft Bot Framework SDK](https://github.com/Microsoft/botbuilder). The Bot Framework SDK v4 enables developers to model conversation and build sophisticated bot applications using Python.  This Python version is in **Preview** state and is being actively developed.
+This SDK enables developers to model conversation and build sophisticated bot applications using Python. SDKs for [JavaScript](https://github.com/Microsoft/botbuilder-js), [.NET](https://github.com/Microsoft/botbuilder-dotnet) and [Java (preview)](https://github.com/Microsoft/botbuilder-java) are also available.
 
-This repo is part the [Microsoft Bot Framework](https://github.com/Microsoft/botframework) - a comprehensive framework for building enterprise-grade conversational AI experiences.
+To get started building bots using the SDK, see the [Azure Bot Service Documentation](https://docs.microsoft.com/en-us/azure/bot-service/?view=azure-bot-service-4.0).
 
-In addition to the Python SDK, Bot Builder supports creating bots in other popular programming languages like [.Net SDK](https://github.com/Microsoft/botbuilder-dotnet), [JavaScript](https://github.com/Microsoft/botbuilder-js), and [Java](https://github.com/Microsoft/botbuilder-java). Production bots should be developed using the JavaScript or .Net SDKs.
+For more information jump to a section below.
 
-To get started see the [Azure Bot Service Documentation](https://docs.microsoft.com/en-us/azure/bot-service/?view=azure-bot-service-4.0) for the v4 SDK.
+* [Build status](#build-status)
+* [Packages](#packages)
+* [Getting started](#getting-started)
+* [Getting support and providing feedback](#getting-support-and-providing-feedback)
+* [Contributing and our code of conduct](contributing-and-our-code-of-conduct)
+* [Reporting security issues](#reporting-security-issues)
+
+## Build Status
+
+| Branch | Description        | Build Status | Coverage Status | Code Style |
+ |----|---------------|--------------|-----------------|--|
+| Main | 4.12.* Preview Builds | [](https://fuselabs.visualstudio.com/SDK_v4/_build/latest?definitionId=771&branchName=main) | [](https://coveralls.io/github/microsoft/botbuilder-python?branch=HEAD) | [](https://github.com/psf/black) |
 
 ## Packages
 
-- [](https://pypi.org/project/botbuilder-ai/) botbuilder-ai
-- [](https://pypi.org/project/botbuilder-applicationinsights/) botbuilder-applicationinsights
-- [](https://pypi.org/project/botbuilder-azure/) botbuilder-azure
-- [](https://pypi.org/project/botbuilder-core/) botbuilder-core
-- [](https://pypi.org/project/botbuilder-dialogs/) botbuilder-dialogs
-- [](https://pypi.org/project/botbuilder-schema/) botbuilder-schema
-- [](https://pypi.org/project/botframework-connector/) botframework-connector
-
-## Contributing
-This project welcomes contributions and suggestions. Most contributions require you to agree to a
-Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
-the rights to use your contribution. For details, visit https://cla.microsoft.com.
-When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
-a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
-provided by the bot. You will only need to do this once across all repos using our CLA.
+| Build | Released Package |
+ |----|---------------|
+| botbuilder-ai | [](https://pypi.org/project/botbuilder-ai/) |
+| botbuilder-applicationinsights | [](https://pypi.org/project/botbuilder-applicationinsights/) |
+| botbuilder-azure | [](https://pypi.org/project/botbuilder-azure/) |
+| botbuilder-core | [](https://pypi.org/project/botbuilder-core/) |
+| botbuilder-dialogs | [](https://pypi.org/project/botbuilder-dialogs/) |
+| botbuilder-schema | [](https://pypi.org/project/botbuilder-schema/) |
+| botframework-connector | [](https://pypi.org/project/botframework-connector/) |
+
+## Getting Started
+To get started building bots using the SDK, see the [Azure Bot Service Documentation](https://docs.microsoft.com/en-us/azure/bot-service/?view=azure-bot-service-4.0).
+
+The [Bot Framework Samples](https://github.com/microsoft/botbuilder-samples) includes a rich set of samples repository.
+
+If you want to debug an issue, would like to [contribute](#contributing-code), or understand how the Bot Builder SDK works, instructions for building and testing the SDK are below.
+
+### Prerequisites
+- [Git](https://git-scm.com/downloads)
+- [Python 3.8.2](https://www.python.org/downloads/)
+
+Python "Virtual Environments" allow Python packages to be installed in an isolated location for a particular application, rather than being installed globally, as such it is common practice to use them. Click [here](https://packaging.python.org/tutorials/installing-packages/#creating-virtual-environments) to learn more about creating _and activating_ Virtual Environments in Python.
+
+### Clone
+Clone a copy of the repo:
+```bash
+git clone https://github.com/Microsoft/botbuilder-python.git
+```
+Change to the SDK's directory:
+```bash
+cd botbuilder-python
+```
+
+### Using the SDK locally
+
+To use a local copy of the SDK you can link to these packages with the pip -e option.
+
+```bash
+pip install -e ./libraries/botbuilder-schema
+pip install -e ./libraries/botframework-connector
+pip install -e ./libraries/botbuilder-core
+pip install -e ./libraries/botbuilder-integration-aiohttp
+pip install -e ./libraries/botbuilder-ai
+pip install -e ./libraries/botbuilder-applicationinsights
+pip install -e ./libraries/botbuilder-integration-applicationinsights-aiohttp
+pip install -e ./libraries/botbuilder-dialogs
+pip install -e ./libraries/botbuilder-azure
+pip install -e ./libraries/botbuilder-adapters-slack
+pip install -e ./libraries/botbuilder-testing
+```
+
+### Running unit tests
+First execute the following command from the root level of the repo:
+```bash
+pip install -r ./libraries/botframework-connector/tests/requirements.txt
+pip install -r ./libraries/botbuilder-core/tests/requirements.txt
+pip install -r ./libraries/botbuilder-ai/tests/requirements.txt
+```
+
+Then enter run pytest by simply typing it into your CLI:
+
+```bash
+pytest
+```
+
+This is the expected output:
+```bash
+============================= test session starts =============================
+platform win32 -- Python 3.8.2, pytest-3.4.0, py-1.5.2, pluggy-0.6.0
+rootdir: C:\projects\botbuilder-python, inifile:
+plugins: cov-2.5.1
+...
+```
+
+## Getting support and providing feedback
+Below are the various channels that are available to you for obtaining support and providing feedback. Please pay carful attention to which channel should be used for which type of content. e.g. general "how do I..." questions should be asked on Stack Overflow, Twitter or Gitter, with GitHub issues being for feature requests and bug reports.
+
+### Github issues
+[Github issues](https://github.com/Microsoft/botbuilder-python/issues) should be used for bugs and feature requests.
+
+### Stack overflow
+[Stack Overflow](https://stackoverflow.com/questions/tagged/botframework) is a great place for getting high-quality answers. Our support team, as well as many of our community members are already on Stack Overflow providing answers to 'how-to' questions.
+
+### Azure Support
+If you issues relates to [Azure Bot Service](https://azure.microsoft.com/en-gb/services/bot-service/), you can take advantage of the available [Azure support options](https://azure.microsoft.com/en-us/support/options/).
+
+### Twitter
+We use the [@botframework](https://twitter.com/botframework) account on twitter for announcements and members from the development team watch for tweets for @botframework.
+
+### Gitter Chat Room
+The [Gitter Channel](https://gitter.im/Microsoft/BotBuilder) provides a place where the Community can get together and collaborate.
+
+## Contributing and our code of conduct
+We welcome contributions and suggestions. Please see our [contributing guidelines](./contributing.md) for more information.
+
 This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
-For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
-contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
+
+For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact
+ [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
+
+### Contributing Code
+
+In order to create pull requests, submitted code must pass ```pylint``` and ```black``` checks.  Run both tools on every file you've changed.
+
+For more information and installation instructions, see:
+
+* [black](https://pypi.org/project/black/)
+* [pylint](https://pylint.org/)
 
 ## Reporting Security Issues
-Security issues and bugs should be reported privately, via email, to the Microsoft Security Response Center (MSRC) at [secure@microsoft.com](mailto:secure@microsoft.com). You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Further information, including the [MSRC PGP](https://technet.microsoft.com/en-us/security/dn606155) key, can be found in the [Security TechCenter](https://technet.microsoft.com/en-us/security/default).
+Security issues and bugs should be reported privately, via email, to the Microsoft Security Response Center (MSRC)
+at [secure@microsoft.com](mailto:secure@microsoft.com).  You should receive a response within 24 hours.  If for some
+ reason you do not, please follow up via email to ensure we received your original message. Further information,
+ including the [MSRC PGP](https://technet.microsoft.com/en-us/security/dn606155) key, can be found in the
+[Security TechCenter](https://technet.microsoft.com/en-us/security/default).
 
 Copyright (c) Microsoft Corporation. All rights reserved.
+
+Licensed under the [MIT](./LICENSE) License.
+
+
diff --git a/UsingTestPyPI.md b/UsingTestPyPI.md
new file mode 100644
index 000000000..4bbe31a4f
--- /dev/null
+++ b/UsingTestPyPI.md
@@ -0,0 +1,19 @@
+# Using TestPyPI to consume rc builds
+The BotBuilder SDK rc build feed is found on [TestPyPI](https://test.pypi.org/). 
+
+The daily builds will be available soon through the mentioned feed as well.
+
+
+# Configure TestPyPI
+
+You can tell pip to download packages from TestPyPI instead of PyPI by specifying the --index-url flag (in the example below, replace 'botbuilder-core' for the name of the library you want to install)
+
+```bash
+$ pip install --index-url https://test.pypi.org/simple/ botbuilder-core
+```
+If you want to allow pip to also pull other packages from PyPI you can specify --extra-index-url to point to PyPI.
+This is useful when the package you’re testing has dependencies:
+
+```bash
+pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ botbuilder-core
+```
diff --git a/generators/LICENSE.md b/generators/LICENSE.md
deleted file mode 100644
index 506ab97e5..000000000
--- a/generators/LICENSE.md
+++ /dev/null
@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2018 Microsoft Corporation
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/generators/README.md b/generators/README.md
deleted file mode 100644
index 761d8ee79..000000000
--- a/generators/README.md
+++ /dev/null
@@ -1,215 +0,0 @@
-# python-generator-botbuilder
-
-Cookiecutter generators for [Bot Framework v4](https://dev.botframework.com).  Will let you quickly set up a conversational AI bot
-using core AI capabilities.
-
-## About
-
-`python-generator-botbuilder` will help you build new conversational AI bots using the [Bot Framework v4](https://dev.botframework.com).
-
-## Templates
-
-The generator supports three different template options.  The table below can help guide which template is right for you.
-
-|  Template  |  Description  |
-| ---------- |  ---------  |
-| Echo Bot | A good template if you want a little more than "Hello World!", but not much more.  This template handles the very basics of sending messages to a bot, and having the bot process the messages by repeating them back to the user.  This template produces a bot that simply "echoes" back to the user anything the user says to the bot. |
-| Core Bot | Our most advanced template, the Core template provides 6 core features every bot is likely to have.  This template covers the core features of a Conversational-AI bot using [LUIS](https://www.luis.ai).  See the **Core Bot Features** table below for more details. |
-| Empty Bot | A good template if you are familiar with Bot Framework v4, and simply want a basic skeleton project.  Also a good option if you want to take sample code from the documentation and paste it into a minimal bot in order to learn. |
-
-### How to Choose a Template
-
-| Template | When This Template is a Good Choice |
-| -------- | -------- |
-| Echo Bot  | You are new to Bot Framework v4 and want a working bot with minimal features. |
-| Core Bot | You understand some of the core concepts of Bot Framework v4 and are beyond the concepts introduced in the Echo Bot template.  You're familiar with or are ready to learn concepts such as language understanding using LUIS, managing multi-turn conversations with Dialogs, handling user initiated Dialog interruptions, and using Adaptive Cards to welcome your users. |
-| Empty Bot  | You are a seasoned Bot Framework v4 developer.  You've built bots before, and want the minimum skeleton of a bot. |
-
-### Template Overview
-
-#### Echo Bot Template
-
-The Echo Bot template is slightly more than the a classic "Hello World!" example, but not by much.  This template shows the basic structure of a bot, how a bot recieves messages from a user, and how a bot sends messages to a user.  The bot will "echo" back to the user, what the user says to the bot.  It is a good choice for first time, new to Bot Framework v4 developers.
-
-#### Core Bot Template
-
-The Core Bot template consists of set of core features most every bot is likely to have.  Building off of the core message processing features found in the Echo Bot template, this template adds a number of more sophisticated features.  The table below lists these features and provides links to additional documentation.
-
-| Core Bot Features | Description |
-| ------------------ | ----------- |
-| [Send and receive messages](https://docs.microsoft.com/azure/bot-service/bot-builder-howto-send-messages?view=azure-bot-service-4.0) | The primary way your bot will communicate with users, and likewise receive communication, is through message activities. Some messages may simply consist of plain text, while others may contain richer content such as cards or attachments. |
-| [Proactive messaging](https://docs.microsoft.com/azure/bot-service/bot-builder-howto-proactive-message?view=azure-bot-service-4.0) using [Adaptive Cards](https://docs.microsoft.com/azure/bot-service/bot-builder-send-welcome-message?view=azure-bot-service-4.0?#using-adaptive-card-greeting) | The primary goal when creating any bot is to engage your user in a meaningful conversation. One of the best ways to achieve this goal is to ensure that from the moment a user first connects to your bot, they understand your bot’s main purpose and capabilities.  We refer to this as "welcoming the user."  The Core template uses an [Adaptive Card](http://adaptivecards.io) to implement this behavior.  |
-| [Language understanding using LUIS](https://docs.microsoft.com/azure/bot-service/bot-builder-howto-v4-luis?view=azure-bot-service-4.0) | The ability to understand what your user means conversationally and contextually can be a difficult task, but can provide your bot a more natural conversation feel. Language Understanding, called LUIS, enables you to do just that so that your bot can recognize the intent of user messages, allow for more natural language from your user, and better direct the conversation flow. |
-| [Multi-turn conversation support using Dialogs](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-dialog?view=azure-bot-service-4.0) | The ability to manage conversations is an important part of the bot/user interation.  Bot Framework introduces the  concept of a Dialog to handle this conversational pattern.  Dialog objects process inbound Activities and generate outbound responses. The business logic of the bot runs either directly or indirectly within Dialog classes.  |
-| [Managing conversation state](https://docs.microsoft.com/azure/bot-service/bot-builder-howto-v4-state?view=azure-bot-service-4.0) | A key to good bot design is to track the context of a conversation, so that your bot remembers things like the answers to previous questions. |
-| [How to handle user-initiated interruptions](https://docs.microsoft.com/azure/bot-service/bot-builder-howto-handle-user-interrupt?view=azure-bot-service-4.0) | While you may think that your users will follow your defined conversation flow step by step, chances are good that they will change their minds or ask a question in the middle of the process instead of answering the question. Handling interruptions means making sure your bot is prepared to handle situations like this. |
-| [How to unit test a bot](https://aka.ms/cs-unit-test-docs) | Optionally, the Core Bot template can generate corresponding unit tests that shows how to use the testing framework introduced in Bot Framework version 4.5.  Selecting this option provides a complete set of units tests for Core Bot.  It shows how to write unit tests to test the various features of Core Bot. To add the Core Bot unit tests, run the generator and answer `yes` when prompted.  See below for an example of how to do this from the command line.  |
-
-#### Empty Bot Template
-
-The Empty Bot template is the minimal skeleton code for a bot.  It provides a stub `on_turn` handler but does not perform any actions.  If you are experienced writing bots with Bot Framework v4 and want the minimum scaffolding, the Empty template is for you.
-
-## Features by Template
-
-|  Feature  |  Empty Bot  |  Echo Bot   |  Core Bot*  |
-| --------- | :-----: | :-----: | :-----: |
-| Generate code in Python | X | X | X |
-| Support local development and testing using the [Bot Framework Emulator v4](https://www.github.com/microsoft/botframework-emulator) | X | X | X |
-| Core bot message processing |  | X | X |
-| Deploy your bot to Microsoft Azure |  | Pending | Pending |
-| Welcome new users using Adaptive Card technology |  |  | X |
-| Support AI-based greetings using [LUIS](https://www.luis.ai) |  |  | X |
-| Use Dialogs to manage more in-depth conversations |  |  | X |
-| Manage conversation state |  |  | X |
-| Handle user interruptions |  |  | X |
-| Unit test a bot using Bot Framework Testing framework (optional) |  |  | X |
-
-*Core Bot template is a work in progress landing soon. 
-## Installation
-
-1. Install [cookiecutter](https://github.com/cookiecutter/cookiecutter) using [pip](https://pip.pypa.io/en/stable/) (we assume you have pre-installed [python 3](https://www.python.org/downloads/)).
-
-    ```bash
-    pip install cookiecutter
-    ```
-
-2. Verify that cookiecutter has been installed correctly by typing the following into your console:
-
-    ```bash
-    cookiecutter --help
-    ```
-
-
-## Usage
-
-### Creating a New Bot Project
-
-To create an Echo Bot project:
-
-```bash
-cookiecutter https://github.com/microsoft/botbuilder-python/releases/download/Templates/echo.zip
-```
-
-To create a Core Bot project:
-
-```bash
-# Work in progress
-```
-
-To create an Empty Bot project:
-
-```bash
-cookiecutter https://github.com/microsoft/botbuilder-python/releases/download/Templates/empty.zip
-```
-
-When the generator is launched, it will prompt for the information required to create a new bot.
-
-### Generator Command Line Options and Arguments
-
-Cookiecutter supports a set of pre-defined command line options, the complete list with descriptions is available [here](https://cookiecutter.readthedocs.io/en/0.9.1/advanced_usage.html#command-line-options).
-
-Each generator can recieve a series of named arguments to pre-seed the prompt default value. If the `--no-input` option flag is send, these named arguments will be the default values for the template.
-
-| Named argument  | Description |
-| ------------------- | ----------- |
-| project_name    | The name given to the bot project |
-| bot_description | A brief bit of text that describes the purpose of the bot |
-| add_tests        | **PENDING** _A Core Bot Template Only Feature_.  The generator will add unit tests to the Core Bot generated bot.  This option is not available to other templates at this time.  To learn more about the test framework released with Bot Framework v4.5, see [How to unit test bots](https://aka.ms/js-unit-test-docs).  This option is intended to enable automated bot generation for testing purposes. |
-
-#### Example Using Named Arguments
-
-This example shows how to pass named arguments to the generator, setting the default bot name to test_project.
-
-```bash
-# Run the generator defaulting the bot name to test_project
-cookiecutter https://github.com/microsoft/botbuilder-python/releases/download/Templates/echo.zip project_name="test_project"
-```
-
-### Generating a Bot Using --no-input
-
-The generator can be run in `--no-input` mode, which can be used for automated bot creation.  When run in `--no-input` mode, the generator can be configured using named arguments as documented above.  If a named argument is ommitted a reasonable default will be used.
-
-#### Default Values
-
-| Named argument  | Default Value |
-| ------------------- | ----------- |
-| bot_name     | `my-chat-bot` |
-| bot_description | "Demonstrate the core capabilities of the Microsoft Bot Framework" |
-| add_tests        | `False`|
-
-#### Examples Using --no-input
-
-This example shows how to run the generator in --no-input mode, setting all required options on the command line.
-
-```bash
-# Run the generator, setting all command line options
-cookiecutter https://github.com/microsoft/botbuilder-python/releases/download/Templates/echo.zip --no-input project_name="test_bot" bot_description="Test description"
-```
-
-This example shows how to run the generator in --no-input mode, using all the default command line options.  The generator will create a bot project using all the default values specified in the **Default Options** table above.
-
-```bash
-# Run the generator using all default options
-cookiecutter https://github.com/microsoft/botbuilder-python/releases/download/Templates/echo.zip --no-input
-```
-
-This example shows how to run the generator in --no-input mode, with unit tests.
-
-```bash
-# PENDING: Run the generator using all default options
-```
-
-## Running Your Bot
-
-### Running Your Bot Locally
-
-To run your bot locally, type the following in your console:
-
-```bash
-# install dependencies
-pip install -r requirements.txt
-```
-
-```bash
-# run the bot
-python app.py
-```
-
-Alternatively to the last command, you can set the file in an environment variable with `set FLASK_APP=app.py` in windows (`export FLASK_APP=app.py` in mac/linux) and then run `flask run --host=127.0.0.1 --port=3978`
-
-### Interacting With Your Bot Using the Emulator
-
-- Launch Bot Framework Emulator
-- File -> Open Bot
-- Enter a Bot URL of `http://localhost:3978/api/messages`
-
-Once the Emulator is connected, you can interact with and receive messages from your bot.
-
-#### Lint Compliant Code
-
-The code generated by the botbuilder generator is pylint compliant to our ruleset. To use pylint as your develop your bot:
-
-```bash
-# Assuming you created a project with the bot_name value 'my_chat_bot'
-pylint --rcfile=my_chat_bot/.pylintrc my_chat_bot
-```
-
-#### Testing Core Bots with Tests (Pending)
-
-Core Bot templates generated with unit tests can be tested using the following:
-
-```bash
-# launch pytest
-pytest
-```
-
-## Deploy Your Bot to Azure (PENDING)
-
-After creating the bot and testing it locally, you can deploy it to Azure to make it accessible from anywhere.
-To learn how, see [Deploy your bot to Azure](https://aka.ms/azuredeployment) for a complete set of deployment instructions.
-
-If you are new to Microsoft Azure, please refer to [Getting started with Azure](https://azure.microsoft.com/get-started/) for guidance on how to get started on Azure.
-
-## Logging Issues and Providing Feedback
-
-Issues and feedback about the botbuilder generator can be submitted through the project's [GitHub Issues](https://github.com/Microsoft/botbuilder-samples/issues) page.
diff --git a/generators/app/templates/echo/cookiecutter.json b/generators/app/templates/echo/cookiecutter.json
deleted file mode 100644
index 4a14b6ade..000000000
--- a/generators/app/templates/echo/cookiecutter.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
-    "bot_name": "my_chat_bot",
-    "bot_description": "Demonstrate the core capabilities of the Microsoft Bot Framework"
-}
\ No newline at end of file
diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/.pylintrc b/generators/app/templates/echo/{{cookiecutter.bot_name}}/.pylintrc
deleted file mode 100644
index a77268c79..000000000
--- a/generators/app/templates/echo/{{cookiecutter.bot_name}}/.pylintrc
+++ /dev/null
@@ -1,494 +0,0 @@
-[MASTER]
-
-# A comma-separated list of package or module names from where C extensions may
-# be loaded. Extensions are loading into the active Python interpreter and may
-# run arbitrary code.
-extension-pkg-whitelist=
-
-# Add files or directories to the blacklist. They should be base names, not
-# paths.
-ignore=
-
-# Add files or directories matching the regex patterns to the blacklist. The
-# regex matches against base names, not paths.
-ignore-patterns=
-
-# Python code to execute, usually for sys.path manipulation such as
-# pygtk.require().
-#init-hook=
-
-# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
-# number of processors available to use.
-jobs=1
-
-# Control the amount of potential inferred values when inferring a single
-# object. This can help the performance when dealing with large functions or
-# complex, nested conditions.
-limit-inference-results=100
-
-# List of plugins (as comma separated values of python modules names) to load,
-# usually to register additional checkers.
-load-plugins=
-
-# Pickle collected data for later comparisons.
-persistent=yes
-
-# Specify a configuration file.
-#rcfile=
-
-# When enabled, pylint would attempt to guess common misconfiguration and emit
-# user-friendly hints instead of false-positive error messages.
-suggestion-mode=yes
-
-# Allow loading of arbitrary C extensions. Extensions are imported into the
-# active Python interpreter and may run arbitrary code.
-unsafe-load-any-extension=no
-
-
-[MESSAGES CONTROL]
-
-# Only show warnings with the listed confidence levels. Leave empty to show
-# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
-confidence=
-
-# Disable the message, report, category or checker with the given id(s). You
-# can either give multiple identifiers separated by comma (,) or put this
-# option multiple times (only on the command line, not in the configuration
-# file where it should appear only once). You can also use "--disable=all" to
-# disable everything first and then reenable specific checks. For example, if
-# you want to run only the similarities checker, you can use "--disable=all
-# --enable=similarities". If you want to run only the classes checker, but have
-# no Warning level messages displayed, use "--disable=all --enable=classes
-# --disable=W".
-disable=missing-docstring,
-        too-few-public-methods,
-        bad-continuation
-
-# Enable the message, report, category or checker with the given id(s). You can
-# either give multiple identifier separated by comma (,) or put this option
-# multiple time (only on the command line, not in the configuration file where
-# it should appear only once). See also the "--disable" option for examples.
-enable=c-extension-no-member
-
-
-[REPORTS]
-
-# Python expression which should return a note less than 10 (10 is the highest
-# note). You have access to the variables errors warning, statement which
-# respectively contain the number of errors / warnings messages and the total
-# number of statements analyzed. This is used by the global evaluation report
-# (RP0004).
-evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
-
-# Template used to display messages. This is a python new-style format string
-# used to format the message information. See doc for all details.
-#msg-template=
-
-# Set the output format. Available formats are text, parseable, colorized, json
-# and msvs (visual studio). You can also give a reporter class, e.g.
-# mypackage.mymodule.MyReporterClass.
-output-format=text
-
-# Tells whether to display a full report or only the messages.
-reports=no
-
-# Activate the evaluation score.
-score=yes
-
-
-[REFACTORING]
-
-# Maximum number of nested blocks for function / method body
-max-nested-blocks=5
-
-# Complete name of functions that never returns. When checking for
-# inconsistent-return-statements if a never returning function is called then
-# it will be considered as an explicit return statement and no message will be
-# printed.
-never-returning-functions=sys.exit
-
-
-[LOGGING]
-
-# Format style used to check logging format string. `old` means using %
-# formatting, while `new` is for `{}` formatting.
-logging-format-style=old
-
-# Logging modules to check that the string format arguments are in logging
-# function parameter format.
-logging-modules=logging
-
-
-[SPELLING]
-
-# Limits count of emitted suggestions for spelling mistakes.
-max-spelling-suggestions=4
-
-# Spelling dictionary name. Available dictionaries: none. To make it working
-# install python-enchant package..
-spelling-dict=
-
-# List of comma separated words that should not be checked.
-spelling-ignore-words=
-
-# A path to a file that contains private dictionary; one word per line.
-spelling-private-dict-file=
-
-# Tells whether to store unknown words to indicated private dictionary in
-# --spelling-private-dict-file option instead of raising a message.
-spelling-store-unknown-words=no
-
-
-[MISCELLANEOUS]
-
-# List of note tags to take in consideration, separated by a comma.
-notes=FIXME,
-      XXX,
-      TODO
-
-
-[TYPECHECK]
-
-# List of decorators that produce context managers, such as
-# contextlib.contextmanager. Add to this list to register other decorators that
-# produce valid context managers.
-contextmanager-decorators=contextlib.contextmanager
-
-# List of members which are set dynamically and missed by pylint inference
-# system, and so shouldn't trigger E1101 when accessed. Python regular
-# expressions are accepted.
-generated-members=
-
-# Tells whether missing members accessed in mixin class should be ignored. A
-# mixin class is detected if its name ends with "mixin" (case insensitive).
-ignore-mixin-members=yes
-
-# Tells whether to warn about missing members when the owner of the attribute
-# is inferred to be None.
-ignore-none=yes
-
-# This flag controls whether pylint should warn about no-member and similar
-# checks whenever an opaque object is returned when inferring. The inference
-# can return multiple potential results while evaluating a Python object, but
-# some branches might not be evaluated, which results in partial inference. In
-# that case, it might be useful to still emit no-member and other checks for
-# the rest of the inferred objects.
-ignore-on-opaque-inference=yes
-
-# List of class names for which member attributes should not be checked (useful
-# for classes with dynamically set attributes). This supports the use of
-# qualified names.
-ignored-classes=optparse.Values,thread._local,_thread._local
-
-# List of module names for which member attributes should not be checked
-# (useful for modules/projects where namespaces are manipulated during runtime
-# and thus existing member attributes cannot be deduced by static analysis. It
-# supports qualified module names, as well as Unix pattern matching.
-ignored-modules=
-
-# Show a hint with possible names when a member name was not found. The aspect
-# of finding the hint is based on edit distance.
-missing-member-hint=yes
-
-# The minimum edit distance a name should have in order to be considered a
-# similar match for a missing member name.
-missing-member-hint-distance=1
-
-# The total number of similar names that should be taken in consideration when
-# showing a hint for a missing member.
-missing-member-max-choices=1
-
-
-[VARIABLES]
-
-# List of additional names supposed to be defined in builtins. Remember that
-# you should avoid defining new builtins when possible.
-additional-builtins=
-
-# Tells whether unused global variables should be treated as a violation.
-allow-global-unused-variables=yes
-
-# List of strings which can identify a callback function by name. A callback
-# name must start or end with one of those strings.
-callbacks=cb_,
-          _cb
-
-# A regular expression matching the name of dummy variables (i.e. expected to
-# not be used).
-dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
-
-# Argument names that match this expression will be ignored. Default to name
-# with leading underscore.
-ignored-argument-names=_.*|^ignored_|^unused_
-
-# Tells whether we should check for unused import in __init__ files.
-init-import=no
-
-# List of qualified module names which can have objects that can redefine
-# builtins.
-redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
-
-
-[FORMAT]
-
-# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
-expected-line-ending-format=
-
-# Regexp for a line that is allowed to be longer than the limit.
-ignore-long-lines=^\s*(# )??$
-
-# Number of spaces of indent required inside a hanging or continued line.
-indent-after-paren=4
-
-# String used as indentation unit. This is usually "    " (4 spaces) or "\t" (1
-# tab).
-indent-string='    '
-
-# Maximum number of characters on a single line.
-max-line-length=120
-
-# Maximum number of lines in a module.
-max-module-lines=1000
-
-# List of optional constructs for which whitespace checking is disabled. `dict-
-# separator` is used to allow tabulation in dicts, etc.: {1  : 1,\n222: 2}.
-# `trailing-comma` allows a space between comma and closing bracket: (a, ).
-# `empty-line` allows space-only lines.
-no-space-check=trailing-comma,
-               dict-separator
-
-# Allow the body of a class to be on the same line as the declaration if body
-# contains single statement.
-single-line-class-stmt=no
-
-# Allow the body of an if to be on the same line as the test if there is no
-# else.
-single-line-if-stmt=no
-
-
-[SIMILARITIES]
-
-# Ignore comments when computing similarities.
-ignore-comments=yes
-
-# Ignore docstrings when computing similarities.
-ignore-docstrings=yes
-
-# Ignore imports when computing similarities.
-ignore-imports=no
-
-# Minimum lines number of a similarity.
-min-similarity-lines=4
-
-
-[BASIC]
-
-# Naming style matching correct argument names.
-argument-naming-style=snake_case
-
-# Regular expression matching correct argument names. Overrides argument-
-# naming-style.
-#argument-rgx=
-
-# Naming style matching correct attribute names.
-attr-naming-style=snake_case
-
-# Regular expression matching correct attribute names. Overrides attr-naming-
-# style.
-#attr-rgx=
-
-# Bad variable names which should always be refused, separated by a comma.
-bad-names=foo,
-          bar,
-          baz,
-          toto,
-          tutu,
-          tata
-
-# Naming style matching correct class attribute names.
-class-attribute-naming-style=any
-
-# Regular expression matching correct class attribute names. Overrides class-
-# attribute-naming-style.
-#class-attribute-rgx=
-
-# Naming style matching correct class names.
-class-naming-style=PascalCase
-
-# Regular expression matching correct class names. Overrides class-naming-
-# style.
-#class-rgx=
-
-# Naming style matching correct constant names.
-const-naming-style=UPPER_CASE
-
-# Regular expression matching correct constant names. Overrides const-naming-
-# style.
-#const-rgx=
-
-# Minimum line length for functions/classes that require docstrings, shorter
-# ones are exempt.
-docstring-min-length=-1
-
-# Naming style matching correct function names.
-function-naming-style=snake_case
-
-# Regular expression matching correct function names. Overrides function-
-# naming-style.
-#function-rgx=
-
-# Good variable names which should always be accepted, separated by a comma.
-good-names=i,
-           j,
-           k,
-           ex,
-           Run,
-           _
-
-# Include a hint for the correct naming format with invalid-name.
-include-naming-hint=no
-
-# Naming style matching correct inline iteration names.
-inlinevar-naming-style=any
-
-# Regular expression matching correct inline iteration names. Overrides
-# inlinevar-naming-style.
-#inlinevar-rgx=
-
-# Naming style matching correct method names.
-method-naming-style=snake_case
-
-# Regular expression matching correct method names. Overrides method-naming-
-# style.
-#method-rgx=
-
-# Naming style matching correct module names.
-module-naming-style=snake_case
-
-# Regular expression matching correct module names. Overrides module-naming-
-# style.
-#module-rgx=
-
-# Colon-delimited sets of names that determine each other's naming style when
-# the name regexes allow several styles.
-name-group=
-
-# Regular expression which should only match function or class names that do
-# not require a docstring.
-no-docstring-rgx=^_
-
-# List of decorators that produce properties, such as abc.abstractproperty. Add
-# to this list to register other decorators that produce valid properties.
-# These decorators are taken in consideration only for invalid-name.
-property-classes=abc.abstractproperty
-
-# Naming style matching correct variable names.
-variable-naming-style=snake_case
-
-# Regular expression matching correct variable names. Overrides variable-
-# naming-style.
-#variable-rgx=
-
-
-[STRING]
-
-# This flag controls whether the implicit-str-concat-in-sequence should
-# generate a warning on implicit string concatenation in sequences defined over
-# several lines.
-check-str-concat-over-line-jumps=no
-
-
-[IMPORTS]
-
-# Allow wildcard imports from modules that define __all__.
-allow-wildcard-with-all=no
-
-# Analyse import fallback blocks. This can be used to support both Python 2 and
-# 3 compatible code, which means that the block might have code that exists
-# only in one or another interpreter, leading to false positives when analysed.
-analyse-fallback-blocks=no
-
-# Deprecated modules which should not be used, separated by a comma.
-deprecated-modules=optparse,tkinter.tix
-
-# Create a graph of external dependencies in the given file (report RP0402 must
-# not be disabled).
-ext-import-graph=
-
-# Create a graph of every (i.e. internal and external) dependencies in the
-# given file (report RP0402 must not be disabled).
-import-graph=
-
-# Create a graph of internal dependencies in the given file (report RP0402 must
-# not be disabled).
-int-import-graph=
-
-# Force import order to recognize a module as part of the standard
-# compatibility libraries.
-known-standard-library=
-
-# Force import order to recognize a module as part of a third party library.
-known-third-party=enchant
-
-
-[CLASSES]
-
-# List of method names used to declare (i.e. assign) instance attributes.
-defining-attr-methods=__init__,
-                      __new__,
-                      setUp
-
-# List of member names, which should be excluded from the protected access
-# warning.
-exclude-protected=_asdict,
-                  _fields,
-                  _replace,
-                  _source,
-                  _make
-
-# List of valid names for the first argument in a class method.
-valid-classmethod-first-arg=cls
-
-# List of valid names for the first argument in a metaclass class method.
-valid-metaclass-classmethod-first-arg=cls
-
-
-[DESIGN]
-
-# Maximum number of arguments for function / method.
-max-args=5
-
-# Maximum number of attributes for a class (see R0902).
-max-attributes=7
-
-# Maximum number of boolean expressions in an if statement.
-max-bool-expr=5
-
-# Maximum number of branch for function / method body.
-max-branches=12
-
-# Maximum number of locals for function / method body.
-max-locals=15
-
-# Maximum number of parents for a class (see R0901).
-max-parents=7
-
-# Maximum number of public methods for a class (see R0904).
-max-public-methods=20
-
-# Maximum number of return / yield for function / method body.
-max-returns=6
-
-# Maximum number of statements in function / method body.
-max-statements=50
-
-# Minimum number of public methods for a class (see R0903).
-min-public-methods=2
-
-
-[EXCEPTIONS]
-
-# Exceptions that will emit a warning when being caught. Defaults to
-# "BaseException, Exception".
-overgeneral-exceptions=BaseException,
-                       Exception
diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/README.md b/generators/app/templates/echo/{{cookiecutter.bot_name}}/README.md
deleted file mode 100644
index 5eeee191f..000000000
--- a/generators/app/templates/echo/{{cookiecutter.bot_name}}/README.md
+++ /dev/null
@@ -1,43 +0,0 @@
-# {{cookiecutter.bot_name}}
-
-{{cookiecutter.bot_description}}
-
-This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a simple bot that accepts input from the user and echoes it back.
-
-## Prerequisites
-
-This sample **requires** prerequisites in order to run.
-
-### Install Python 3.6
-
-## Running the sample
-- Run `pip install -r requirements.txt` to install all dependencies
-- Run `python app.py`
-- Alternatively to the last command, you can set the file in an environment variable with `set FLASK_APP=app.py` in windows (`export FLASK_APP=app.py` in mac/linux) and then run `flask run --host=127.0.0.1 --port=3978`
-
-
-## Testing the bot using Bot Framework Emulator
-
-[Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel.
-
-- Install the Bot Framework Emulator version 4.3.0 or greater from [here](https://github.com/Microsoft/BotFramework-Emulator/releases)
-
-### Connect to the bot using Bot Framework Emulator
-
-- Launch Bot Framework Emulator
-- Enter a Bot URL of `http://localhost:3978/api/messages`
-
-
-## Further reading
-
-- [Bot Framework Documentation](https://docs.botframework.com)
-- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0)
-- [Dialogs](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-dialog?view=azure-bot-service-4.0)
-- [Gathering Input Using Prompts](https://docs.microsoft.com/azure/bot-service/bot-builder-prompts?view=azure-bot-service-4.0&tabs=csharp)
-- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0)
-- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0)
-- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0)
-- [Azure CLI](https://docs.microsoft.com/cli/azure/?view=azure-cli-latest)
-- [Azure Portal](https://portal.azure.com)
-- [Language Understanding using LUIS](https://docs.microsoft.com/azure/cognitive-services/luis/)
-- [Channels and Bot Connector Service](https://docs.microsoft.com/azure/bot-service/bot-concepts?view=azure-bot-service-4.0)
\ No newline at end of file
diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/__init__.py b/generators/app/templates/echo/{{cookiecutter.bot_name}}/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/generators/app/templates/empty/cookiecutter.json b/generators/app/templates/empty/cookiecutter.json
deleted file mode 100644
index 4a14b6ade..000000000
--- a/generators/app/templates/empty/cookiecutter.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
-    "bot_name": "my_chat_bot",
-    "bot_description": "Demonstrate the core capabilities of the Microsoft Bot Framework"
-}
\ No newline at end of file
diff --git a/generators/app/templates/empty/{{cookiecutter.bot_name}}/.pylintrc b/generators/app/templates/empty/{{cookiecutter.bot_name}}/.pylintrc
deleted file mode 100644
index a77268c79..000000000
--- a/generators/app/templates/empty/{{cookiecutter.bot_name}}/.pylintrc
+++ /dev/null
@@ -1,494 +0,0 @@
-[MASTER]
-
-# A comma-separated list of package or module names from where C extensions may
-# be loaded. Extensions are loading into the active Python interpreter and may
-# run arbitrary code.
-extension-pkg-whitelist=
-
-# Add files or directories to the blacklist. They should be base names, not
-# paths.
-ignore=
-
-# Add files or directories matching the regex patterns to the blacklist. The
-# regex matches against base names, not paths.
-ignore-patterns=
-
-# Python code to execute, usually for sys.path manipulation such as
-# pygtk.require().
-#init-hook=
-
-# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
-# number of processors available to use.
-jobs=1
-
-# Control the amount of potential inferred values when inferring a single
-# object. This can help the performance when dealing with large functions or
-# complex, nested conditions.
-limit-inference-results=100
-
-# List of plugins (as comma separated values of python modules names) to load,
-# usually to register additional checkers.
-load-plugins=
-
-# Pickle collected data for later comparisons.
-persistent=yes
-
-# Specify a configuration file.
-#rcfile=
-
-# When enabled, pylint would attempt to guess common misconfiguration and emit
-# user-friendly hints instead of false-positive error messages.
-suggestion-mode=yes
-
-# Allow loading of arbitrary C extensions. Extensions are imported into the
-# active Python interpreter and may run arbitrary code.
-unsafe-load-any-extension=no
-
-
-[MESSAGES CONTROL]
-
-# Only show warnings with the listed confidence levels. Leave empty to show
-# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
-confidence=
-
-# Disable the message, report, category or checker with the given id(s). You
-# can either give multiple identifiers separated by comma (,) or put this
-# option multiple times (only on the command line, not in the configuration
-# file where it should appear only once). You can also use "--disable=all" to
-# disable everything first and then reenable specific checks. For example, if
-# you want to run only the similarities checker, you can use "--disable=all
-# --enable=similarities". If you want to run only the classes checker, but have
-# no Warning level messages displayed, use "--disable=all --enable=classes
-# --disable=W".
-disable=missing-docstring,
-        too-few-public-methods,
-        bad-continuation
-
-# Enable the message, report, category or checker with the given id(s). You can
-# either give multiple identifier separated by comma (,) or put this option
-# multiple time (only on the command line, not in the configuration file where
-# it should appear only once). See also the "--disable" option for examples.
-enable=c-extension-no-member
-
-
-[REPORTS]
-
-# Python expression which should return a note less than 10 (10 is the highest
-# note). You have access to the variables errors warning, statement which
-# respectively contain the number of errors / warnings messages and the total
-# number of statements analyzed. This is used by the global evaluation report
-# (RP0004).
-evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
-
-# Template used to display messages. This is a python new-style format string
-# used to format the message information. See doc for all details.
-#msg-template=
-
-# Set the output format. Available formats are text, parseable, colorized, json
-# and msvs (visual studio). You can also give a reporter class, e.g.
-# mypackage.mymodule.MyReporterClass.
-output-format=text
-
-# Tells whether to display a full report or only the messages.
-reports=no
-
-# Activate the evaluation score.
-score=yes
-
-
-[REFACTORING]
-
-# Maximum number of nested blocks for function / method body
-max-nested-blocks=5
-
-# Complete name of functions that never returns. When checking for
-# inconsistent-return-statements if a never returning function is called then
-# it will be considered as an explicit return statement and no message will be
-# printed.
-never-returning-functions=sys.exit
-
-
-[LOGGING]
-
-# Format style used to check logging format string. `old` means using %
-# formatting, while `new` is for `{}` formatting.
-logging-format-style=old
-
-# Logging modules to check that the string format arguments are in logging
-# function parameter format.
-logging-modules=logging
-
-
-[SPELLING]
-
-# Limits count of emitted suggestions for spelling mistakes.
-max-spelling-suggestions=4
-
-# Spelling dictionary name. Available dictionaries: none. To make it working
-# install python-enchant package..
-spelling-dict=
-
-# List of comma separated words that should not be checked.
-spelling-ignore-words=
-
-# A path to a file that contains private dictionary; one word per line.
-spelling-private-dict-file=
-
-# Tells whether to store unknown words to indicated private dictionary in
-# --spelling-private-dict-file option instead of raising a message.
-spelling-store-unknown-words=no
-
-
-[MISCELLANEOUS]
-
-# List of note tags to take in consideration, separated by a comma.
-notes=FIXME,
-      XXX,
-      TODO
-
-
-[TYPECHECK]
-
-# List of decorators that produce context managers, such as
-# contextlib.contextmanager. Add to this list to register other decorators that
-# produce valid context managers.
-contextmanager-decorators=contextlib.contextmanager
-
-# List of members which are set dynamically and missed by pylint inference
-# system, and so shouldn't trigger E1101 when accessed. Python regular
-# expressions are accepted.
-generated-members=
-
-# Tells whether missing members accessed in mixin class should be ignored. A
-# mixin class is detected if its name ends with "mixin" (case insensitive).
-ignore-mixin-members=yes
-
-# Tells whether to warn about missing members when the owner of the attribute
-# is inferred to be None.
-ignore-none=yes
-
-# This flag controls whether pylint should warn about no-member and similar
-# checks whenever an opaque object is returned when inferring. The inference
-# can return multiple potential results while evaluating a Python object, but
-# some branches might not be evaluated, which results in partial inference. In
-# that case, it might be useful to still emit no-member and other checks for
-# the rest of the inferred objects.
-ignore-on-opaque-inference=yes
-
-# List of class names for which member attributes should not be checked (useful
-# for classes with dynamically set attributes). This supports the use of
-# qualified names.
-ignored-classes=optparse.Values,thread._local,_thread._local
-
-# List of module names for which member attributes should not be checked
-# (useful for modules/projects where namespaces are manipulated during runtime
-# and thus existing member attributes cannot be deduced by static analysis. It
-# supports qualified module names, as well as Unix pattern matching.
-ignored-modules=
-
-# Show a hint with possible names when a member name was not found. The aspect
-# of finding the hint is based on edit distance.
-missing-member-hint=yes
-
-# The minimum edit distance a name should have in order to be considered a
-# similar match for a missing member name.
-missing-member-hint-distance=1
-
-# The total number of similar names that should be taken in consideration when
-# showing a hint for a missing member.
-missing-member-max-choices=1
-
-
-[VARIABLES]
-
-# List of additional names supposed to be defined in builtins. Remember that
-# you should avoid defining new builtins when possible.
-additional-builtins=
-
-# Tells whether unused global variables should be treated as a violation.
-allow-global-unused-variables=yes
-
-# List of strings which can identify a callback function by name. A callback
-# name must start or end with one of those strings.
-callbacks=cb_,
-          _cb
-
-# A regular expression matching the name of dummy variables (i.e. expected to
-# not be used).
-dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
-
-# Argument names that match this expression will be ignored. Default to name
-# with leading underscore.
-ignored-argument-names=_.*|^ignored_|^unused_
-
-# Tells whether we should check for unused import in __init__ files.
-init-import=no
-
-# List of qualified module names which can have objects that can redefine
-# builtins.
-redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
-
-
-[FORMAT]
-
-# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
-expected-line-ending-format=
-
-# Regexp for a line that is allowed to be longer than the limit.
-ignore-long-lines=^\s*(# )??$
-
-# Number of spaces of indent required inside a hanging or continued line.
-indent-after-paren=4
-
-# String used as indentation unit. This is usually "    " (4 spaces) or "\t" (1
-# tab).
-indent-string='    '
-
-# Maximum number of characters on a single line.
-max-line-length=120
-
-# Maximum number of lines in a module.
-max-module-lines=1000
-
-# List of optional constructs for which whitespace checking is disabled. `dict-
-# separator` is used to allow tabulation in dicts, etc.: {1  : 1,\n222: 2}.
-# `trailing-comma` allows a space between comma and closing bracket: (a, ).
-# `empty-line` allows space-only lines.
-no-space-check=trailing-comma,
-               dict-separator
-
-# Allow the body of a class to be on the same line as the declaration if body
-# contains single statement.
-single-line-class-stmt=no
-
-# Allow the body of an if to be on the same line as the test if there is no
-# else.
-single-line-if-stmt=no
-
-
-[SIMILARITIES]
-
-# Ignore comments when computing similarities.
-ignore-comments=yes
-
-# Ignore docstrings when computing similarities.
-ignore-docstrings=yes
-
-# Ignore imports when computing similarities.
-ignore-imports=no
-
-# Minimum lines number of a similarity.
-min-similarity-lines=4
-
-
-[BASIC]
-
-# Naming style matching correct argument names.
-argument-naming-style=snake_case
-
-# Regular expression matching correct argument names. Overrides argument-
-# naming-style.
-#argument-rgx=
-
-# Naming style matching correct attribute names.
-attr-naming-style=snake_case
-
-# Regular expression matching correct attribute names. Overrides attr-naming-
-# style.
-#attr-rgx=
-
-# Bad variable names which should always be refused, separated by a comma.
-bad-names=foo,
-          bar,
-          baz,
-          toto,
-          tutu,
-          tata
-
-# Naming style matching correct class attribute names.
-class-attribute-naming-style=any
-
-# Regular expression matching correct class attribute names. Overrides class-
-# attribute-naming-style.
-#class-attribute-rgx=
-
-# Naming style matching correct class names.
-class-naming-style=PascalCase
-
-# Regular expression matching correct class names. Overrides class-naming-
-# style.
-#class-rgx=
-
-# Naming style matching correct constant names.
-const-naming-style=UPPER_CASE
-
-# Regular expression matching correct constant names. Overrides const-naming-
-# style.
-#const-rgx=
-
-# Minimum line length for functions/classes that require docstrings, shorter
-# ones are exempt.
-docstring-min-length=-1
-
-# Naming style matching correct function names.
-function-naming-style=snake_case
-
-# Regular expression matching correct function names. Overrides function-
-# naming-style.
-#function-rgx=
-
-# Good variable names which should always be accepted, separated by a comma.
-good-names=i,
-           j,
-           k,
-           ex,
-           Run,
-           _
-
-# Include a hint for the correct naming format with invalid-name.
-include-naming-hint=no
-
-# Naming style matching correct inline iteration names.
-inlinevar-naming-style=any
-
-# Regular expression matching correct inline iteration names. Overrides
-# inlinevar-naming-style.
-#inlinevar-rgx=
-
-# Naming style matching correct method names.
-method-naming-style=snake_case
-
-# Regular expression matching correct method names. Overrides method-naming-
-# style.
-#method-rgx=
-
-# Naming style matching correct module names.
-module-naming-style=snake_case
-
-# Regular expression matching correct module names. Overrides module-naming-
-# style.
-#module-rgx=
-
-# Colon-delimited sets of names that determine each other's naming style when
-# the name regexes allow several styles.
-name-group=
-
-# Regular expression which should only match function or class names that do
-# not require a docstring.
-no-docstring-rgx=^_
-
-# List of decorators that produce properties, such as abc.abstractproperty. Add
-# to this list to register other decorators that produce valid properties.
-# These decorators are taken in consideration only for invalid-name.
-property-classes=abc.abstractproperty
-
-# Naming style matching correct variable names.
-variable-naming-style=snake_case
-
-# Regular expression matching correct variable names. Overrides variable-
-# naming-style.
-#variable-rgx=
-
-
-[STRING]
-
-# This flag controls whether the implicit-str-concat-in-sequence should
-# generate a warning on implicit string concatenation in sequences defined over
-# several lines.
-check-str-concat-over-line-jumps=no
-
-
-[IMPORTS]
-
-# Allow wildcard imports from modules that define __all__.
-allow-wildcard-with-all=no
-
-# Analyse import fallback blocks. This can be used to support both Python 2 and
-# 3 compatible code, which means that the block might have code that exists
-# only in one or another interpreter, leading to false positives when analysed.
-analyse-fallback-blocks=no
-
-# Deprecated modules which should not be used, separated by a comma.
-deprecated-modules=optparse,tkinter.tix
-
-# Create a graph of external dependencies in the given file (report RP0402 must
-# not be disabled).
-ext-import-graph=
-
-# Create a graph of every (i.e. internal and external) dependencies in the
-# given file (report RP0402 must not be disabled).
-import-graph=
-
-# Create a graph of internal dependencies in the given file (report RP0402 must
-# not be disabled).
-int-import-graph=
-
-# Force import order to recognize a module as part of the standard
-# compatibility libraries.
-known-standard-library=
-
-# Force import order to recognize a module as part of a third party library.
-known-third-party=enchant
-
-
-[CLASSES]
-
-# List of method names used to declare (i.e. assign) instance attributes.
-defining-attr-methods=__init__,
-                      __new__,
-                      setUp
-
-# List of member names, which should be excluded from the protected access
-# warning.
-exclude-protected=_asdict,
-                  _fields,
-                  _replace,
-                  _source,
-                  _make
-
-# List of valid names for the first argument in a class method.
-valid-classmethod-first-arg=cls
-
-# List of valid names for the first argument in a metaclass class method.
-valid-metaclass-classmethod-first-arg=cls
-
-
-[DESIGN]
-
-# Maximum number of arguments for function / method.
-max-args=5
-
-# Maximum number of attributes for a class (see R0902).
-max-attributes=7
-
-# Maximum number of boolean expressions in an if statement.
-max-bool-expr=5
-
-# Maximum number of branch for function / method body.
-max-branches=12
-
-# Maximum number of locals for function / method body.
-max-locals=15
-
-# Maximum number of parents for a class (see R0901).
-max-parents=7
-
-# Maximum number of public methods for a class (see R0904).
-max-public-methods=20
-
-# Maximum number of return / yield for function / method body.
-max-returns=6
-
-# Maximum number of statements in function / method body.
-max-statements=50
-
-# Minimum number of public methods for a class (see R0903).
-min-public-methods=2
-
-
-[EXCEPTIONS]
-
-# Exceptions that will emit a warning when being caught. Defaults to
-# "BaseException, Exception".
-overgeneral-exceptions=BaseException,
-                       Exception
diff --git a/generators/app/templates/empty/{{cookiecutter.bot_name}}/README.md b/generators/app/templates/empty/{{cookiecutter.bot_name}}/README.md
deleted file mode 100644
index 5eeee191f..000000000
--- a/generators/app/templates/empty/{{cookiecutter.bot_name}}/README.md
+++ /dev/null
@@ -1,43 +0,0 @@
-# {{cookiecutter.bot_name}}
-
-{{cookiecutter.bot_description}}
-
-This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a simple bot that accepts input from the user and echoes it back.
-
-## Prerequisites
-
-This sample **requires** prerequisites in order to run.
-
-### Install Python 3.6
-
-## Running the sample
-- Run `pip install -r requirements.txt` to install all dependencies
-- Run `python app.py`
-- Alternatively to the last command, you can set the file in an environment variable with `set FLASK_APP=app.py` in windows (`export FLASK_APP=app.py` in mac/linux) and then run `flask run --host=127.0.0.1 --port=3978`
-
-
-## Testing the bot using Bot Framework Emulator
-
-[Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel.
-
-- Install the Bot Framework Emulator version 4.3.0 or greater from [here](https://github.com/Microsoft/BotFramework-Emulator/releases)
-
-### Connect to the bot using Bot Framework Emulator
-
-- Launch Bot Framework Emulator
-- Enter a Bot URL of `http://localhost:3978/api/messages`
-
-
-## Further reading
-
-- [Bot Framework Documentation](https://docs.botframework.com)
-- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0)
-- [Dialogs](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-dialog?view=azure-bot-service-4.0)
-- [Gathering Input Using Prompts](https://docs.microsoft.com/azure/bot-service/bot-builder-prompts?view=azure-bot-service-4.0&tabs=csharp)
-- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0)
-- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0)
-- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0)
-- [Azure CLI](https://docs.microsoft.com/cli/azure/?view=azure-cli-latest)
-- [Azure Portal](https://portal.azure.com)
-- [Language Understanding using LUIS](https://docs.microsoft.com/azure/cognitive-services/luis/)
-- [Channels and Bot Connector Service](https://docs.microsoft.com/azure/bot-service/bot-concepts?view=azure-bot-service-4.0)
\ No newline at end of file
diff --git a/generators/app/templates/empty/{{cookiecutter.bot_name}}/__init__.py b/generators/app/templates/empty/{{cookiecutter.bot_name}}/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/generators/app/templates/empty/{{cookiecutter.bot_name}}/bot.py b/generators/app/templates/empty/{{cookiecutter.bot_name}}/bot.py
deleted file mode 100644
index f0c2122cf..000000000
--- a/generators/app/templates/empty/{{cookiecutter.bot_name}}/bot.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from botbuilder.core import ActivityHandler, TurnContext
-from botbuilder.schema import ChannelAccount
-
-
-class MyBot(ActivityHandler):
-    async def on_members_added_activity(
-        self,
-        members_added: ChannelAccount,
-        turn_context: TurnContext
-    ):
-        for member_added in members_added:
-            if member_added.id != turn_context.activity.recipient.id:
-                await turn_context.send_activity("Hello world!")
diff --git a/generators/app/templates/empty/{{cookiecutter.bot_name}}/requirements.txt b/generators/app/templates/empty/{{cookiecutter.bot_name}}/requirements.txt
deleted file mode 100644
index 2e5ecf3fc..000000000
--- a/generators/app/templates/empty/{{cookiecutter.bot_name}}/requirements.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-botbuilder-core>=4.5.0.b4
-flask>=1.0.3
-
diff --git a/libraries/botbuilder-adapters-slack/README.rst b/libraries/botbuilder-adapters-slack/README.rst
new file mode 100644
index 000000000..9465f3997
--- /dev/null
+++ b/libraries/botbuilder-adapters-slack/README.rst
@@ -0,0 +1,83 @@
+
+==================================
+BotBuilder-Adapters SDK for Python
+==================================
+
+.. image:: https://dev.azure.com/FuseLabs/SDK_v4/_apis/build/status/Python/Python-CI-PR-yaml?branchName=master
+   :target:  https://dev.azure.com/FuseLabs/SDK_v4/_apis/build/status/Python/Python-CI-PR-yaml?branchName=master
+   :align: right
+   :alt: Azure DevOps status for master branch
+.. image:: https://badge.fury.io/py/botbuilder-adapters-slack.svg
+   :target: https://badge.fury.io/py/botbuilder-adapters-slack
+   :alt: Latest PyPI package version
+
+A dialog stack based conversation manager for Microsoft BotBuilder.
+
+How to Install
+==============
+
+.. code-block:: python
+  
+  pip install botbuilder-adapters-slack
+
+
+Documentation/Wiki
+==================
+
+You can find more information on the botbuilder-python project by visiting our `Wiki`_.
+
+Requirements
+============
+
+* `Python >= 3.7.0`_
+
+
+Source Code
+===========
+The latest developer version is available in a github repository:
+https://github.com/Microsoft/botbuilder-python/
+
+
+Contributing
+============
+
+This project welcomes contributions and suggestions.  Most contributions require you to agree to a
+Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
+the rights to use your contribution. For details, visit https://cla.microsoft.com.
+
+When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
+a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
+provided by the bot. You will only need to do this once across all repos using our CLA.
+
+This project has adopted the `Microsoft Open Source Code of Conduct`_.
+For more information see the `Code of Conduct FAQ`_ or
+contact `opencode@microsoft.com`_ with any additional questions or comments.
+
+Reporting Security Issues
+=========================
+
+Security issues and bugs should be reported privately, via email, to the Microsoft Security
+Response Center (MSRC) at `secure@microsoft.com`_. You should
+receive a response within 24 hours. If for some reason you do not, please follow up via
+email to ensure we received your original message. Further information, including the
+`MSRC PGP`_ key, can be found in
+the `Security TechCenter`_.
+
+License
+=======
+
+Copyright (c) Microsoft Corporation. All rights reserved.
+
+Licensed under the MIT_ License.
+
+.. _Wiki: https://github.com/Microsoft/botbuilder-python/wiki
+.. _Python >= 3.7.0: https://www.python.org/downloads/
+.. _MIT: https://github.com/Microsoft/vscode/blob/master/LICENSE.txt
+.. _Microsoft Open Source Code of Conduct: https://opensource.microsoft.com/codeofconduct/
+.. _Code of Conduct FAQ: https://opensource.microsoft.com/codeofconduct/faq/
+.. _opencode@microsoft.com: mailto:opencode@microsoft.com
+.. _secure@microsoft.com: mailto:secure@microsoft.com
+.. _MSRC PGP: https://technet.microsoft.com/en-us/security/dn606155
+.. _Security TechCenter: https://github.com/Microsoft/vscode/blob/master/LICENSE.txt
+
+.. `_
\ No newline at end of file
diff --git a/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/__init__.py b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/__init__.py
new file mode 100644
index 000000000..1ab395b75
--- /dev/null
+++ b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/__init__.py
@@ -0,0 +1,30 @@
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+# --------------------------------------------------------------------------
+
+from .about import __version__
+from .slack_options import SlackAdapterOptions
+from .slack_client import SlackClient
+from .slack_adapter import SlackAdapter
+from .slack_payload import SlackPayload
+from .slack_message import SlackMessage
+from .slack_event import SlackEvent
+from .activity_resourceresponse import ActivityResourceResponse
+from .slack_request_body import SlackRequestBody
+from .slack_helper import SlackHelper
+
+__all__ = [
+    "__version__",
+    "SlackAdapterOptions",
+    "SlackClient",
+    "SlackAdapter",
+    "SlackPayload",
+    "SlackMessage",
+    "SlackEvent",
+    "ActivityResourceResponse",
+    "SlackRequestBody",
+    "SlackHelper",
+]
diff --git a/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/about.py b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/about.py
new file mode 100644
index 000000000..405dd97ef
--- /dev/null
+++ b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/about.py
@@ -0,0 +1,14 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+
+__title__ = "botbuilder-adapters-slack"
+__version__ = (
+    os.environ["packageVersion"] if "packageVersion" in os.environ else "4.12.0"
+)
+__uri__ = "https://www.github.com/Microsoft/botbuilder-python"
+__author__ = "Microsoft"
+__description__ = "Microsoft Bot Framework Bot Builder"
+__summary__ = "Microsoft Bot Framework Bot Builder SDK for Python."
+__license__ = "MIT"
diff --git a/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/activity_resourceresponse.py b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/activity_resourceresponse.py
new file mode 100644
index 000000000..e99b2edd9
--- /dev/null
+++ b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/activity_resourceresponse.py
@@ -0,0 +1,11 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.schema import ResourceResponse, ConversationAccount
+
+
+class ActivityResourceResponse(ResourceResponse):
+    def __init__(self, activity_id: str, conversation: ConversationAccount, **kwargs):
+        super().__init__(**kwargs)
+        self.activity_id = activity_id
+        self.conversation = conversation
diff --git a/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_adapter.py b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_adapter.py
new file mode 100644
index 000000000..3a03d7553
--- /dev/null
+++ b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_adapter.py
@@ -0,0 +1,239 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from abc import ABC
+from typing import List, Callable, Awaitable
+
+from aiohttp.web_request import Request
+from aiohttp.web_response import Response
+from botframework.connector.auth import ClaimsIdentity
+from botbuilder.core import conversation_reference_extension
+from botbuilder.core import BotAdapter, TurnContext
+from botbuilder.schema import (
+    Activity,
+    ResourceResponse,
+    ActivityTypes,
+    ConversationAccount,
+    ConversationReference,
+)
+
+from .activity_resourceresponse import ActivityResourceResponse
+from .slack_client import SlackClient
+from .slack_helper import SlackHelper
+
+
+class SlackAdapter(BotAdapter, ABC):
+    """
+    BotAdapter that can handle incoming Slack events. Incoming Slack events are deserialized to an Activity that is
+     dispatched through the middleware and bot pipeline.
+    """
+
+    def __init__(
+        self,
+        client: SlackClient,
+        on_turn_error: Callable[[TurnContext, Exception], Awaitable] = None,
+    ):
+        super().__init__(on_turn_error)
+        self.slack_client = client
+        self.slack_logged_in = False
+
+    async def send_activities(
+        self, context: TurnContext, activities: List[Activity]
+    ) -> List[ResourceResponse]:
+        """
+        Send a message from the bot to the messaging API.
+
+        :param context: A TurnContext representing the current incoming message and environment.
+        :type context: :class:`botbuilder.core.TurnContext`
+        :param activities: An array of outgoing activities to be sent back to the messaging API.
+        :type activities: :class:`typing.List[Activity]`
+        :return: An array of ResourceResponse objects containing the IDs that Slack assigned to the sent messages.
+        :rtype: :class:`typing.List[ResourceResponse]`
+        """
+
+        if not context:
+            raise Exception("TurnContext is required")
+        if not activities:
+            raise Exception("List[Activity] is required")
+
+        responses = []
+
+        for activity in activities:
+            if activity.type == ActivityTypes.message:
+                message = SlackHelper.activity_to_slack(activity)
+
+                slack_response = await self.slack_client.post_message_to_slack(message)
+
+                if slack_response and slack_response.status_code / 100 == 2:
+                    resource_response = ActivityResourceResponse(
+                        id=slack_response.data["ts"],
+                        activity_id=slack_response.data["ts"],
+                        conversation=ConversationAccount(
+                            id=slack_response.data["channel"]
+                        ),
+                    )
+
+                    responses.append(resource_response)
+
+        return responses
+
+    async def update_activity(self, context: TurnContext, activity: Activity):
+        """
+        Update a previous message with new content.
+
+        :param context: A TurnContext representing the current incoming message and environment.
+        :type context: :class:`botbuilder.core.TurnContext`
+        :param activity: The updated activity in the form '{id: `id of activity to update`, ...}'.
+        :type activity: :class:`botbuilder.schema.Activity`
+        :return: A resource response with the ID of the updated activity.
+        :rtype: :class:`botbuilder.schema.ResourceResponse`
+        """
+
+        if not context:
+            raise Exception("TurnContext is required")
+        if not activity:
+            raise Exception("Activity is required")
+        if not activity.id:
+            raise Exception("Activity.id is required")
+        if not activity.conversation:
+            raise Exception("Activity.conversation is required")
+
+        message = SlackHelper.activity_to_slack(activity)
+        results = await self.slack_client.update(
+            timestamp=message.ts, channel_id=message.channel, text=message.text,
+        )
+
+        if results.status_code / 100 != 2:
+            raise Exception(f"Error updating activity on slack: {results}")
+
+        return ResourceResponse(id=activity.id)
+
+    async def delete_activity(
+        self, context: TurnContext, reference: ConversationReference
+    ):
+        """
+        Delete a previous message.
+
+        :param context: A TurnContext representing the current incoming message and environment.
+        :type context: :class:`botbuilder.core.TurnContext`
+        :param reference: An object in the form "{activityId: `id of message to delete`,conversation: { id: `id of Slack
+         channel`}}".
+        :type reference: :class:`botbuilder.schema.ConversationReference`
+        """
+
+        if not context:
+            raise Exception("TurnContext is required")
+        if not reference:
+            raise Exception("ConversationReference is required")
+        if not reference.channel_id:
+            raise Exception("ConversationReference.channel_id is required")
+        if not context.activity.timestamp:
+            raise Exception("Activity.timestamp is required")
+
+        await self.slack_client.delete_message(
+            channel_id=reference.channel_id, timestamp=context.activity.timestamp
+        )
+
+    async def continue_conversation(
+        self,
+        reference: ConversationReference,
+        callback: Callable,
+        bot_id: str = None,
+        claims_identity: ClaimsIdentity = None,
+        audience: str = None,
+    ):
+        """
+        Send a proactive message to a conversation.
+
+        .. remarks::
+
+            Most channels require a user to initiate a conversation with a bot before the bot can send activities to the
+             user.
+
+        :param reference: A reference to the conversation to continue.
+        :type reference: :class:`botbuilder.schema.ConversationReference`
+        :param callback: The method to call for the resulting bot turn.
+        :type callback: :class:`typing.Callable`
+        :param bot_id: Unused for this override.
+        :type bot_id: str
+        :param claims_identity: A ClaimsIdentity for the conversation.
+        :type claims_identity: :class:`botframework.connector.auth.ClaimsIdentity`
+        :param audience: Unused for this override.
+        :type audience: str
+        """
+
+        if not reference:
+            raise Exception("ConversationReference is required")
+        if not callback:
+            raise Exception("callback is required")
+
+        if claims_identity:
+            request = conversation_reference_extension.get_continuation_activity(
+                reference
+            )
+            context = TurnContext(self, request)
+            context.turn_state[BotAdapter.BOT_IDENTITY_KEY] = claims_identity
+            context.turn_state[BotAdapter.BOT_CALLBACK_HANDLER_KEY] = callback
+        else:
+            request = TurnContext.apply_conversation_reference(
+                conversation_reference_extension.get_continuation_activity(reference),
+                reference,
+            )
+            context = TurnContext(self, request)
+
+        return await self.run_pipeline(context, callback)
+
+    async def process(self, req: Request, logic: Callable) -> Response:
+        """
+        Accept an incoming webhook request and convert it into a TurnContext which can be processed by the bot's logic.
+
+        :param req: The aiohttp Request object.
+        :type req: :class:`aiohttp.web_request.Request`
+        :param logic: The method to call for the resulting bot turn.
+        :type logic: :class:`tying.Callable`
+        :return: The aiohttp Response.
+        :rtype: :class:`aiohttp.web_response.Response`
+        """
+
+        if not req:
+            raise Exception("Request is required")
+
+        if not self.slack_logged_in:
+            await self.slack_client.login_with_slack()
+            self.slack_logged_in = True
+
+        body = await req.text()
+        slack_body = SlackHelper.deserialize_body(req.content_type, body)
+
+        if slack_body.type == "url_verification":
+            return SlackHelper.response(req, 200, slack_body.challenge)
+
+        if not self.slack_client.verify_signature(req, body):
+            text = "Rejected due to mismatched header signature"
+            return SlackHelper.response(req, 401, text)
+
+        if (
+            not self.slack_client.options.slack_verification_token
+            and slack_body.token != self.slack_client.options.slack_verification_token
+        ):
+            text = f"Rejected due to mismatched verificationToken:{body}"
+            return SlackHelper.response(req, 403, text)
+
+        if slack_body.payload:
+            # handle interactive_message callbacks and block_actions
+            activity = SlackHelper.payload_to_activity(slack_body.payload)
+        elif slack_body.type == "event_callback":
+            activity = await SlackHelper.event_to_activity(
+                slack_body.event, self.slack_client
+            )
+        elif slack_body.command:
+            activity = await SlackHelper.command_to_activity(
+                slack_body, self.slack_client
+            )
+        else:
+            raise Exception(f"Unknown Slack event type {slack_body.type}")
+
+        context = TurnContext(self, activity)
+        await self.run_pipeline(context, logic)
+
+        return SlackHelper.response(req, 200)
diff --git a/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_client.py b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_client.py
new file mode 100644
index 000000000..297fb6b8e
--- /dev/null
+++ b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_client.py
@@ -0,0 +1,450 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import hashlib
+import hmac
+import json
+from io import IOBase
+from typing import List, Union
+
+import aiohttp
+from aiohttp.web_request import Request
+
+from slack.web.client import WebClient
+from slack.web.slack_response import SlackResponse
+
+from botbuilder.schema import Activity
+from botbuilder.adapters.slack import SlackAdapterOptions
+from botbuilder.adapters.slack.slack_message import SlackMessage
+
+POST_MESSAGE_URL = "https://slack.com/api/chat.postMessage"
+POST_EPHEMERAL_MESSAGE_URL = "https://slack.com/api/chat.postEphemeral"
+
+
+class SlackClient(WebClient):
+    """
+    Slack client that extends https://github.com/slackapi/python-slackclient.
+    """
+
+    def __init__(self, options: SlackAdapterOptions):
+        if not options or not options.slack_bot_token:
+            raise Exception("SlackAdapterOptions and bot_token are required")
+
+        if (
+            not options.slack_verification_token
+            and not options.slack_client_signing_secret
+        ):
+            warning = (
+                "\n****************************************************************************************\n"
+                "* WARNING: Your bot is operating without recommended security mechanisms in place.     *\n"
+                "* Initialize your adapter with a clientSigningSecret parameter to enable               *\n"
+                "* verification that all incoming webhooks originate with Slack:                        *\n"
+                "*                                                                                      *\n"
+                "* adapter = new SlackAdapter({clientSigningSecret: });           *\n"
+                "*                                                                                      *\n"
+                "****************************************************************************************\n"
+                ">> Slack docs: https://api.slack.com/docs/verifying-requests-from-slack"
+            )
+            raise Exception(
+                warning
+                + "Required: include a verificationToken or clientSigningSecret to verify incoming Events API webhooks"
+            )
+
+        super().__init__(token=options.slack_bot_token, run_async=True)
+
+        self.options = options
+        self.identity = None
+
+    async def login_with_slack(self):
+        if self.options.slack_bot_token:
+            self.identity = await self.test_auth()
+        elif (
+            not self.options.slack_client_id
+            or not self.options.slack_client_secret
+            or not self.options.slack_redirect_uri
+            or not self.options.slack_scopes
+        ):
+            raise Exception(
+                "Missing Slack API credentials! Provide SlackClientId, SlackClientSecret, scopes and SlackRedirectUri "
+                "as part of the SlackAdapter options."
+            )
+
+    def is_logged_in(self):
+        return self.identity is not None
+
+    async def test_auth(self) -> str:
+        auth = await self.auth_test()
+        return auth.data["user_id"]
+
+    async def channels_list_ex(self, exclude_archived: bool = True) -> SlackResponse:
+        args = {"exclude_archived": "1" if exclude_archived else "0"}
+        return await self.channels_list(**args)
+
+    async def users_counts(self) -> SlackResponse:
+        return await self.api_call("users.counts")
+
+    async def im_history_ex(
+        self,
+        channel: str,
+        latest_timestamp: str = None,
+        oldest_timestamp: str = None,
+        count: int = None,
+        unreads: bool = None,
+    ) -> SlackResponse:
+        args = {}
+        if latest_timestamp:
+            args["latest"] = latest_timestamp
+        if oldest_timestamp:
+            args["oldest"] = oldest_timestamp
+        if count:
+            args["count"] = str(count)
+        if unreads:
+            args["unreads"] = "1" if unreads else "0"
+
+        return await self.im_history(channel=channel, **args)
+
+    async def files_info_ex(
+        self, file_id: str, page: int = None, count: int = None
+    ) -> SlackResponse:
+        args = {"count": str(count), "page": str(page)}
+        return await self.files_info(file=file_id, **args)
+
+    async def files_list_ex(
+        self,
+        user_id: str = None,
+        date_from: str = None,
+        date_to: str = None,
+        count: int = None,
+        page: int = None,
+        types: List[str] = None,
+    ) -> SlackResponse:
+        args = {}
+
+        if user_id:
+            args["user"] = user_id
+
+        if date_from:
+            args["ts_from"] = date_from
+        if date_to:
+            args["ts_to"] = date_to
+
+        if count:
+            args["count"] = str(count)
+        if page:
+            args["page"] = str(page)
+
+        if types:
+            args["types"] = ",".join(types)
+
+        return await self.files_list(**args)
+
+    async def groups_history_ex(
+        self, channel: str, latest: str = None, oldest: str = None, count: int = None
+    ) -> SlackResponse:
+        args = {}
+
+        if latest:
+            args["latest"] = latest
+        if oldest:
+            args["oldest"] = oldest
+
+        if count:
+            args["count"] = count
+
+        return await self.groups_history(channel=channel, **args)
+
+    async def groups_list_ex(self, exclude_archived: bool = True) -> SlackResponse:
+        args = {"exclude_archived": "1" if exclude_archived else "0"}
+        return await self.groups_list(**args)
+
+    async def get_preferences(self) -> SlackResponse:
+        return await self.api_call("users.prefs.get", http_verb="GET")
+
+    async def stars_list_ex(
+        self, user: str = None, count: int = None, page: int = None
+    ) -> SlackResponse:
+        args = {}
+
+        if user:
+            args["user"] = user
+        if count:
+            args["count"] = str(count)
+        if page:
+            args["page"] = str(page)
+
+        return await self.stars_list(**args)
+
+    async def groups_close(self, channel: str) -> SlackResponse:
+        args = {"channel": channel}
+        return await self.api_call("groups.close", params=args)
+
+    async def chat_post_ephemeral_ex(
+        self,
+        channel: str,
+        text: str,
+        target_user: str,
+        parse: str = None,
+        link_names: bool = False,
+        attachments: List[str] = None,  # pylint: disable=unused-argument
+        as_user: bool = False,
+    ) -> SlackResponse:
+        args = {
+            "text": text,
+            "link_names": "1" if link_names else "0",
+            "as_user": "1" if as_user else "0",
+        }
+
+        if parse:
+            args["parse"] = parse
+
+        # TODO: attachments (see PostEphemeralMessageAsync)
+        # See: https://api.slack.com/messaging/composing/layouts#attachments
+        # See: https://github.com/Inumedia/SlackAPI/blob/master/SlackAPI/Attachment.cs
+
+        return await self.chat_postEphemeral(channel=channel, user=target_user, **args)
+
+    async def chat_post_message_ex(
+        self,
+        channel: str,
+        text: str,
+        bot_name: str = None,
+        parse: str = None,
+        link_names: bool = False,
+        blocks: List[str] = None,  # pylint: disable=unused-argument
+        attachments: List[str] = None,  # pylint: disable=unused-argument
+        unfurl_links: bool = False,
+        icon_url: str = None,
+        icon_emoji: str = None,
+        as_user: bool = False,
+    ) -> SlackResponse:
+        args = {
+            "text": text,
+            "link_names": "1" if link_names else "0",
+            "as_user": "1" if as_user else "0",
+        }
+
+        if bot_name:
+            args["username"] = bot_name
+
+        if parse:
+            args["parse"] = parse
+
+        if unfurl_links:
+            args["unfurl_links"] = "1" if unfurl_links else "0"
+
+        if icon_url:
+            args["icon_url"] = icon_url
+
+        if icon_emoji:
+            args["icon_emoji"] = icon_emoji
+
+        # TODO: blocks and attachments (see PostMessageAsync)
+        # the blocks and attachments are combined into a single dict
+        # See: https://api.slack.com/messaging/composing/layouts#attachments
+        # See: https://github.com/Inumedia/SlackAPI/blob/master/SlackAPI/Attachment.cs
+
+        return await self.chat_postMessage(channel=channel, **args)
+
+    async def search_all_ex(
+        self,
+        query: str,
+        sorting: str = None,
+        direction: str = None,
+        enable_highlights: bool = False,
+        count: int = None,
+        page: int = None,
+    ) -> SlackResponse:
+        args = {"highlight": "1" if enable_highlights else "0"}
+
+        if sorting:
+            args["sort"] = sorting
+
+        if direction:
+            args["sort_dir"] = direction
+
+        if count:
+            args["count"] = str(count)
+
+        if page:
+            args["page"] = str(page)
+
+        return await self.search_all(query=query, **args)
+
+    async def search_files_ex(
+        self,
+        query: str,
+        sorting: str = None,
+        direction: str = None,
+        enable_highlights: bool = False,
+        count: int = None,
+        page: int = None,
+    ) -> SlackResponse:
+        args = {"highlight": "1" if enable_highlights else "0"}
+
+        if sorting:
+            args["sort"] = sorting
+
+        if direction:
+            args["sort_dir"] = direction
+
+        if count:
+            args["count"] = str(count)
+
+        if page:
+            args["page"] = str(page)
+
+        return await self.search_files(query=query, **args)
+
+    async def search_messages_ex(
+        self,
+        query: str,
+        sorting: str = None,
+        direction: str = None,
+        enable_highlights: bool = False,
+        count: int = None,
+        page: int = None,
+    ) -> SlackResponse:
+        args = {"highlight": "1" if enable_highlights else "0"}
+
+        if sorting:
+            args["sort"] = sorting
+
+        if direction:
+            args["sort_dir"] = direction
+
+        if count:
+            args["count"] = str(count)
+
+        if page:
+            args["page"] = str(page)
+
+        return await self.search_messages(query=query, **args)
+
+    async def chat_update_ex(
+        self,
+        timestamp: str,
+        channel: str,
+        text: str,
+        bot_name: str = None,
+        parse: str = None,
+        link_names: bool = False,
+        attachments: List[str] = None,  # pylint: disable=unused-argument
+        as_user: bool = False,
+    ):
+        args = {
+            "text": text,
+            "link_names": "1" if link_names else "0",
+            "as_user": "1" if as_user else "0",
+        }
+
+        if bot_name:
+            args["username"] = bot_name
+
+        if parse:
+            args["parse"] = parse
+
+        # TODO: attachments (see PostEphemeralMessageAsync)
+        # See: https://api.slack.com/messaging/composing/layouts#attachments
+        # See: https://github.com/Inumedia/SlackAPI/blob/master/SlackAPI/Attachment.cs
+
+        return await self.chat_update(channel=channel, ts=timestamp)
+
+    async def files_upload_ex(
+        self,
+        file: Union[str, IOBase] = None,
+        content: str = None,
+        channels: List[str] = None,
+        title: str = None,
+        initial_comment: str = None,
+        file_type: str = None,
+    ):
+        args = {}
+
+        if channels:
+            args["channels"] = ",".join(channels)
+
+        if title:
+            args["title"] = title
+
+        if initial_comment:
+            args["initial_comment"] = initial_comment
+
+        if file_type:
+            args["filetype"] = file_type
+
+        return await self.files_upload(file=file, content=content, **args)
+
+    async def get_bot_user_by_team(self, activity: Activity) -> str:
+        if self.identity:
+            return self.identity
+
+        if not activity.conversation.properties["team"]:
+            return None
+
+        user = await self.options.get_bot_user_by_team(
+            activity.conversation.properties["team"]
+        )
+        if user:
+            return user
+        raise Exception("Missing credentials for team.")
+
+    def verify_signature(self, req: Request, body: str) -> bool:
+        timestamp = req.headers["X-Slack-Request-Timestamp"]
+        message = ":".join(["v0", timestamp, body])
+
+        computed_signature = "V0=" + hmac.new(
+            bytes(self.options.slack_client_signing_secret, "utf-8"),
+            msg=bytes(message, "utf-8"),
+            digestmod=hashlib.sha256,
+        ).hexdigest().upper().replace("-", "")
+
+        received_signature = req.headers["X-Slack-Signature"].upper()
+
+        return computed_signature == received_signature
+
+    async def post_message_to_slack(self, message: SlackMessage) -> SlackResponse:
+        if not message:
+            return None
+
+        request_content = {
+            "token": self.options.slack_bot_token,
+            "channel": message.channel,
+            "text": message.text,
+        }
+
+        if message.thread_ts:
+            request_content["thread_ts"] = message.thread_ts
+
+        if message.blocks:
+            request_content["blocks"] = json.dumps(message.blocks)
+
+        session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=30),)
+
+        http_verb = "POST"
+        api_url = POST_EPHEMERAL_MESSAGE_URL if message.ephemeral else POST_MESSAGE_URL
+        req_args = {"data": request_content}
+
+        async with session.request(http_verb, api_url, **req_args) as res:
+            response_content = {}
+            try:
+                response_content = await res.json()
+            except aiohttp.ContentTypeError:
+                pass
+
+            response_data = {
+                "data": response_content,
+                "headers": res.headers,
+                "status_code": res.status,
+            }
+
+            data = {
+                "client": self,
+                "http_verb": http_verb,
+                "api_url": api_url,
+                "req_args": req_args,
+            }
+            response = SlackResponse(**{**data, **response_data}).validate()
+
+        await session.close()
+
+        return response
diff --git a/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_event.py b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_event.py
new file mode 100644
index 000000000..66b810ffb
--- /dev/null
+++ b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_event.py
@@ -0,0 +1,34 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from typing import List
+from botbuilder.adapters.slack.slack_message import SlackMessage
+
+
+class SlackEvent:
+    """
+    Wrapper class for an incoming slack event.
+    """
+
+    def __init__(self, **kwargs):
+        self.client_msg_id = kwargs.get("client_msg_id")
+        self.type = kwargs.get("type")
+        self.subtype = kwargs.get("subtype")
+        self.text = kwargs.get("text")
+        self.ts = kwargs.get("ts")  # pylint: disable=invalid-name
+        self.team = kwargs.get("team")
+        self.channel = kwargs.get("channel")
+        self.channel_id = kwargs.get("channel_id")
+        self.event_ts = kwargs.get("event_ts")
+        self.channel_type = kwargs.get("channel_type")
+        self.thread_ts = kwargs.get("thread_ts")
+        self.user = kwargs.get("user")
+        self.user_id = kwargs.get("user_id")
+        self.bot_id = kwargs.get("bot_id")
+        self.actions: List[str] = kwargs.get("actions")
+        self.item = kwargs.get("item")
+        self.item_channel = kwargs.get("item_channel")
+        self.files: [] = kwargs.get("files")
+        self.message = (
+            None if "message" not in kwargs else SlackMessage(**kwargs.get("message"))
+        )
diff --git a/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_helper.py b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_helper.py
new file mode 100644
index 000000000..d71fd7852
--- /dev/null
+++ b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_helper.py
@@ -0,0 +1,293 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import json
+import urllib.parse
+
+from aiohttp.web_request import Request
+from aiohttp.web_response import Response
+
+from slack.web.classes.attachments import Attachment
+
+from botbuilder.schema import (
+    Activity,
+    ConversationAccount,
+    ChannelAccount,
+    ActivityTypes,
+)
+
+from .slack_message import SlackMessage
+from .slack_client import SlackClient
+from .slack_event import SlackEvent
+from .slack_payload import SlackPayload
+from .slack_request_body import SlackRequestBody
+
+
+class SlackHelper:
+    @staticmethod
+    def activity_to_slack(activity: Activity) -> SlackMessage:
+        """
+        Formats a BotBuilder Activity into an outgoing Slack message.
+
+        :param activity: A BotBuilder Activity object.
+        :type activity: :class:`botbuilder.schema.Activity`
+        :return: A Slack message object with {text, attachments, channel, thread ts} and any fields found in
+         activity.channelData.
+        :rtype: :class:`SlackMessage`
+        """
+
+        if not activity:
+            raise Exception("Activity required")
+
+        # use ChannelData if available
+        if activity.channel_data:
+            message = activity.channel_data
+        else:
+            message = SlackMessage(
+                ts=activity.timestamp,
+                text=activity.text,
+                channel=activity.conversation.id,
+            )
+
+            if activity.attachments:
+                attachments = []
+                for att in activity.attachments:
+                    if att.name == "blocks":
+                        message.blocks = att.content
+                    else:
+                        new_attachment = Attachment(
+                            author_name=att.name, thumb_url=att.thumbnail_url, text="",
+                        )
+                        attachments.append(new_attachment)
+
+                if attachments:
+                    message.attachments = attachments
+
+            if (
+                activity.conversation.properties
+                and "thread_ts" in activity.conversation.properties
+            ):
+                message.thread_ts = activity.conversation.properties["thread_ts"]
+
+        if message.ephemeral:
+            message.user = activity.recipient.id
+
+        if (
+            message.icon_url
+            or not (message.icons and message.icons.status_emoji)
+            or not message.username
+        ):
+            message.as_user = False
+
+        return message
+
+    @staticmethod
+    def response(  # pylint: disable=unused-argument
+        req: Request, code: int, text: str = None, encoding: str = None
+    ) -> Response:
+        """
+        Formats an aiohttp Response.
+
+        :param req: The original aiohttp Request.
+        :type req: :class:`aiohttp.web_request.Request`
+        :param code: The HTTP result code to return.
+        :type code: int
+        :param text: The text to return.
+        :type text: str
+        :param encoding: The text encoding. Defaults to UTF-8.
+        :type encoding: str
+        :return: The aoihttp Response
+        :rtype: :class:`aiohttp.web_response.Response`
+        """
+
+        response = Response(status=code)
+
+        if text:
+            response.content_type = "text/plain"
+            response.body = text.encode(encoding=encoding if encoding else "utf-8")
+
+        return response
+
+    @staticmethod
+    def payload_to_activity(payload: SlackPayload) -> Activity:
+        """
+        Creates an activity based on the Slack event payload.
+
+        :param payload: The payload of the Slack event.
+        :type payload: :class:`SlackPayload`
+        :return: An activity containing the event data.
+        :rtype: :class:`botbuilder.schema.Activity`
+        """
+
+        if not payload:
+            raise Exception("payload is required")
+
+        activity = Activity(
+            channel_id="slack",
+            conversation=ConversationAccount(id=payload.channel.id, properties={}),
+            from_property=ChannelAccount(
+                id=payload.message.bot_id if payload.message.bot_id else payload.user.id
+            ),
+            recipient=ChannelAccount(),
+            channel_data=payload,
+            text=None,
+            type=ActivityTypes.event,
+        )
+
+        if payload.thread_ts:
+            activity.conversation.properties["thread_ts"] = payload.thread_ts
+
+        if payload.actions and (
+            payload.type == "block_actions" or payload.type == "interactive_message"
+        ):
+            activity.type = ActivityTypes.message
+            activity.text = payload.actions.value
+
+        return activity
+
+    @staticmethod
+    async def event_to_activity(event: SlackEvent, client: SlackClient) -> Activity:
+        """
+        Creates an activity based on the Slack event data.
+
+        :param event: The data of the Slack event.
+        :type event: :class:`SlackEvent`
+        :param client: The Slack client.
+        :type client: :class:`SlackClient`
+        :return: An activity containing the event data.
+        :rtype: :class:`botbuilder.schema.Activity`
+        """
+
+        if not event:
+            raise Exception("slack event is required")
+
+        activity = Activity(
+            id=event.event_ts,
+            channel_id="slack",
+            conversation=ConversationAccount(
+                id=event.channel if event.channel else event.channel_id, properties={}
+            ),
+            from_property=ChannelAccount(
+                id=event.bot_id if event.bot_id else event.user_id
+            ),
+            recipient=ChannelAccount(id=None),
+            channel_data=event,
+            text=event.text,
+            type=ActivityTypes.event,
+        )
+
+        if event.thread_ts:
+            activity.conversation.properties["thread_ts"] = event.thread_ts
+
+        if not activity.conversation.id:
+            if event.item and event.item_channel:
+                activity.conversation.id = event.item_channel
+            else:
+                activity.conversation.id = event.team
+
+        activity.recipient.id = await client.get_bot_user_by_team(activity=activity)
+
+        # If this is a message originating from a user, we'll mark it as such
+        # If this is a message from a bot (bot_id != None), we want to ignore it by
+        # leaving the activity type as Event.  This will stop it from being included in dialogs,
+        # but still allow the Bot to act on it if it chooses (via ActivityHandler.on_event_activity).
+        # NOTE: This catches a message from ANY bot, including this bot.
+        # Note also, bot_id here is not the same as bot_user_id so we can't (yet) identify messages
+        # originating from this bot without doing an additional API call.
+        if event.type == "message" and not event.subtype and not event.bot_id:
+            activity.type = ActivityTypes.message
+
+        return activity
+
+    @staticmethod
+    async def command_to_activity(
+        body: SlackRequestBody, client: SlackClient
+    ) -> Activity:
+        """
+        Creates an activity based on a Slack event related to a slash command.
+
+        :param body: The data of the Slack event.
+        :type body: :class:`SlackRequestBody`
+        :param client: The Slack client.
+        :type client: :class:`SlackClient`
+        :return: An activity containing the event data.
+        :rtype: :class:`botbuilder.schema.Activity`
+        """
+
+        if not body:
+            raise Exception("body is required")
+
+        activity = Activity(
+            id=body.trigger_id,
+            channel_id="slack",
+            conversation=ConversationAccount(id=body.channel_id, properties={}),
+            from_property=ChannelAccount(id=body.user_id),
+            recipient=ChannelAccount(id=None),
+            channel_data=body,
+            text=body.text,
+            type=ActivityTypes.event,
+        )
+
+        activity.recipient.id = await client.get_bot_user_by_team(activity)
+        activity.conversation.properties["team"] = body.team_id
+
+        return activity
+
+    @staticmethod
+    def query_string_to_dictionary(query: str) -> {}:
+        """
+        Converts a query string to a dictionary with key-value pairs.
+
+        :param query: The query string to convert.
+        :type query: str
+        :return: A dictionary with the query values.
+        :rtype: :class:`typing.Dict`
+        """
+
+        values = {}
+
+        if not query:
+            return values
+
+        pairs = query.replace("+", "%20").split("&")
+
+        for pair in pairs:
+            key_value = pair.split("=")
+            key = key_value[0]
+            value = urllib.parse.unquote(key_value[1])
+
+            values[key] = value
+
+        return values
+
+    @staticmethod
+    def deserialize_body(content_type: str, request_body: str) -> SlackRequestBody:
+        """
+        Deserializes the request's body as a SlackRequestBody object.
+
+        :param content_type: The content type of the body.
+        :type content_type: str
+        :param request_body: The body of the request.
+        :type request_body: str
+        :return: A SlackRequestBody object.
+        :rtype: :class:`SlackRequestBody`
+        """
+
+        if not request_body:
+            return None
+
+        if content_type == "application/x-www-form-urlencoded":
+            request_dict = SlackHelper.query_string_to_dictionary(request_body)
+        elif content_type == "application/json":
+            request_dict = json.loads(request_body)
+        else:
+            raise Exception("Unknown request content type")
+
+        if "command=%2F" in request_body:
+            return SlackRequestBody(**request_dict)
+
+        if "payload=" in request_body:
+            payload = SlackPayload(**request_dict)
+            return SlackRequestBody(payload=payload, token=payload.token)
+
+        return SlackRequestBody(**request_dict)
diff --git a/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_message.py b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_message.py
new file mode 100644
index 000000000..38a7e3297
--- /dev/null
+++ b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_message.py
@@ -0,0 +1,33 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from slack.web.classes.attachments import Attachment
+from slack.web.classes.blocks import Block
+
+
+class SlackMessage:
+    def __init__(self, **kwargs):
+        self.ephemeral = kwargs.get("ephemeral")
+        self.as_user = kwargs.get("as_user")
+        self.icon_url = kwargs.get("icon_url")
+        self.icon_emoji = kwargs.get("icon_emoji")
+        self.thread_ts = kwargs.get("thread_ts")
+        self.user = kwargs.get("user")
+        self.channel = kwargs.get("channel")
+        self.text = kwargs.get("text")
+        self.team = kwargs.get("team")
+        self.ts = kwargs.get("ts")  # pylint: disable=invalid-name
+        self.username = kwargs.get("username")
+        self.bot_id = kwargs.get("bot_id")
+        self.icons = kwargs.get("icons")
+        self.blocks: [Block] = kwargs.get("blocks")
+
+        self.attachments = None
+        if "attachments" in kwargs:
+            # Create proper Attachment objects
+            # It would appear that we can get dict fields from the wire that aren't defined
+            # in the Attachment class.  So only pass in known fields.
+            self.attachments = [
+                Attachment(**{x: att[x] for x in att if x in Attachment.attributes})
+                for att in kwargs.get("attachments")
+            ]
diff --git a/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_options.py b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_options.py
new file mode 100644
index 000000000..11cc9b62b
--- /dev/null
+++ b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_options.py
@@ -0,0 +1,53 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+
+class SlackAdapterOptions:
+    """
+    Defines the implementation of the SlackAdapter options.
+    """
+
+    def __init__(
+        self,
+        slack_verification_token: str,
+        slack_bot_token: str,
+        slack_client_signing_secret: str,
+    ):
+        """
+        Initializes a new instance of SlackAdapterOptions.
+
+        :param slack_verification_token: A token for validating the origin of incoming webhooks.
+        :type slack_verification_token: str
+        :param slack_bot_token: A token for a bot to work on a single workspace.
+        :type slack_bot_token: str
+        :param slack_client_signing_secret: The token used to validate that incoming webhooks originated from Slack.
+        :type slack_client_signing_secret: str
+        """
+        self.slack_verification_token = slack_verification_token
+        self.slack_bot_token = slack_bot_token
+        self.slack_client_signing_secret = slack_client_signing_secret
+        self.slack_client_id = None
+        self.slack_client_secret = None
+        self.slack_redirect_uri = None
+        self.slack_scopes = [str]
+
+    async def get_token_for_team(self, team_id: str) -> str:
+        """
+        Receives a Slack team ID and returns the bot token associated with that team. Required for multi-team apps.
+
+        :param team_id: The team ID.
+        :type team_id: str
+        :raises: :func:`NotImplementedError`
+        """
+        raise NotImplementedError()
+
+    async def get_bot_user_by_team(self, team_id: str) -> str:
+        """
+        A method that receives a Slack team ID and returns the bot user ID associated with that team. Required for
+         multi-team apps.
+
+        :param team_id: The team ID.
+        :type team_id: str
+        :raises: :func:`NotImplementedError`
+        """
+        raise NotImplementedError()
diff --git a/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_payload.py b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_payload.py
new file mode 100644
index 000000000..9b7438619
--- /dev/null
+++ b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_payload.py
@@ -0,0 +1,31 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from typing import Optional, List
+from slack.web.classes.actions import Action
+from botbuilder.adapters.slack.slack_message import SlackMessage
+
+
+class SlackPayload:
+    def __init__(self, **kwargs):
+        self.type: List[str] = kwargs.get("type")
+        self.token: str = kwargs.get("token")
+        self.channel: str = kwargs.get("channel")
+        self.thread_ts: str = kwargs.get("thread_ts")
+        self.team: str = kwargs.get("team")
+        self.user: str = kwargs.get("user")
+        self.actions: Optional[List[Action]] = None
+        self.trigger_id: str = kwargs.get("trigger_id")
+        self.action_ts: str = kwargs.get("action_ts")
+        self.submission: str = kwargs.get("submission")
+        self.callback_id: str = kwargs.get("callback_id")
+        self.state: str = kwargs.get("state")
+        self.response_url: str = kwargs.get("response_url")
+
+        if "message" in kwargs:
+            message = kwargs.get("message")
+            self.message = (
+                message
+                if isinstance(message) is SlackMessage
+                else SlackMessage(**message)
+            )
diff --git a/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_request_body.py b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_request_body.py
new file mode 100644
index 000000000..b8ad4bd06
--- /dev/null
+++ b/libraries/botbuilder-adapters-slack/botbuilder/adapters/slack/slack_request_body.py
@@ -0,0 +1,37 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from typing import List
+from botbuilder.adapters.slack.slack_event import SlackEvent
+from botbuilder.adapters.slack.slack_payload import SlackPayload
+
+
+class SlackRequestBody:
+    def __init__(self, **kwargs):
+        self.challenge = kwargs.get("challenge")
+        self.token = kwargs.get("token")
+        self.team_id = kwargs.get("team_id")
+        self.api_app_id = kwargs.get("api_app_id")
+        self.type = kwargs.get("type")
+        self.event_id = kwargs.get("event_id")
+        self.event_time = kwargs.get("event_time")
+        self.authed_users: List[str] = kwargs.get("authed_users")
+        self.trigger_id = kwargs.get("trigger_id")
+        self.channel_id = kwargs.get("channel_id")
+        self.user_id = kwargs.get("user_id")
+        self.text = kwargs.get("text")
+        self.command = kwargs.get("command")
+
+        self.payload: SlackPayload = None
+        if "payload" in kwargs:
+            payload = kwargs.get("payload")
+            self.payload = (
+                payload
+                if isinstance(payload, SlackPayload)
+                else SlackPayload(**payload)
+            )
+
+        self.event: SlackEvent = None
+        if "event" in kwargs:
+            event = kwargs.get("event")
+            self.event = event if isinstance(event, SlackEvent) else SlackEvent(**event)
diff --git a/libraries/botbuilder-adapters-slack/requirements.txt b/libraries/botbuilder-adapters-slack/requirements.txt
new file mode 100644
index 000000000..69aba26a6
--- /dev/null
+++ b/libraries/botbuilder-adapters-slack/requirements.txt
@@ -0,0 +1,4 @@
+aiohttp==3.6.2
+pyslack
+botbuilder-core==4.12.0
+slackclient
\ No newline at end of file
diff --git a/libraries/botbuilder-adapters-slack/setup.cfg b/libraries/botbuilder-adapters-slack/setup.cfg
new file mode 100644
index 000000000..57e1947c4
--- /dev/null
+++ b/libraries/botbuilder-adapters-slack/setup.cfg
@@ -0,0 +1,2 @@
+[bdist_wheel]
+universal=0
\ No newline at end of file
diff --git a/libraries/botbuilder-adapters-slack/setup.py b/libraries/botbuilder-adapters-slack/setup.py
new file mode 100644
index 000000000..0f121de69
--- /dev/null
+++ b/libraries/botbuilder-adapters-slack/setup.py
@@ -0,0 +1,49 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+from setuptools import setup
+
+REQUIRES = [
+    "botbuilder-schema==4.12.0",
+    "botframework-connector==4.12.0",
+    "botbuilder-core==4.12.0",
+    "pyslack",
+    "slackclient",
+]
+
+TEST_REQUIRES = ["aiounittest==1.3.0"]
+
+root = os.path.abspath(os.path.dirname(__file__))
+
+with open(os.path.join(root, "botbuilder", "adapters", "slack", "about.py")) as f:
+    package_info = {}
+    info = f.read()
+    exec(info, package_info)
+
+with open(os.path.join(root, "README.rst"), encoding="utf-8") as f:
+    long_description = f.read()
+
+setup(
+    name=package_info["__title__"],
+    version=package_info["__version__"],
+    url=package_info["__uri__"],
+    author=package_info["__author__"],
+    description=package_info["__description__"],
+    keywords=["BotBuilderAdapters", "bots", "ai", "botframework", "botbuilder"],
+    long_description=long_description,
+    long_description_content_type="text/x-rst",
+    license=package_info["__license__"],
+    packages=["botbuilder.adapters.slack"],
+    install_requires=REQUIRES + TEST_REQUIRES,
+    tests_require=TEST_REQUIRES,
+    include_package_data=True,
+    classifiers=[
+        "Programming Language :: Python :: 3.7",
+        "Intended Audience :: Developers",
+        "License :: OSI Approved :: MIT License",
+        "Operating System :: OS Independent",
+        "Development Status :: 5 - Production/Stable",
+        "Topic :: Scientific/Engineering :: Artificial Intelligence",
+    ],
+)
diff --git a/libraries/botbuilder-ai/README.rst b/libraries/botbuilder-ai/README.rst
index aef9094cc..c4a4269b9 100644
--- a/libraries/botbuilder-ai/README.rst
+++ b/libraries/botbuilder-ai/README.rst
@@ -3,8 +3,8 @@
 BotBuilder-AI SDK for Python
 ============================
 
-.. image:: https://fuselabs.visualstudio.com/SDK_v4/_apis/build/status/Python/SDK_v4-Python-CI?branchName=master
-   :target:  https://fuselabs.visualstudio.com/SDK_v4/_apis/build/status/Python/SDK_v4-Python-CI
+.. image:: https://dev.azure.com/FuseLabs/SDK_v4/_apis/build/status/Python/Python-CI-PR-yaml?branchName=master
+   :target:  https://dev.azure.com/FuseLabs/SDK_v4/_apis/build/status/Python/Python-CI-PR-yaml?branchName=master
    :align: right
    :alt: Azure DevOps status for master branch
 .. image:: https://badge.fury.io/py/botbuilder-ai.svg
diff --git a/libraries/botbuilder-ai/botbuilder/ai/about.py b/libraries/botbuilder-ai/botbuilder/ai/about.py
index 96d47800b..5439d7f89 100644
--- a/libraries/botbuilder-ai/botbuilder/ai/about.py
+++ b/libraries/botbuilder-ai/botbuilder/ai/about.py
@@ -1,14 +1,14 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import os
-
-__title__ = "botbuilder-ai"
-__version__ = (
-    os.environ["packageVersion"] if "packageVersion" in os.environ else "4.4.0b1"
-)
-__uri__ = "https://www.github.com/Microsoft/botbuilder-python"
-__author__ = "Microsoft"
-__description__ = "Microsoft Bot Framework Bot Builder"
-__summary__ = "Microsoft Bot Framework Bot Builder SDK for Python."
-__license__ = "MIT"
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+
+__title__ = "botbuilder-ai"
+__version__ = (
+    os.environ["packageVersion"] if "packageVersion" in os.environ else "4.12.0"
+)
+__uri__ = "https://www.github.com/Microsoft/botbuilder-python"
+__author__ = "Microsoft"
+__description__ = "Microsoft Bot Framework Bot Builder"
+__summary__ = "Microsoft Bot Framework Bot Builder SDK for Python."
+__license__ = "MIT"
diff --git a/libraries/botbuilder-ai/botbuilder/ai/luis/__init__.py b/libraries/botbuilder-ai/botbuilder/ai/luis/__init__.py
index cbee8bdc2..823d15dd9 100644
--- a/libraries/botbuilder-ai/botbuilder/ai/luis/__init__.py
+++ b/libraries/botbuilder-ai/botbuilder/ai/luis/__init__.py
@@ -2,12 +2,14 @@
 # Licensed under the MIT License.
 
 from .luis_application import LuisApplication
+from .luis_recognizer_options_v3 import LuisRecognizerOptionsV3
 from .luis_prediction_options import LuisPredictionOptions
 from .luis_telemetry_constants import LuisTelemetryConstants
 from .luis_recognizer import LuisRecognizer
 
 __all__ = [
     "LuisApplication",
+    "LuisRecognizerOptionsV3",
     "LuisPredictionOptions",
     "LuisRecognizer",
     "LuisTelemetryConstants",
diff --git a/libraries/botbuilder-ai/botbuilder/ai/luis/luis_application.py b/libraries/botbuilder-ai/botbuilder/ai/luis/luis_application.py
index 8d8f8e09d..3351b5882 100644
--- a/libraries/botbuilder-ai/botbuilder/ai/luis/luis_application.py
+++ b/libraries/botbuilder-ai/botbuilder/ai/luis/luis_application.py
@@ -13,12 +13,13 @@ class LuisApplication:
     """
 
     def __init__(self, application_id: str, endpoint_key: str, endpoint: str):
-        """Initializes a new instance of the  class.
+        """Initializes a new instance of the :class:`LuisApplication` class.
+
         :param application_id: LUIS application ID.
         :type application_id: str
         :param endpoint_key: LUIS subscription or endpoint key.
         :type endpoint_key: str
-        :param endpoint: LUIS endpoint to use like https://westus.api.cognitive.microsoft.com.
+        :param endpoint: LUIS endpoint to use, like https://westus.api.cognitive.microsoft.com.
         :type endpoint: str
         :raises ValueError:
         :raises ValueError:
@@ -46,7 +47,8 @@ def __init__(self, application_id: str, endpoint_key: str, endpoint: str):
 
     @classmethod
     def from_application_endpoint(cls, application_endpoint: str):
-        """Initializes a new instance of the  class.
+        """Initializes a new instance of the :class:`LuisApplication` class.
+
         :param application_endpoint: LUIS application endpoint.
         :type application_endpoint: str
         :return:
diff --git a/libraries/botbuilder-ai/botbuilder/ai/luis/luis_recognizer.py b/libraries/botbuilder-ai/botbuilder/ai/luis/luis_recognizer.py
index 6733e8a1c..8eef3e4dc 100644
--- a/libraries/botbuilder-ai/botbuilder/ai/luis/luis_recognizer.py
+++ b/libraries/botbuilder-ai/botbuilder/ai/luis/luis_recognizer.py
@@ -3,11 +3,6 @@
 
 import json
 from typing import Dict, List, Tuple, Union
-
-from azure.cognitiveservices.language.luis.runtime import LUISRuntimeClient
-from azure.cognitiveservices.language.luis.runtime.models import LuisResult
-from msrest.authentication import CognitiveServicesCredentials
-
 from botbuilder.core import (
     BotAssert,
     IntentScore,
@@ -16,15 +11,16 @@
     TurnContext,
 )
 from botbuilder.schema import ActivityTypes
-
 from . import LuisApplication, LuisPredictionOptions, LuisTelemetryConstants
-from .activity_util import ActivityUtil
-from .luis_util import LuisUtil
+from .luis_recognizer_v3 import LuisRecognizerV3
+from .luis_recognizer_v2 import LuisRecognizerV2
+from .luis_recognizer_options_v2 import LuisRecognizerOptionsV2
+from .luis_recognizer_options_v3 import LuisRecognizerOptionsV3
 
 
 class LuisRecognizer(Recognizer):
     """
-    A LUIS based implementation of .
+    A LUIS based implementation of :class:`botbuilder.core.Recognizer`.
     """
 
     # The value type for a LUIS trace activity.
@@ -36,17 +32,20 @@ class LuisRecognizer(Recognizer):
     def __init__(
         self,
         application: Union[LuisApplication, str],
-        prediction_options: LuisPredictionOptions = None,
+        prediction_options: Union[
+            LuisRecognizerOptionsV2, LuisRecognizerOptionsV3, LuisPredictionOptions
+        ] = None,
         include_api_results: bool = False,
     ):
-        """Initializes a new instance of the  class.
+        """Initializes a new instance of the :class:`LuisRecognizer` class.
+
         :param application: The LUIS application to use to recognize text.
-        :type application: LuisApplication
-        :param prediction_options: The LUIS prediction options to use, defaults to None
-        :param prediction_options: LuisPredictionOptions, optional
-        :param include_api_results: True to include raw LUIS API response, defaults to False
-        :param include_api_results: bool, optional
-        :raises TypeError:
+        :type application: :class:`LuisApplication`
+        :param prediction_options: The LUIS prediction options to use, defaults to None.
+        :type prediction_options: :class:`LuisPredictionOptions`, optional
+        :param include_api_results: True to include raw LUIS API response, defaults to False.
+        :type include_api_results: bool, optional
+        :raises: TypeError
         """
 
         if isinstance(application, LuisApplication):
@@ -59,30 +58,31 @@ def __init__(
             )
 
         self._options = prediction_options or LuisPredictionOptions()
-
-        self._include_api_results = include_api_results
+        self._include_api_results = include_api_results or (
+            prediction_options.include_api_results
+            if isinstance(
+                prediction_options, (LuisRecognizerOptionsV3, LuisRecognizerOptionsV2)
+            )
+            else False
+        )
 
         self.telemetry_client = self._options.telemetry_client
         self.log_personal_information = self._options.log_personal_information
 
-        credentials = CognitiveServicesCredentials(self._application.endpoint_key)
-        self._runtime = LUISRuntimeClient(self._application.endpoint, credentials)
-        self._runtime.config.add_user_agent(LuisUtil.get_user_agent())
-        self._runtime.config.connection.timeout = self._options.timeout // 1000
-
     @staticmethod
     def top_intent(
         results: RecognizerResult, default_intent: str = "None", min_score: float = 0.0
     ) -> str:
         """Returns the name of the top scoring intent from a set of LUIS results.
+
         :param results: Result set to be searched.
-        :type results: RecognizerResult
-        :param default_intent: Intent name to return should a top intent be found, defaults to "None"
-        :param default_intent: str, optional
-        :param min_score: Minimum score needed for an intent to be considered as a top intent. If all intents in the
-         set are below this threshold then the `defaultIntent` will be returned, defaults to 0.0
-        :param min_score: float, optional
-        :raises TypeError:
+        :type results: :class:`botbuilder.core.RecognizerResult`
+        :param default_intent: Intent name to return should a top intent be found, defaults to None.
+        :type default_intent: str, optional
+        :param min_score: Minimum score needed for an intent to be considered as a top intent. If all intents in the set
+         are below this threshold then the `defaultIntent` is returned, defaults to 0.0.
+        :type min_score: float, optional
+        :raises: TypeError
         :return: The top scoring intent name.
         :rtype: str
         """
@@ -108,17 +108,18 @@ async def recognize(  # pylint: disable=arguments-differ
         telemetry_metrics: Dict[str, float] = None,
         luis_prediction_options: LuisPredictionOptions = None,
     ) -> RecognizerResult:
-        """Return results of the analysis (Suggested actions and intents).
-        :param turn_context: Context object containing information for a single turn of conversation with a user.
-        :type turn_context: TurnContext
+        """Return results of the analysis (suggested actions and intents).
+
+        :param turn_context: Context object containing information for a single conversation turn with a user.
+        :type turn_context: :class:`botbuilder.core.TurnContext`
         :param telemetry_properties: Additional properties to be logged to telemetry with the LuisResult event, defaults
-         to None
-        :param telemetry_properties: Dict[str, str], optional
+         to None.
+        :type telemetry_properties: :class:`typing.Dict[str, str]`, optional
         :param telemetry_metrics: Additional metrics to be logged to telemetry with the LuisResult event, defaults to
-         None
-        :param telemetry_metrics: Dict[str, float], optional
+         None.
+        :type telemetry_metrics: :class:`typing.Dict[str, float]`, optional
         :return: The LUIS results of the analysis of the current message text in the current turn's context activity.
-        :rtype: RecognizerResult
+        :rtype: :class:`botbuilder.core.RecognizerResult`
         """
 
         return await self._recognize_internal(
@@ -136,16 +137,17 @@ def on_recognizer_result(
         telemetry_metrics: Dict[str, float] = None,
     ):
         """Invoked prior to a LuisResult being logged.
-        :param recognizer_result: The Luis Results for the call.
-        :type recognizer_result: RecognizerResult
+
+        :param recognizer_result: The LuisResult for the call.
+        :type recognizer_result: :class:`botbuilder.core.RecognizerResult`
         :param turn_context: Context object containing information for a single turn of conversation with a user.
-        :type turn_context: TurnContext
+        :type turn_context: :class:`botbuilder.core.TurnContext`
         :param telemetry_properties: Additional properties to be logged to telemetry with the LuisResult event, defaults
-         to None
-        :param telemetry_properties: Dict[str, str], optional
-        :param telemetry_metrics: Additional metrics to be logged to telemetry with the LuisResult event, defaults to
-         None
-        :param telemetry_metrics: Dict[str, float], optional
+         to None.
+        :type telemetry_properties: :class:`typing.Dict[str, str]`, optional
+        :param telemetry_metrics: Additional metrics to be logged to telemetry with the LuisResult event, defaults
+         to None.
+        :type telemetry_metrics: :class:`typing.Dict[str, float]`, optional
         """
 
         properties = self.fill_luis_event_properties(
@@ -180,16 +182,17 @@ def fill_luis_event_properties(
     ) -> Dict[str, str]:
         """Fills the event properties for LuisResult event for telemetry.
         These properties are logged when the recognizer is called.
+
         :param recognizer_result: Last activity sent from user.
-        :type recognizer_result: RecognizerResult
+        :type recognizer_result: :class:`botbuilder.core.RecognizerResult`
         :param turn_context: Context object containing information for a single turn of conversation with a user.
-        :type turn_context: TurnContext
-        :param telemetry_properties: Additional properties to be logged to telemetry with the LuisResult event,
-         defaults to None
-        :param telemetry_properties: Dict[str, str], optional
-        :return: A dictionary that is sent as "Properties" to IBotTelemetryClient.TrackEvent method for the
+        :type turn_context: :class:`botbuilder.core.TurnContext`
+        :param telemetry_properties: Additional properties to be logged to telemetry with the LuisResult event, defaults
+         to None.
+        :type telemetry_properties: :class:`typing.Dict[str, str]`, optional
+        :return: A dictionary sent as "Properties" to :func:`botbuilder.core.BotTelemetryClient.track_event` for the
          BotMessageSend event.
-        :rtype: Dict[str, str]
+        :rtype: `typing.Dict[str, str]`
         """
 
         intents = recognizer_result.intents
@@ -249,7 +252,9 @@ async def _recognize_internal(
         turn_context: TurnContext,
         telemetry_properties: Dict[str, str],
         telemetry_metrics: Dict[str, float],
-        luis_prediction_options: LuisPredictionOptions = None,
+        luis_prediction_options: Union[
+            LuisPredictionOptions, LuisRecognizerOptionsV2, LuisRecognizerOptionsV3
+        ] = None,
     ) -> RecognizerResult:
 
         BotAssert.context_not_none(turn_context)
@@ -259,10 +264,9 @@ async def _recognize_internal(
 
         utterance: str = turn_context.activity.text if turn_context.activity is not None else None
         recognizer_result: RecognizerResult = None
-        luis_result: LuisResult = None
 
         if luis_prediction_options:
-            options = self._merge_options(luis_prediction_options)
+            options = luis_prediction_options
         else:
             options = self._options
 
@@ -271,71 +275,49 @@ async def _recognize_internal(
                 text=utterance, intents={"": IntentScore(score=1.0)}, entities={}
             )
         else:
-            luis_result = self._runtime.prediction.resolve(
-                self._application.application_id,
-                utterance,
-                timezone_offset=options.timezone_offset,
-                verbose=options.include_all_intents,
-                staging=options.staging,
-                spell_check=options.spell_check,
-                bing_spell_check_subscription_key=options.bing_spell_check_subscription_key,
-                log=options.log if options.log is not None else True,
-            )
 
-            recognizer_result = RecognizerResult(
-                text=utterance,
-                altered_text=luis_result.altered_query,
-                intents=LuisUtil.get_intents(luis_result),
-                entities=LuisUtil.extract_entities_and_metadata(
-                    luis_result.entities,
-                    luis_result.composite_entities,
-                    options.include_instance_data
-                    if options.include_instance_data is not None
-                    else True,
-                ),
-            )
-            LuisUtil.add_properties(luis_result, recognizer_result)
-            if self._include_api_results:
-                recognizer_result.properties["luisResult"] = luis_result
+            luis_recognizer = self._build_recognizer(options)
+            recognizer_result = await luis_recognizer.recognizer_internal(turn_context)
 
         # Log telemetry
         self.on_recognizer_result(
             recognizer_result, turn_context, telemetry_properties, telemetry_metrics
         )
 
-        await self._emit_trace_info(
-            turn_context, luis_result, recognizer_result, options
-        )
-
         return recognizer_result
 
-    async def _emit_trace_info(
-        self,
-        turn_context: TurnContext,
-        luis_result: LuisResult,
-        recognizer_result: RecognizerResult,
-        options: LuisPredictionOptions,
-    ) -> None:
-        trace_info: Dict[str, object] = {
-            "recognizerResult": LuisUtil.recognizer_result_as_dict(recognizer_result),
-            "luisModel": {"ModelID": self._application.application_id},
-            "luisOptions": {"Staging": options.staging},
-            "luisResult": LuisUtil.luis_result_as_dict(luis_result),
-        }
-
-        trace_activity = ActivityUtil.create_trace(
-            turn_context.activity,
-            "LuisRecognizer",
-            trace_info,
-            LuisRecognizer.luis_trace_type,
-            LuisRecognizer.luis_trace_label,
-        )
-
-        await turn_context.send_activity(trace_activity)
-
     def _merge_options(
-        self, user_defined_options: LuisPredictionOptions
+        self,
+        user_defined_options: Union[
+            LuisRecognizerOptionsV3, LuisRecognizerOptionsV2, LuisPredictionOptions
+        ],
     ) -> LuisPredictionOptions:
         merged_options = LuisPredictionOptions()
         merged_options.__dict__.update(user_defined_options.__dict__)
         return merged_options
+
+    def _build_recognizer(
+        self,
+        luis_prediction_options: Union[
+            LuisRecognizerOptionsV3, LuisRecognizerOptionsV2, LuisPredictionOptions
+        ],
+    ):
+        if isinstance(luis_prediction_options, LuisRecognizerOptionsV3):
+            return LuisRecognizerV3(self._application, luis_prediction_options)
+        if isinstance(luis_prediction_options, LuisRecognizerOptionsV2):
+            return LuisRecognizerV3(self._application, luis_prediction_options)
+
+        recognizer_options = LuisRecognizerOptionsV2(
+            luis_prediction_options.bing_spell_check_subscription_key,
+            luis_prediction_options.include_all_intents,
+            luis_prediction_options.include_instance_data,
+            luis_prediction_options.log,
+            luis_prediction_options.spell_check,
+            luis_prediction_options.staging,
+            luis_prediction_options.timeout,
+            luis_prediction_options.timezone_offset,
+            self._include_api_results,
+            luis_prediction_options.telemetry_client,
+            luis_prediction_options.log_personal_information,
+        )
+        return LuisRecognizerV2(self._application, recognizer_options)
diff --git a/libraries/botbuilder-ai/botbuilder/ai/luis/luis_recognizer_internal.py b/libraries/botbuilder-ai/botbuilder/ai/luis/luis_recognizer_internal.py
new file mode 100644
index 000000000..66ec5a4ce
--- /dev/null
+++ b/libraries/botbuilder-ai/botbuilder/ai/luis/luis_recognizer_internal.py
@@ -0,0 +1,18 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from abc import ABC, abstractmethod
+from botbuilder.core import TurnContext
+from .luis_application import LuisApplication
+
+
+class LuisRecognizerInternal(ABC):
+    def __init__(self, luis_application: LuisApplication):
+        if luis_application is None:
+            raise TypeError(luis_application.__class__.__name__)
+
+        self.luis_application = luis_application
+
+    @abstractmethod
+    async def recognizer_internal(self, turn_context: TurnContext):
+        raise NotImplementedError()
diff --git a/libraries/botbuilder-ai/botbuilder/ai/luis/luis_recognizer_options.py b/libraries/botbuilder-ai/botbuilder/ai/luis/luis_recognizer_options.py
new file mode 100644
index 000000000..4368aa443
--- /dev/null
+++ b/libraries/botbuilder-ai/botbuilder/ai/luis/luis_recognizer_options.py
@@ -0,0 +1,16 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.core import BotTelemetryClient, NullTelemetryClient
+
+
+class LuisRecognizerOptions:
+    def __init__(
+        self,
+        include_api_results: bool = None,
+        telemetry_client: BotTelemetryClient = NullTelemetryClient(),
+        log_personal_information: bool = False,
+    ):
+        self.include_api_results = include_api_results
+        self.telemetry_client = telemetry_client
+        self.log_personal_information = log_personal_information
diff --git a/libraries/botbuilder-ai/botbuilder/ai/luis/luis_recognizer_options_v2.py b/libraries/botbuilder-ai/botbuilder/ai/luis/luis_recognizer_options_v2.py
new file mode 100644
index 000000000..a06c6c5cc
--- /dev/null
+++ b/libraries/botbuilder-ai/botbuilder/ai/luis/luis_recognizer_options_v2.py
@@ -0,0 +1,33 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.core import BotTelemetryClient, NullTelemetryClient
+from .luis_recognizer_options import LuisRecognizerOptions
+
+
+class LuisRecognizerOptionsV2(LuisRecognizerOptions):
+    def __init__(
+        self,
+        bing_spell_check_subscription_key: str = None,
+        include_all_intents: bool = None,
+        include_instance_data: bool = True,
+        log: bool = True,
+        spell_check: bool = None,
+        staging: bool = None,
+        timeout: float = 100000,
+        timezone_offset: float = None,
+        include_api_results: bool = True,
+        telemetry_client: BotTelemetryClient = NullTelemetryClient(),
+        log_personal_information: bool = False,
+    ):
+        super().__init__(
+            include_api_results, telemetry_client, log_personal_information
+        )
+        self.bing_spell_check_subscription_key = bing_spell_check_subscription_key
+        self.include_all_intents = include_all_intents
+        self.include_instance_data = include_instance_data
+        self.log = log
+        self.spell_check = spell_check
+        self.staging = staging
+        self.timeout = timeout
+        self.timezone_offset = timezone_offset
diff --git a/libraries/botbuilder-ai/botbuilder/ai/luis/luis_recognizer_options_v3.py b/libraries/botbuilder-ai/botbuilder/ai/luis/luis_recognizer_options_v3.py
new file mode 100644
index 000000000..4793e36f8
--- /dev/null
+++ b/libraries/botbuilder-ai/botbuilder/ai/luis/luis_recognizer_options_v3.py
@@ -0,0 +1,37 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from typing import List
+
+from botbuilder.core import BotTelemetryClient, NullTelemetryClient
+from .luis_recognizer_options import LuisRecognizerOptions
+
+
+class LuisRecognizerOptionsV3(LuisRecognizerOptions):
+    def __init__(
+        self,
+        include_all_intents: bool = False,
+        include_instance_data: bool = True,
+        log: bool = True,
+        prefer_external_entities: bool = True,
+        datetime_reference: str = None,
+        dynamic_lists: List = None,
+        external_entities: List = None,
+        slot: str = "production",
+        version: str = None,
+        include_api_results: bool = True,
+        telemetry_client: BotTelemetryClient = NullTelemetryClient(),
+        log_personal_information: bool = False,
+    ):
+        super().__init__(
+            include_api_results, telemetry_client, log_personal_information
+        )
+        self.include_all_intents = include_all_intents
+        self.include_instance_data = include_instance_data
+        self.log = log
+        self.prefer_external_entities = prefer_external_entities
+        self.datetime_reference = datetime_reference
+        self.dynamic_lists = dynamic_lists
+        self.external_entities = external_entities
+        self.slot = slot
+        self.version: str = version
diff --git a/libraries/botbuilder-ai/botbuilder/ai/luis/luis_recognizer_v2.py b/libraries/botbuilder-ai/botbuilder/ai/luis/luis_recognizer_v2.py
new file mode 100644
index 000000000..c1ed5ed6b
--- /dev/null
+++ b/libraries/botbuilder-ai/botbuilder/ai/luis/luis_recognizer_v2.py
@@ -0,0 +1,109 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from typing import Dict
+from azure.cognitiveservices.language.luis.runtime import LUISRuntimeClient
+from azure.cognitiveservices.language.luis.runtime.models import LuisResult
+from msrest.authentication import CognitiveServicesCredentials
+from botbuilder.core import (
+    TurnContext,
+    RecognizerResult,
+)
+from .luis_recognizer_internal import LuisRecognizerInternal
+from .luis_recognizer_options_v2 import LuisRecognizerOptionsV2
+from .luis_application import LuisApplication
+from .luis_util import LuisUtil
+
+from .activity_util import ActivityUtil
+
+
+class LuisRecognizerV2(LuisRecognizerInternal):
+
+    # The value type for a LUIS trace activity.
+    luis_trace_type: str = "https://www.luis.ai/schemas/trace"
+
+    # The context label for a LUIS trace activity.
+    luis_trace_label: str = "Luis Trace"
+
+    def __init__(
+        self,
+        luis_application: LuisApplication,
+        luis_recognizer_options_v2: LuisRecognizerOptionsV2 = None,
+    ):
+        super().__init__(luis_application)
+        credentials = CognitiveServicesCredentials(luis_application.endpoint_key)
+        self._runtime = LUISRuntimeClient(luis_application.endpoint, credentials)
+        self._runtime.config.add_user_agent(LuisUtil.get_user_agent())
+        self._runtime.config.connection.timeout = (
+            luis_recognizer_options_v2.timeout // 1000
+        )
+        self.luis_recognizer_options_v2 = (
+            luis_recognizer_options_v2 or LuisRecognizerOptionsV2()
+        )
+        self._application = luis_application
+
+    async def recognizer_internal(self, turn_context: TurnContext):
+
+        utterance: str = turn_context.activity.text if turn_context.activity is not None else None
+        luis_result: LuisResult = self._runtime.prediction.resolve(
+            self._application.application_id,
+            utterance,
+            timezone_offset=self.luis_recognizer_options_v2.timezone_offset,
+            verbose=self.luis_recognizer_options_v2.include_all_intents,
+            staging=self.luis_recognizer_options_v2.staging,
+            spell_check=self.luis_recognizer_options_v2.spell_check,
+            bing_spell_check_subscription_key=self.luis_recognizer_options_v2.bing_spell_check_subscription_key,
+            log=self.luis_recognizer_options_v2.log
+            if self.luis_recognizer_options_v2.log is not None
+            else True,
+        )
+
+        recognizer_result: RecognizerResult = RecognizerResult(
+            text=utterance,
+            altered_text=luis_result.altered_query,
+            intents=LuisUtil.get_intents(luis_result),
+            entities=LuisUtil.extract_entities_and_metadata(
+                luis_result.entities,
+                luis_result.composite_entities,
+                self.luis_recognizer_options_v2.include_instance_data
+                if self.luis_recognizer_options_v2.include_instance_data is not None
+                else True,
+            ),
+        )
+
+        LuisUtil.add_properties(luis_result, recognizer_result)
+        if self.luis_recognizer_options_v2.include_api_results:
+            recognizer_result.properties["luisResult"] = luis_result
+
+        await self._emit_trace_info(
+            turn_context,
+            luis_result,
+            recognizer_result,
+            self.luis_recognizer_options_v2,
+        )
+
+        return recognizer_result
+
+    async def _emit_trace_info(
+        self,
+        turn_context: TurnContext,
+        luis_result: LuisResult,
+        recognizer_result: RecognizerResult,
+        options: LuisRecognizerOptionsV2,
+    ) -> None:
+        trace_info: Dict[str, object] = {
+            "recognizerResult": LuisUtil.recognizer_result_as_dict(recognizer_result),
+            "luisModel": {"ModelID": self._application.application_id},
+            "luisOptions": {"Staging": options.staging},
+            "luisResult": LuisUtil.luis_result_as_dict(luis_result),
+        }
+
+        trace_activity = ActivityUtil.create_trace(
+            turn_context.activity,
+            "LuisRecognizer",
+            trace_info,
+            LuisRecognizerV2.luis_trace_type,
+            LuisRecognizerV2.luis_trace_label,
+        )
+
+        await turn_context.send_activity(trace_activity)
diff --git a/libraries/botbuilder-ai/botbuilder/ai/luis/luis_recognizer_v3.py b/libraries/botbuilder-ai/botbuilder/ai/luis/luis_recognizer_v3.py
new file mode 100644
index 000000000..61fdfef6f
--- /dev/null
+++ b/libraries/botbuilder-ai/botbuilder/ai/luis/luis_recognizer_v3.py
@@ -0,0 +1,286 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import re
+from typing import Dict
+
+import aiohttp
+from botbuilder.ai.luis.activity_util import ActivityUtil
+from botbuilder.ai.luis.luis_util import LuisUtil
+from botbuilder.core import (
+    IntentScore,
+    RecognizerResult,
+    TurnContext,
+)
+from .luis_recognizer_internal import LuisRecognizerInternal
+from .luis_recognizer_options_v3 import LuisRecognizerOptionsV3
+from .luis_application import LuisApplication
+
+
+# from .activity_util import ActivityUtil
+
+
+class LuisRecognizerV3(LuisRecognizerInternal):
+    _dateSubtypes = [
+        "date",
+        "daterange",
+        "datetime",
+        "datetimerange",
+        "duration",
+        "set",
+        "time",
+        "timerange",
+    ]
+    _geographySubtypes = ["poi", "city", "countryRegion", "continent", "state"]
+    _metadata_key = "$instance"
+
+    # The value type for a LUIS trace activity.
+    luis_trace_type: str = "https://www.luis.ai/schemas/trace"
+
+    # The context label for a LUIS trace activity.
+    luis_trace_label: str = "Luis Trace"
+
+    def __init__(
+        self,
+        luis_application: LuisApplication,
+        luis_recognizer_options_v3: LuisRecognizerOptionsV3 = None,
+    ):
+        super().__init__(luis_application)
+
+        self.luis_recognizer_options_v3 = (
+            luis_recognizer_options_v3 or LuisRecognizerOptionsV3()
+        )
+        self._application = luis_application
+
+    async def recognizer_internal(self, turn_context: TurnContext):
+        recognizer_result: RecognizerResult = None
+
+        utterance: str = turn_context.activity.text if turn_context.activity is not None else None
+
+        url = self._build_url()
+        body = self._build_request(utterance)
+        headers = {
+            "Ocp-Apim-Subscription-Key": self.luis_application.endpoint_key,
+            "Content-Type": "application/json",
+        }
+
+        async with aiohttp.ClientSession() as session:
+            async with session.post(
+                url, json=body, headers=headers, ssl=False
+            ) as result:
+                luis_result = await result.json()
+
+                recognizer_result = RecognizerResult(
+                    text=utterance,
+                    intents=self._get_intents(luis_result["prediction"]),
+                    entities=self._extract_entities_and_metadata(
+                        luis_result["prediction"]
+                    ),
+                )
+
+                if self.luis_recognizer_options_v3.include_instance_data:
+                    recognizer_result.entities[self._metadata_key] = (
+                        recognizer_result.entities[self._metadata_key]
+                        if self._metadata_key in recognizer_result.entities
+                        else {}
+                    )
+
+                if "sentiment" in luis_result["prediction"]:
+                    recognizer_result.properties["sentiment"] = self._get_sentiment(
+                        luis_result["prediction"]
+                    )
+
+                await self._emit_trace_info(
+                    turn_context,
+                    luis_result,
+                    recognizer_result,
+                    self.luis_recognizer_options_v3,
+                )
+
+        return recognizer_result
+
+    def _build_url(self):
+
+        base_uri = (
+            self._application.endpoint or "https://westus.api.cognitive.microsoft.com"
+        )
+        uri = "%s/luis/prediction/v3.0/apps/%s" % (
+            base_uri,
+            self._application.application_id,
+        )
+
+        if self.luis_recognizer_options_v3.version:
+            uri += "/versions/%s/predict" % (self.luis_recognizer_options_v3.version)
+        else:
+            uri += "/slots/%s/predict" % (self.luis_recognizer_options_v3.slot)
+
+        params = "?verbose=%s&show-all-intents=%s&log=%s" % (
+            "true"
+            if self.luis_recognizer_options_v3.include_instance_data
+            else "false",
+            "true" if self.luis_recognizer_options_v3.include_all_intents else "false",
+            "true" if self.luis_recognizer_options_v3.log else "false",
+        )
+
+        return uri + params
+
+    def _build_request(self, utterance: str):
+        body = {
+            "query": utterance,
+            "options": {
+                "preferExternalEntities": self.luis_recognizer_options_v3.prefer_external_entities,
+            },
+        }
+
+        if self.luis_recognizer_options_v3.datetime_reference:
+            body["options"][
+                "datetimeReference"
+            ] = self.luis_recognizer_options_v3.datetime_reference
+
+        if self.luis_recognizer_options_v3.dynamic_lists:
+            body["dynamicLists"] = self.luis_recognizer_options_v3.dynamic_lists
+
+        if self.luis_recognizer_options_v3.external_entities:
+            body["externalEntities"] = self.luis_recognizer_options_v3.external_entities
+
+        return body
+
+    def _get_intents(self, luis_result):
+        intents = {}
+        if not luis_result["intents"]:
+            return intents
+
+        for intent in luis_result["intents"]:
+            intents[self._normalize_name(intent)] = IntentScore(
+                luis_result["intents"][intent]["score"]
+            )
+
+        return intents
+
+    def _normalize_name(self, name):
+        return re.sub(r"\.", "_", name)
+
+    def _normalize(self, entity):
+        split_entity = entity.split(":")
+        entity_name = split_entity[-1]
+        return self._normalize_name(entity_name)
+
+    def _extract_entities_and_metadata(self, luis_result):
+        entities = luis_result["entities"]
+        return self._map_properties(entities, False)
+
+    def _map_properties(self, source, in_instance):
+
+        if isinstance(source, (int, float, bool, str)):
+            return source
+
+        result = source
+        if isinstance(source, list):
+            narr = []
+            for item in source:
+                is_geography_v2 = ""
+                if (
+                    isinstance(item, dict)
+                    and "type" in item
+                    and item["type"] in self._geographySubtypes
+                ):
+                    is_geography_v2 = item["type"]
+
+                if not in_instance and is_geography_v2:
+                    geo_entity = {}
+                    for item_props in item:
+                        if item_props == "value":
+                            geo_entity["location"] = item[item_props]
+
+                    geo_entity["type"] = is_geography_v2
+                    narr.append(geo_entity)
+                else:
+                    narr.append(self._map_properties(item, in_instance))
+
+            result = narr
+
+        elif not isinstance(source, str):
+            nobj = {}
+            if (
+                not in_instance
+                and isinstance(source, dict)
+                and "type" in source
+                and isinstance(source["type"], str)
+                and source["type"] in self._dateSubtypes
+            ):
+                timexs = source["values"]
+                arr = []
+                if timexs:
+                    unique = []
+                    for elt in timexs:
+                        if elt["timex"] and elt["timex"] not in unique:
+                            unique.append(elt["timex"])
+
+                    for timex in unique:
+                        arr.append(timex)
+
+                    nobj["timex"] = arr
+
+                nobj["type"] = source["type"]
+
+            else:
+                for property in source:
+                    name = self._normalize(property)
+                    is_array = isinstance(source[property], list)
+                    is_string = isinstance(source[property], str)
+                    is_int = isinstance(source[property], (int, float))
+                    val = self._map_properties(
+                        source[property], in_instance or property == self._metadata_key
+                    )
+                    if name == "datetime" and is_array:
+                        nobj["datetimeV1"] = val
+
+                    elif name == "datetimeV2" and is_array:
+                        nobj["datetime"] = val
+
+                    elif in_instance:
+                        if name == "length" and is_int:
+                            nobj["endIndex"] = source[name] + source["startIndex"]
+                        elif not (
+                            (is_int and name == "modelTypeId")
+                            or (is_string and name == "role")
+                        ):
+                            nobj[name] = val
+                    else:
+                        if name == "unit" and is_string:
+                            nobj["units"] = val
+                        else:
+                            nobj[name] = val
+
+            result = nobj
+        return result
+
+    def _get_sentiment(self, luis_result):
+        return {
+            "label": luis_result["sentiment"]["label"],
+            "score": luis_result["sentiment"]["score"],
+        }
+
+    async def _emit_trace_info(
+        self,
+        turn_context: TurnContext,
+        luis_result,
+        recognizer_result: RecognizerResult,
+        options: LuisRecognizerOptionsV3,
+    ) -> None:
+        trace_info: Dict[str, object] = {
+            "recognizerResult": LuisUtil.recognizer_result_as_dict(recognizer_result),
+            "luisModel": {"ModelID": self._application.application_id},
+            "luisOptions": {"Slot": options.slot},
+            "luisResult": luis_result,
+        }
+
+        trace_activity = ActivityUtil.create_trace(
+            turn_context.activity,
+            "LuisRecognizer",
+            trace_info,
+            LuisRecognizerV3.luis_trace_type,
+            LuisRecognizerV3.luis_trace_label,
+        )
+
+        await turn_context.send_activity(trace_activity)
diff --git a/libraries/botbuilder-ai/botbuilder/ai/luis/luis_util.py b/libraries/botbuilder-ai/botbuilder/ai/luis/luis_util.py
index d9ea75df8..9f620be34 100644
--- a/libraries/botbuilder-ai/botbuilder/ai/luis/luis_util.py
+++ b/libraries/botbuilder-ai/botbuilder/ai/luis/luis_util.py
@@ -299,7 +299,7 @@ def get_user_agent():
 
     @staticmethod
     def recognizer_result_as_dict(
-        recognizer_result: RecognizerResult
+        recognizer_result: RecognizerResult,
     ) -> Dict[str, object]:
         # an internal method that returns a dict for json serialization.
 
diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/__init__.py b/libraries/botbuilder-ai/botbuilder/ai/qna/__init__.py
index dc3f3ccba..938010a71 100644
--- a/libraries/botbuilder-ai/botbuilder/ai/qna/__init__.py
+++ b/libraries/botbuilder-ai/botbuilder/ai/qna/__init__.py
@@ -1,39 +1,41 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from .qnamaker import QnAMaker
-from .qnamaker_endpoint import QnAMakerEndpoint
-from .qnamaker_options import QnAMakerOptions
-from .qnamaker_telemetry_client import QnAMakerTelemetryClient
-from .utils import (
-    ActiveLearningUtils,
-    GenerateAnswerUtils,
-    HttpRequestUtils,
-    QnATelemetryConstants,
-)
-
-from .models import (
-    FeedbackRecord,
-    FeedbackRecords,
-    Metadata,
-    QnAMakerTraceInfo,
-    QueryResult,
-    QueryResults,
-)
-
-__all__ = [
-    "ActiveLearningUtils",
-    "FeedbackRecord",
-    "FeedbackRecords",
-    "GenerateAnswerUtils",
-    "HttpRequestUtils",
-    "Metadata",
-    "QueryResult",
-    "QueryResults",
-    "QnAMaker",
-    "QnAMakerEndpoint",
-    "QnAMakerOptions",
-    "QnAMakerTelemetryClient",
-    "QnAMakerTraceInfo",
-    "QnATelemetryConstants",
-]
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from .qnamaker import QnAMaker
+from .qnamaker_endpoint import QnAMakerEndpoint
+from .qnamaker_options import QnAMakerOptions
+from .qnamaker_telemetry_client import QnAMakerTelemetryClient
+from .qna_dialog_response_options import QnADialogResponseOptions
+from .utils import (
+    ActiveLearningUtils,
+    GenerateAnswerUtils,
+    HttpRequestUtils,
+    QnATelemetryConstants,
+)
+
+from .models import (
+    FeedbackRecord,
+    FeedbackRecords,
+    Metadata,
+    QnAMakerTraceInfo,
+    QueryResult,
+    QueryResults,
+)
+
+__all__ = [
+    "ActiveLearningUtils",
+    "FeedbackRecord",
+    "FeedbackRecords",
+    "GenerateAnswerUtils",
+    "HttpRequestUtils",
+    "Metadata",
+    "QueryResult",
+    "QueryResults",
+    "QnAMaker",
+    "QnAMakerEndpoint",
+    "QnAMakerOptions",
+    "QnAMakerTelemetryClient",
+    "QnAMakerTraceInfo",
+    "QnATelemetryConstants",
+    "QnADialogResponseOptions",
+]
diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/dialogs/__init__.py b/libraries/botbuilder-ai/botbuilder/ai/qna/dialogs/__init__.py
new file mode 100644
index 000000000..a6fb238d1
--- /dev/null
+++ b/libraries/botbuilder-ai/botbuilder/ai/qna/dialogs/__init__.py
@@ -0,0 +1,14 @@
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+# --------------------------------------------------------------------------
+
+from .qnamaker_dialog import QnAMakerDialog
+from .qnamaker_dialog_options import QnAMakerDialogOptions
+
+__all__ = [
+    "QnAMakerDialogOptions",
+    "QnAMakerDialog",
+]
diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/dialogs/qnamaker_dialog.py b/libraries/botbuilder-ai/botbuilder/ai/qna/dialogs/qnamaker_dialog.py
new file mode 100644
index 000000000..f1b052207
--- /dev/null
+++ b/libraries/botbuilder-ai/botbuilder/ai/qna/dialogs/qnamaker_dialog.py
@@ -0,0 +1,467 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+from typing import List
+
+from botbuilder.dialogs import (
+    WaterfallDialog,
+    WaterfallStepContext,
+    DialogContext,
+    DialogTurnResult,
+    Dialog,
+    ObjectPath,
+    DialogTurnStatus,
+    DialogReason,
+)
+from botbuilder.schema import Activity, ActivityTypes
+
+from .qnamaker_dialog_options import QnAMakerDialogOptions
+from .. import (
+    QnAMakerOptions,
+    QnADialogResponseOptions,
+    QnAMaker,
+    QnAMakerEndpoint,
+)
+from ..models import QnARequestContext, Metadata, QueryResult, FeedbackRecord
+from ..models.ranker_types import RankerTypes
+from ..utils import QnACardBuilder
+
+
+class QnAMakerDialog(WaterfallDialog):
+    """
+    A dialog that supports multi-step and adaptive-learning QnA Maker services.
+
+    .. remarks::
+      An instance of this class targets a specific QnA Maker knowledge base.
+      It supports knowledge bases that include follow-up prompt and active learning features.
+    """
+
+    KEY_QNA_CONTEXT_DATA = "qnaContextData"
+    """
+    The path for storing and retrieving QnA Maker context data.
+
+    .. remarks:
+      This represents context about the current or previous call to QnA Maker.
+      It is stored within the current step's :class:'botbuilder.dialogs.WaterfallStepContext'.
+      It supports QnA Maker's follow-up prompt and active learning features.
+    """
+
+    KEY_PREVIOUS_QNA_ID = "prevQnAId"
+    """
+    The path for storing and retrieving the previous question ID.
+
+    .. remarks:
+      This represents the QnA question ID from the previous turn.
+      It is stored within the current step's :class:'botbuilder.dialogs.WaterfallStepContext'.
+      It supports QnA Maker's follow-up prompt and active learning features.
+    """
+
+    KEY_OPTIONS = "options"
+    """
+    The path for storing and retrieving the options for this instance of the dialog.
+
+    .. remarks:
+      This includes the options with which the dialog was started and options
+      expected by the QnA Maker service.
+      It is stored within the current step's :class:'botbuilder.dialogs.WaterfallStepContext'.
+      It supports QnA Maker and the dialog system.
+    """
+
+    # Dialog Options parameters
+    DEFAULT_THRESHOLD = 0.3
+    """ The default threshold for answers returned, based on score. """
+
+    DEFAULT_TOP_N = 3
+    """ The default maximum number of answers to be returned for the question. """
+
+    DEFAULT_NO_ANSWER = "No QnAMaker answers found."
+    """ The default no answer text sent to the user. """
+
+    # Card parameters
+    DEFAULT_CARD_TITLE = "Did you mean:"
+    """ The default active learning card title. """
+
+    DEFAULT_CARD_NO_MATCH_TEXT = "None of the above."
+    """ The default active learning no match text. """
+
+    DEFAULT_CARD_NO_MATCH_RESPONSE = "Thanks for the feedback."
+    """ The default active learning response text. """
+
+    # Value Properties
+    PROPERTY_CURRENT_QUERY = "currentQuery"
+    PROPERTY_QNA_DATA = "qnaData"
+
+    def __init__(
+        self,
+        knowledgebase_id: str,
+        endpoint_key: str,
+        hostname: str,
+        no_answer: Activity = None,
+        threshold: float = DEFAULT_THRESHOLD,
+        active_learning_card_title: str = DEFAULT_CARD_TITLE,
+        card_no_match_text: str = DEFAULT_CARD_NO_MATCH_TEXT,
+        top: int = DEFAULT_TOP_N,
+        card_no_match_response: Activity = None,
+        strict_filters: [Metadata] = None,
+        dialog_id: str = "QnAMakerDialog",
+    ):
+        """
+        Initializes a new instance of the QnAMakerDialog class.
+
+        :param knowledgebase_id: The ID of the QnA Maker knowledge base to query.
+        :param endpoint_key: The QnA Maker endpoint key to use to query the knowledge base.
+        :param hostname: The QnA Maker host URL for the knowledge base, starting with "https://" and
+        ending with "/qnamaker".
+        :param no_answer: The activity to send the user when QnA Maker does not find an answer.
+        :param threshold: The threshold for answers returned, based on score.
+        :param active_learning_card_title: The card title to use when showing active learning options
+        to the user, if active learning is enabled.
+        :param card_no_match_text: The button text to use with active learning options,
+        allowing a user to indicate none of the options are applicable.
+        :param top: The maximum number of answers to return from the knowledge base.
+        :param card_no_match_response: The activity to send the user if they select the no match option
+        on an active learning card.
+        :param strict_filters: QnA Maker metadata with which to filter or boost queries to the
+        knowledge base; or null to apply none.
+        :param dialog_id: The ID of this dialog.
+        """
+        super().__init__(dialog_id)
+
+        self.knowledgebase_id = knowledgebase_id
+        self.endpoint_key = endpoint_key
+        self.hostname = hostname
+        self.no_answer = no_answer
+        self.threshold = threshold
+        self.active_learning_card_title = active_learning_card_title
+        self.card_no_match_text = card_no_match_text
+        self.top = top
+        self.card_no_match_response = card_no_match_response
+        self.strict_filters = strict_filters
+
+        self.maximum_score_for_low_score_variation = 0.95
+
+        self.add_step(self.__call_generate_answer)
+        self.add_step(self.__call_train)
+        self.add_step(self.__check_for_multiturn_prompt)
+        self.add_step(self.__display_qna_result)
+
+    async def begin_dialog(
+        self, dialog_context: DialogContext, options: object = None
+    ) -> DialogTurnResult:
+        """
+        Called when the dialog is started and pushed onto the dialog stack.
+
+        .. remarks:
+          If the task is successful, the result indicates whether the dialog is still
+          active after the turn has been processed by the dialog.
+
+        :param dialog_context: The :class:'botbuilder.dialogs.DialogContext' for the current turn of conversation.
+        :param options: Optional, initial information to pass to the dialog.
+        """
+
+        if not dialog_context:
+            raise TypeError("DialogContext is required")
+
+        if (
+            dialog_context.context
+            and dialog_context.context.activity
+            and dialog_context.context.activity.type != ActivityTypes.message
+        ):
+            return Dialog.end_of_turn
+
+        dialog_options = QnAMakerDialogOptions(
+            options=self._get_qnamaker_options(dialog_context),
+            response_options=self._get_qna_response_options(dialog_context),
+        )
+
+        if options:
+            dialog_options = ObjectPath.assign(dialog_options, options)
+
+        ObjectPath.set_path_value(
+            dialog_context.active_dialog.state,
+            QnAMakerDialog.KEY_OPTIONS,
+            dialog_options,
+        )
+
+        return await super().begin_dialog(dialog_context, dialog_options)
+
+    def _get_qnamaker_client(self, dialog_context: DialogContext) -> QnAMaker:
+        """
+        Gets a :class:'botbuilder.ai.qna.QnAMaker' to use to access the QnA Maker knowledge base.
+
+        :param dialog_context: The :class:'botbuilder.dialogs.DialogContext' for the current turn of conversation.
+        """
+
+        endpoint = QnAMakerEndpoint(
+            endpoint_key=self.endpoint_key,
+            host=self.hostname,
+            knowledge_base_id=self.knowledgebase_id,
+        )
+
+        options = self._get_qnamaker_options(dialog_context)
+
+        return QnAMaker(endpoint, options)
+
+    def _get_qnamaker_options(  # pylint: disable=unused-argument
+        self, dialog_context: DialogContext
+    ) -> QnAMakerOptions:
+        """
+        Gets the options for the QnAMaker client that the dialog will use to query the knowledge base.
+
+        :param dialog_context: The :class:'botbuilder.dialogs.DialogContext' for the current turn of conversation.
+        """
+
+        return QnAMakerOptions(
+            score_threshold=self.threshold,
+            strict_filters=self.strict_filters,
+            top=self.top,
+            context=QnARequestContext(),
+            qna_id=0,
+            ranker_type=RankerTypes.DEFAULT,
+            is_test=False,
+        )
+
+    def _get_qna_response_options(  # pylint: disable=unused-argument
+        self, dialog_context: DialogContext
+    ) -> QnADialogResponseOptions:
+        """
+        Gets the options the dialog will use to display query results to the user.
+
+        :param dialog_context: The :class:'botbuilder.dialogs.DialogContext' for the current turn of conversation.
+        """
+
+        return QnADialogResponseOptions(
+            no_answer=self.no_answer,
+            active_learning_card_title=self.active_learning_card_title
+            or QnAMakerDialog.DEFAULT_CARD_TITLE,
+            card_no_match_text=self.card_no_match_text
+            or QnAMakerDialog.DEFAULT_CARD_NO_MATCH_TEXT,
+            card_no_match_response=self.card_no_match_response,
+        )
+
+    async def __call_generate_answer(self, step_context: WaterfallStepContext):
+        dialog_options: QnAMakerDialogOptions = ObjectPath.get_path_value(
+            step_context.active_dialog.state, QnAMakerDialog.KEY_OPTIONS
+        )
+
+        # Resetting context and QnAId
+        dialog_options.options.qna_id = 0
+        dialog_options.options.context = QnARequestContext()
+
+        # Storing the context info
+        step_context.values[
+            QnAMakerDialog.PROPERTY_CURRENT_QUERY
+        ] = step_context.context.activity.text
+
+        # -Check if previous context is present, if yes then put it with the query
+        # -Check for id if query is present in reverse index.
+        previous_context_data = ObjectPath.get_path_value(
+            step_context.active_dialog.state, QnAMakerDialog.KEY_QNA_CONTEXT_DATA, {}
+        )
+        previous_qna_id = ObjectPath.get_path_value(
+            step_context.active_dialog.state, QnAMakerDialog.KEY_PREVIOUS_QNA_ID, 0
+        )
+
+        if previous_qna_id > 0:
+            dialog_options.options.context = QnARequestContext(
+                previous_qna_id=previous_qna_id
+            )
+
+            current_qna_id = previous_context_data.get(
+                step_context.context.activity.text
+            )
+            if current_qna_id:
+                dialog_options.options.qna_id = current_qna_id
+
+        # Calling QnAMaker to get response.
+        qna_client = self._get_qnamaker_client(step_context)
+        response = await qna_client.get_answers_raw(
+            step_context.context, dialog_options.options
+        )
+
+        is_active_learning_enabled = response.active_learning_enabled
+        step_context.values[QnAMakerDialog.PROPERTY_QNA_DATA] = response.answers
+
+        # Resetting previous query.
+        previous_qna_id = -1
+        ObjectPath.set_path_value(
+            step_context.active_dialog.state,
+            QnAMakerDialog.KEY_PREVIOUS_QNA_ID,
+            previous_qna_id,
+        )
+
+        # Check if active learning is enabled and send card
+        # maximum_score_for_low_score_variation is the score above which no need to check for feedback.
+        if (
+            response.answers
+            and response.answers[0].score <= self.maximum_score_for_low_score_variation
+        ):
+            # Get filtered list of the response that support low score variation criteria.
+            response.answers = qna_client.get_low_score_variation(response.answers)
+            if len(response.answers) > 1 and is_active_learning_enabled:
+                suggested_questions = [qna.questions[0] for qna in response.answers]
+                message = QnACardBuilder.get_suggestions_card(
+                    suggested_questions,
+                    dialog_options.response_options.active_learning_card_title,
+                    dialog_options.response_options.card_no_match_text,
+                )
+                await step_context.context.send_activity(message)
+
+                ObjectPath.set_path_value(
+                    step_context.active_dialog.state,
+                    QnAMakerDialog.KEY_OPTIONS,
+                    dialog_options,
+                )
+
+                await qna_client.close()
+
+                return DialogTurnResult(DialogTurnStatus.Waiting)
+
+        # If card is not shown, move to next step with top qna response.
+        result = [response.answers[0]] if response.answers else []
+        step_context.values[QnAMakerDialog.PROPERTY_QNA_DATA] = result
+        ObjectPath.set_path_value(
+            step_context.active_dialog.state, QnAMakerDialog.KEY_OPTIONS, dialog_options
+        )
+
+        await qna_client.close()
+
+        return await step_context.next(result)
+
+    async def __call_train(self, step_context: WaterfallStepContext):
+        dialog_options: QnAMakerDialogOptions = ObjectPath.get_path_value(
+            step_context.active_dialog.state, QnAMakerDialog.KEY_OPTIONS
+        )
+        train_responses: [QueryResult] = step_context.values[
+            QnAMakerDialog.PROPERTY_QNA_DATA
+        ]
+        current_query = step_context.values[QnAMakerDialog.PROPERTY_CURRENT_QUERY]
+
+        reply = step_context.context.activity.text
+
+        if len(train_responses) > 1:
+            qna_results = [
+                result for result in train_responses if result.questions[0] == reply
+            ]
+
+            if qna_results:
+                qna_result = qna_results[0]
+                step_context.values[QnAMakerDialog.PROPERTY_QNA_DATA] = [qna_result]
+
+                feedback_records = [
+                    FeedbackRecord(
+                        user_id=step_context.context.activity.id,
+                        user_question=current_query,
+                        qna_id=qna_result.id,
+                    )
+                ]
+
+                # Call Active Learning Train API
+                qna_client = self._get_qnamaker_client(step_context)
+                await qna_client.call_train(feedback_records)
+                await qna_client.close()
+
+                return await step_context.next([qna_result])
+
+            if (
+                reply.lower()
+                == dialog_options.response_options.card_no_match_text.lower()
+            ):
+                activity = dialog_options.response_options.card_no_match_response
+                if not activity:
+                    await step_context.context.send_activity(
+                        QnAMakerDialog.DEFAULT_CARD_NO_MATCH_RESPONSE
+                    )
+                else:
+                    await step_context.context.send_activity(activity)
+
+                return await step_context.end_dialog()
+
+            return await super().run_step(
+                step_context, index=0, reason=DialogReason.BeginCalled, result=None
+            )
+
+        return await step_context.next(step_context.result)
+
+    async def __check_for_multiturn_prompt(self, step_context: WaterfallStepContext):
+        dialog_options: QnAMakerDialogOptions = ObjectPath.get_path_value(
+            step_context.active_dialog.state, QnAMakerDialog.KEY_OPTIONS
+        )
+
+        response = step_context.result
+        if response and isinstance(response, List):
+            answer = response[0]
+            if answer.context and answer.context.prompts:
+                previous_context_data = ObjectPath.get_path_value(
+                    step_context.active_dialog.state,
+                    QnAMakerDialog.KEY_QNA_CONTEXT_DATA,
+                    {},
+                )
+                for prompt in answer.context.prompts:
+                    previous_context_data[prompt.display_text] = prompt.qna_id
+
+                ObjectPath.set_path_value(
+                    step_context.active_dialog.state,
+                    QnAMakerDialog.KEY_QNA_CONTEXT_DATA,
+                    previous_context_data,
+                )
+                ObjectPath.set_path_value(
+                    step_context.active_dialog.state,
+                    QnAMakerDialog.KEY_PREVIOUS_QNA_ID,
+                    answer.id,
+                )
+                ObjectPath.set_path_value(
+                    step_context.active_dialog.state,
+                    QnAMakerDialog.KEY_OPTIONS,
+                    dialog_options,
+                )
+
+                # Get multi-turn prompts card activity.
+                message = QnACardBuilder.get_qna_prompts_card(
+                    answer, dialog_options.response_options.card_no_match_text
+                )
+                await step_context.context.send_activity(message)
+
+                return DialogTurnResult(DialogTurnStatus.Waiting)
+
+        return await step_context.next(step_context.result)
+
+    async def __display_qna_result(self, step_context: WaterfallStepContext):
+        dialog_options: QnAMakerDialogOptions = ObjectPath.get_path_value(
+            step_context.active_dialog.state, QnAMakerDialog.KEY_OPTIONS
+        )
+
+        reply = step_context.context.activity.text
+        if reply.lower() == dialog_options.response_options.card_no_match_text.lower():
+            activity = dialog_options.response_options.card_no_match_response
+            if not activity:
+                await step_context.context.send_activity(
+                    QnAMakerDialog.DEFAULT_CARD_NO_MATCH_RESPONSE
+                )
+            else:
+                await step_context.context.send_activity(activity)
+
+            return await step_context.end_dialog()
+
+        # If previous QnAId is present, replace the dialog
+        previous_qna_id = ObjectPath.get_path_value(
+            step_context.active_dialog.state, QnAMakerDialog.KEY_PREVIOUS_QNA_ID, 0
+        )
+        if previous_qna_id > 0:
+            return await super().run_step(
+                step_context, index=0, reason=DialogReason.BeginCalled, result=None
+            )
+
+        # If response is present then show that response, else default answer.
+        response = step_context.result
+        if response and isinstance(response, List):
+            await step_context.context.send_activity(response[0].answer)
+        else:
+            activity = dialog_options.response_options.no_answer
+            if not activity:
+                await step_context.context.send_activity(
+                    QnAMakerDialog.DEFAULT_NO_ANSWER
+                )
+            else:
+                await step_context.context.send_activity(activity)
+
+        return await step_context.end_dialog()
diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/dialogs/qnamaker_dialog_options.py b/libraries/botbuilder-ai/botbuilder/ai/qna/dialogs/qnamaker_dialog_options.py
new file mode 100644
index 000000000..99d0e15cf
--- /dev/null
+++ b/libraries/botbuilder-ai/botbuilder/ai/qna/dialogs/qnamaker_dialog_options.py
@@ -0,0 +1,18 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from .. import QnAMakerOptions, QnADialogResponseOptions
+
+
+class QnAMakerDialogOptions:
+    """
+    Defines Dialog Options for QnAMakerDialog.
+    """
+
+    def __init__(
+        self,
+        options: QnAMakerOptions = None,
+        response_options: QnADialogResponseOptions = None,
+    ):
+        self.options = options or QnAMakerOptions()
+        self.response_options = response_options or QnADialogResponseOptions()
diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/models/__init__.py b/libraries/botbuilder-ai/botbuilder/ai/qna/models/__init__.py
index 018d40c95..608ffeef1 100644
--- a/libraries/botbuilder-ai/botbuilder/ai/qna/models/__init__.py
+++ b/libraries/botbuilder-ai/botbuilder/ai/qna/models/__init__.py
@@ -8,6 +8,7 @@
 from .feedback_record import FeedbackRecord
 from .feedback_records import FeedbackRecords
 from .generate_answer_request_body import GenerateAnswerRequestBody
+from .join_operator import JoinOperator
 from .metadata import Metadata
 from .prompt import Prompt
 from .qnamaker_trace_info import QnAMakerTraceInfo
@@ -21,6 +22,7 @@
     "FeedbackRecord",
     "FeedbackRecords",
     "GenerateAnswerRequestBody",
+    "JoinOperator",
     "Metadata",
     "Prompt",
     "QnAMakerTraceInfo",
diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/models/feedback_record.py b/libraries/botbuilder-ai/botbuilder/ai/qna/models/feedback_record.py
index 74a78d5d0..9b9b1b4ce 100644
--- a/libraries/botbuilder-ai/botbuilder/ai/qna/models/feedback_record.py
+++ b/libraries/botbuilder-ai/botbuilder/ai/qna/models/feedback_record.py
@@ -13,20 +13,8 @@ class FeedbackRecord(Model):
         "qna_id": {"key": "qnaId", "type": "int"},
     }
 
-    def __init__(self, user_id: str, user_question: str, qna_id: int, **kwargs):
-        """
-        Parameters:
-        -----------
-
-        user_id: ID of the user.
-
-        user_question: User question.
-
-        qna_id: QnA ID.
-        """
-
+    def __init__(self, **kwargs):
         super().__init__(**kwargs)
-
-        self.user_id = user_id
-        self.user_question = user_question
-        self.qna_id = qna_id
+        self.user_id = kwargs.get("user_id", None)
+        self.user_question = kwargs.get("user_question", None)
+        self.qna_id = kwargs.get("qna_id", None)
diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/models/feedback_records.py b/libraries/botbuilder-ai/botbuilder/ai/qna/models/feedback_records.py
index 62f3983c4..97f9dc776 100644
--- a/libraries/botbuilder-ai/botbuilder/ai/qna/models/feedback_records.py
+++ b/libraries/botbuilder-ai/botbuilder/ai/qna/models/feedback_records.py
@@ -1,26 +1,14 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License.
 
-from typing import List
-
 from msrest.serialization import Model
 
-from .feedback_record import FeedbackRecord
-
 
 class FeedbackRecords(Model):
     """ Active learning feedback records. """
 
     _attribute_map = {"records": {"key": "records", "type": "[FeedbackRecord]"}}
 
-    def __init__(self, records: List[FeedbackRecord], **kwargs):
-        """
-        Parameter(s):
-        -------------
-
-        records: List of feedback records.
-        """
-
+    def __init__(self, **kwargs):
         super().__init__(**kwargs)
-
-        self.records = records
+        self.records = kwargs.get("records", None)
diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/models/generate_answer_request_body.py b/libraries/botbuilder-ai/botbuilder/ai/qna/models/generate_answer_request_body.py
index 34afb4d2f..dd4104185 100644
--- a/libraries/botbuilder-ai/botbuilder/ai/qna/models/generate_answer_request_body.py
+++ b/libraries/botbuilder-ai/botbuilder/ai/qna/models/generate_answer_request_body.py
@@ -1,13 +1,8 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License.
 
-from typing import List
-
 from msrest.serialization import Model
 
-from .metadata import Metadata
-from .qna_request_context import QnARequestContext
-
 
 class GenerateAnswerRequestBody(Model):
     """ Question used as the payload body for QnA Maker's Generate Answer API. """
@@ -19,41 +14,24 @@ class GenerateAnswerRequestBody(Model):
         "strict_filters": {"key": "strictFilters", "type": "[Metadata]"},
         "context": {"key": "context", "type": "QnARequestContext"},
         "qna_id": {"key": "qnaId", "type": "int"},
+        "is_test": {"key": "isTest", "type": "bool"},
+        "ranker_type": {"key": "rankerType", "type": "RankerTypes"},
+        "strict_filters_join_operator": {
+            "key": "strictFiltersCompoundOperationType",
+            "type": "str",
+        },
     }
 
-    def __init__(
-        self,
-        question: str,
-        top: int,
-        score_threshold: float,
-        strict_filters: List[Metadata],
-        context: QnARequestContext = None,
-        qna_id: int = None,
-        **kwargs
-    ):
-        """
-        Parameters:
-        -----------
-
-        question: The user question to query against the knowledge base.
-
-        top: Max number of answers to be returned for the question.
-
-        score_threshold: Threshold for answers returned based on score.
-
-        strict_filters: Find answers that contains these metadata.
-
-        context: The context from which the QnA was extracted.
-
-        qna_id: Id of the current question asked.
-
-        """
-
+    def __init__(self, **kwargs):
         super().__init__(**kwargs)
-
-        self.question = question
-        self.top = top
-        self.score_threshold = score_threshold
-        self.strict_filters = strict_filters
-        self.context = context
-        self.qna_id = qna_id
+        self.question = kwargs.get("question", None)
+        self.top = kwargs.get("top", None)
+        self.score_threshold = kwargs.get("score_threshold", None)
+        self.strict_filters = kwargs.get("strict_filters", None)
+        self.context = kwargs.get("context", None)
+        self.qna_id = kwargs.get("qna_id", None)
+        self.is_test = kwargs.get("is_test", None)
+        self.ranker_type = kwargs.get("ranker_type", None)
+        self.strict_filters_join_operator = kwargs.get(
+            "strict_filters_join_operator", None
+        )
diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/models/join_operator.py b/libraries/botbuilder-ai/botbuilder/ai/qna/models/join_operator.py
new file mode 100644
index 000000000..a454afa81
--- /dev/null
+++ b/libraries/botbuilder-ai/botbuilder/ai/qna/models/join_operator.py
@@ -0,0 +1,21 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from enum import Enum
+
+
+class JoinOperator(str, Enum):
+    """
+    Join Operator for Strict Filters.
+
+    remarks:
+    --------
+    For example, when using multiple filters in a query, if you want results that
+    have metadata that matches all filters, then use `AND` operator.
+
+    If instead you only wish that the results from knowledge base match
+    at least one of the filters, then use `OR` operator.
+    """
+
+    AND = "AND"
+    OR = "OR"
diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/models/metadata.py b/libraries/botbuilder-ai/botbuilder/ai/qna/models/metadata.py
index af0f1f00b..60f52f18a 100644
--- a/libraries/botbuilder-ai/botbuilder/ai/qna/models/metadata.py
+++ b/libraries/botbuilder-ai/botbuilder/ai/qna/models/metadata.py
@@ -12,17 +12,7 @@ class Metadata(Model):
         "value": {"key": "value", "type": "str"},
     }
 
-    def __init__(self, name: str, value: str, **kwargs):
-        """
-        Parameters:
-        -----------
-
-        name: Metadata name. Max length: 100.
-
-        value: Metadata value. Max length: 100.
-        """
-
+    def __init__(self, **kwargs):
         super().__init__(**kwargs)
-
-        self.name = name
-        self.value = value
+        self.name = kwargs.get("name", None)
+        self.value = kwargs.get("value", None)
diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/models/prompt.py b/libraries/botbuilder-ai/botbuilder/ai/qna/models/prompt.py
index d7f090c87..b0a2fe7fe 100644
--- a/libraries/botbuilder-ai/botbuilder/ai/qna/models/prompt.py
+++ b/libraries/botbuilder-ai/botbuilder/ai/qna/models/prompt.py
@@ -14,32 +14,9 @@ class Prompt(Model):
         "display_text": {"key": "displayText", "type": "str"},
     }
 
-    def __init__(
-        self,
-        *,
-        display_order: int,
-        qna_id: int,
-        display_text: str,
-        qna: object = None,
-        **kwargs
-    ):
-        """
-        Parameters:
-        -----------
-
-        display_order: Index of the prompt - used in ordering of the prompts.
-
-        qna_id: QnA ID.
-
-        display_text: Text displayed to represent a follow up question prompt.
-
-        qna: The QnA object returned from the API (Optional).
-
-        """
-
-        super(Prompt, self).__init__(**kwargs)
-
-        self.display_order = display_order
-        self.qna_id = qna_id
-        self.display_text = display_text
-        self.qna = qna
+    def __init__(self, **kwargs):
+        super().__init__(**kwargs)
+        self.display_order = kwargs.get("display_order", None)
+        self.qna_id = kwargs.get("qna_id", None)
+        self.display_text = kwargs.get("display_text", None)
+        self.qna = kwargs.get("qna", None)
diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/models/qna_request_context.py b/libraries/botbuilder-ai/botbuilder/ai/qna/models/qna_request_context.py
index ae3342a76..ff85afc99 100644
--- a/libraries/botbuilder-ai/botbuilder/ai/qna/models/qna_request_context.py
+++ b/libraries/botbuilder-ai/botbuilder/ai/qna/models/qna_request_context.py
@@ -1,31 +1,21 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from msrest.serialization import Model
-
-
-class QnARequestContext(Model):
-    """
-    The context associated with QnA.
-    Used to mark if the current prompt is relevant with a previous question or not.
-    """
-
-    _attribute_map = {
-        "previous_qna_id": {"key": "previousQnAId", "type": "int"},
-        "prvious_user_query": {"key": "previousUserQuery", "type": "string"},
-    }
-
-    def __init__(self, previous_qna_id: int, prvious_user_query: str, **kwargs):
-        """
-        Parameters:
-        -----------
-
-        previous_qna_id: The previous QnA Id that was returned.
-
-        prvious_user_query: The previous user query/question.
-        """
-
-        super().__init__(**kwargs)
-
-        self.previous_qna_id = previous_qna_id
-        self.prvious_user_query = prvious_user_query
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from msrest.serialization import Model
+
+
+class QnARequestContext(Model):
+    """
+    The context associated with QnA.
+    Used to mark if the current prompt is relevant with a previous question or not.
+    """
+
+    _attribute_map = {
+        "previous_qna_id": {"key": "previousQnAId", "type": "int"},
+        "previous_user_query": {"key": "previousUserQuery", "type": "string"},
+    }
+
+    def __init__(self, **kwargs):
+        super().__init__(**kwargs)
+        self.previous_qna_id = kwargs.get("previous_qna_id", None)
+        self.previous_user_query = kwargs.get("previous_user_query", None)
diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/models/qna_response_context.py b/libraries/botbuilder-ai/botbuilder/ai/qna/models/qna_response_context.py
index 537bf09db..bf68bb213 100644
--- a/libraries/botbuilder-ai/botbuilder/ai/qna/models/qna_response_context.py
+++ b/libraries/botbuilder-ai/botbuilder/ai/qna/models/qna_response_context.py
@@ -1,9 +1,7 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License.
 
-from typing import List
 from msrest.serialization import Model
-from .prompt import Prompt
 
 
 class QnAResponseContext(Model):
@@ -17,9 +15,7 @@ class QnAResponseContext(Model):
         "prompts": {"key": "prompts", "type": "[Prompt]"},
     }
 
-    def __init__(
-        self, *, is_context_only: bool = False, prompts: List[Prompt] = None, **kwargs
-    ):
+    def __init__(self, **kwargs):
         """
         Parameters:
         -----------
@@ -30,6 +26,6 @@ def __init__(
 
         """
 
-        super(QnAResponseContext, self).__init__(**kwargs)
-        self.is_context_only = is_context_only
-        self.prompts = prompts
+        super().__init__(**kwargs)
+        self.is_context_only = kwargs.get("is_context_only", None)
+        self.prompts = kwargs.get("prompts", None)
diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/models/qnamaker_trace_info.py b/libraries/botbuilder-ai/botbuilder/ai/qna/models/qnamaker_trace_info.py
index 987ea9677..f585e5c26 100644
--- a/libraries/botbuilder-ai/botbuilder/ai/qna/models/qnamaker_trace_info.py
+++ b/libraries/botbuilder-ai/botbuilder/ai/qna/models/qnamaker_trace_info.py
@@ -7,6 +7,7 @@
 from .metadata import Metadata
 from .query_result import QueryResult
 from .qna_request_context import QnARequestContext
+from .ranker_types import RankerTypes
 
 
 class QnAMakerTraceInfo:
@@ -22,6 +23,8 @@ def __init__(
         strict_filters: List[Metadata],
         context: QnARequestContext = None,
         qna_id: int = None,
+        is_test: bool = False,
+        ranker_type: str = RankerTypes.DEFAULT,
     ):
         """
         Parameters:
@@ -42,6 +45,10 @@ def __init__(
         context: (Optional) The context from which the QnA was extracted.
 
         qna_id: (Optional) Id of the current question asked.
+
+        is_test: (Optional) A value indicating whether to call test or prod environment of knowledgebase.
+
+        ranker_types: (Optional) Ranker types.
         """
         self.message = message
         self.query_results = query_results
@@ -51,3 +58,5 @@ def __init__(
         self.strict_filters = strict_filters
         self.context = context
         self.qna_id = qna_id
+        self.is_test = is_test
+        self.ranker_type = ranker_type
diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/models/query_result.py b/libraries/botbuilder-ai/botbuilder/ai/qna/models/query_result.py
index 321ea64cf..a0b1c2c0a 100644
--- a/libraries/botbuilder-ai/botbuilder/ai/qna/models/query_result.py
+++ b/libraries/botbuilder-ai/botbuilder/ai/qna/models/query_result.py
@@ -1,10 +1,7 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License.
 
-from typing import List
 from msrest.serialization import Model
-from .metadata import Metadata
-from .qna_response_context import QnAResponseContext
 
 
 class QueryResult(Model):
@@ -14,47 +11,18 @@ class QueryResult(Model):
         "questions": {"key": "questions", "type": "[str]"},
         "answer": {"key": "answer", "type": "str"},
         "score": {"key": "score", "type": "float"},
-        "metadata": {"key": "metadata", "type": "object"},
+        "metadata": {"key": "metadata", "type": "[Metadata]"},
         "source": {"key": "source", "type": "str"},
         "id": {"key": "id", "type": "int"},
-        "context": {"key": "context", "type": "object"},
+        "context": {"key": "context", "type": "QnAResponseContext"},
     }
 
-    def __init__(
-        self,
-        *,
-        questions: List[str],
-        answer: str,
-        score: float,
-        metadata: object = None,
-        source: str = None,
-        id: int = None,  # pylint: disable=invalid-name
-        context: QnAResponseContext = None,
-        **kwargs
-    ):
-        """
-        Parameters:
-        -----------
-
-        questions: The list of questions indexed in the QnA Service for the given answer (if any).
-
-        answer: Answer from the knowledge base.
-
-        score: Confidence on a scale from 0.0 to 1.0 that the answer matches the user's intent.
-
-        metadata: Metadata associated with the answer (if any).
-
-        source: The source from which the QnA was extracted (if any).
-
-        id: The index of the answer in the knowledge base. V3 uses 'qnaId', V4 uses 'id' (if any).
-
-        context: The context from which the QnA was extracted.
-        """
-        super(QueryResult, self).__init__(**kwargs)
-        self.questions = questions
-        self.answer = answer
-        self.score = score
-        self.metadata = list(map(lambda meta: Metadata(**meta), metadata))
-        self.source = source
-        self.context = QnAResponseContext(**context) if context is not None else None
-        self.id = id  # pylint: disable=invalid-name
+    def __init__(self, **kwargs):
+        super().__init__(**kwargs)
+        self.questions = kwargs.get("questions", None)
+        self.answer = kwargs.get("answer", None)
+        self.score = kwargs.get("score", None)
+        self.metadata = kwargs.get("metadata", None)
+        self.source = kwargs.get("source", None)
+        self.context = kwargs.get("context", None)
+        self.id = kwargs.get("id", None)  # pylint: disable=invalid-name
diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/models/query_results.py b/libraries/botbuilder-ai/botbuilder/ai/qna/models/query_results.py
index 17fd2a2c8..f3c413618 100644
--- a/libraries/botbuilder-ai/botbuilder/ai/qna/models/query_results.py
+++ b/libraries/botbuilder-ai/botbuilder/ai/qna/models/query_results.py
@@ -25,6 +25,6 @@ def __init__(
 
         active_learning_enabled: The active learning enable flag.
         """
-        super(QueryResults, self).__init__(**kwargs)
+        super().__init__(**kwargs)
         self.answers = answers
         self.active_learning_enabled = active_learning_enabled
diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/models/ranker_types.py b/libraries/botbuilder-ai/botbuilder/ai/qna/models/ranker_types.py
new file mode 100644
index 000000000..a3f0463ca
--- /dev/null
+++ b/libraries/botbuilder-ai/botbuilder/ai/qna/models/ranker_types.py
@@ -0,0 +1,15 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+
+class RankerTypes:
+
+    """ Default Ranker Behaviour. i.e. Ranking based on Questions and Answer. """
+
+    DEFAULT = "Default"
+
+    """ Ranker based on question Only. """
+    QUESTION_ONLY = "QuestionOnly"
+
+    """ Ranker based on Autosuggest for question field only. """
+    AUTO_SUGGEST_QUESTION = "AutoSuggestQuestion"
diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/models/train_request_body.py b/libraries/botbuilder-ai/botbuilder/ai/qna/models/train_request_body.py
index 2ce267831..252f8ae81 100644
--- a/libraries/botbuilder-ai/botbuilder/ai/qna/models/train_request_body.py
+++ b/libraries/botbuilder-ai/botbuilder/ai/qna/models/train_request_body.py
@@ -1,11 +1,8 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License.
 
-from typing import List
 from msrest.serialization import Model
 
-from .feedback_record import FeedbackRecord
-
 
 class TrainRequestBody(Model):
     """ Class the models the request body that is sent as feedback to the Train API. """
@@ -14,14 +11,6 @@ class TrainRequestBody(Model):
         "feedback_records": {"key": "feedbackRecords", "type": "[FeedbackRecord]"}
     }
 
-    def __init__(self, feedback_records: List[FeedbackRecord], **kwargs):
-        """
-        Parameters:
-        -----------
-
-        feedback_records: List of feedback records.
-        """
-
+    def __init__(self, **kwargs):
         super().__init__(**kwargs)
-
-        self.feedback_records = feedback_records
+        self.feedback_records = kwargs.get("feedback_records", None)
diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/qna_dialog_response_options.py b/libraries/botbuilder-ai/botbuilder/ai/qna/qna_dialog_response_options.py
new file mode 100644
index 000000000..5490f7727
--- /dev/null
+++ b/libraries/botbuilder-ai/botbuilder/ai/qna/qna_dialog_response_options.py
@@ -0,0 +1,18 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.schema import Activity
+
+
+class QnADialogResponseOptions:
+    def __init__(
+        self,
+        active_learning_card_title: str = None,
+        card_no_match_text: str = None,
+        no_answer: Activity = None,
+        card_no_match_response: Activity = None,
+    ):
+        self.active_learning_card_title = active_learning_card_title
+        self.card_no_match_text = card_no_match_text
+        self.no_answer = no_answer
+        self.card_no_match_response = card_no_match_response
diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker.py b/libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker.py
index f01aeb453..f7583c571 100644
--- a/libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker.py
+++ b/libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker.py
@@ -1,271 +1,260 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import json
-from typing import Dict, List, NamedTuple, Union
-from aiohttp import ClientSession, ClientTimeout
-
-from botbuilder.schema import Activity
-from botbuilder.core import BotTelemetryClient, NullTelemetryClient, TurnContext
-
-from .models import FeedbackRecord, QueryResult, QueryResults
-from .utils import (
-    ActiveLearningUtils,
-    GenerateAnswerUtils,
-    QnATelemetryConstants,
-    TrainUtils,
-)
-from .qnamaker_endpoint import QnAMakerEndpoint
-from .qnamaker_options import QnAMakerOptions
-from .qnamaker_telemetry_client import QnAMakerTelemetryClient
-
-from .. import __title__, __version__
-
-
-class EventData(NamedTuple):
-    properties: Dict[str, str]
-    metrics: Dict[str, float]
-
-
-class QnAMaker(QnAMakerTelemetryClient):
-    """
-    Class used to query a QnA Maker knowledge base for answers.
-    """
-
-    def __init__(
-        self,
-        endpoint: QnAMakerEndpoint,
-        options: QnAMakerOptions = None,
-        http_client: ClientSession = None,
-        telemetry_client: BotTelemetryClient = None,
-        log_personal_information: bool = None,
-    ):
-        super().__init__(log_personal_information, telemetry_client)
-
-        if not isinstance(endpoint, QnAMakerEndpoint):
-            raise TypeError(
-                "QnAMaker.__init__(): endpoint is not an instance of QnAMakerEndpoint"
-            )
-
-        self._endpoint: str = endpoint
-
-        opt = options or QnAMakerOptions()
-        self._validate_options(opt)
-
-        instance_timeout = ClientTimeout(total=opt.timeout / 1000)
-        self._http_client = http_client or ClientSession(timeout=instance_timeout)
-
-        self.telemetry_client: Union[
-            BotTelemetryClient, NullTelemetryClient
-        ] = telemetry_client or NullTelemetryClient()
-
-        self.log_personal_information = log_personal_information or False
-
-        self._generate_answer_helper = GenerateAnswerUtils(
-            self.telemetry_client, self._endpoint, options, self._http_client
-        )
-        self._active_learning_train_helper = TrainUtils(
-            self._endpoint, self._http_client
-        )
-
-    async def get_answers(
-        self,
-        context: TurnContext,
-        options: QnAMakerOptions = None,
-        telemetry_properties: Dict[str, str] = None,
-        telemetry_metrics: Dict[str, int] = None,
-    ) -> [QueryResult]:
-        """
-        Generates answers from the knowledge base.
-
-        return:
-        -------
-        A list of answers for the user's query, sorted in decreasing order of ranking score.
-
-        rtype:
-        ------
-        List[QueryResult]
-        """
-        result = await self.get_answers_raw(
-            context, options, telemetry_properties, telemetry_metrics
-        )
-
-        return result.answers
-
-    async def get_answers_raw(
-        self,
-        context: TurnContext,
-        options: QnAMakerOptions = None,
-        telemetry_properties: Dict[str, str] = None,
-        telemetry_metrics: Dict[str, int] = None,
-    ) -> QueryResults:
-        """
-        Generates raw answers from the knowledge base.
-
-        return:
-        -------
-        A list of answers for the user's query, sorted in decreasing order of ranking score.
-
-        rtype:
-        ------
-        QueryResults
-        """
-        if not context:
-            raise TypeError("QnAMaker.get_answers(): context cannot be None.")
-
-        if not isinstance(context.activity, Activity):
-            raise TypeError(
-                "QnAMaker.get_answers(): TurnContext's activity must be an Activity instance."
-            )
-
-        result = await self._generate_answer_helper.get_answers_raw(context, options)
-
-        await self.on_qna_result(
-            result.answers, context, telemetry_properties, telemetry_metrics
-        )
-
-        return result
-
-    def get_low_score_variation(self, query_result: QueryResult) -> List[QueryResult]:
-        """
-        Filters the ambiguous question for active learning.
-
-        Parameters:
-        -----------
-        query_result: User query output.
-
-        Return:
-        -------
-        Filtered aray of ambigous questions.
-        """
-        return ActiveLearningUtils.get_low_score_variation(query_result)
-
-    async def call_train(self, feedback_records: List[FeedbackRecord]):
-        """
-        Sends feedback to the knowledge base.
-
-        Parameters:
-        -----------
-        feedback_records
-        """
-        return await self._active_learning_train_helper.call_train(feedback_records)
-
-    async def on_qna_result(
-        self,
-        query_results: [QueryResult],
-        turn_context: TurnContext,
-        telemetry_properties: Dict[str, str] = None,
-        telemetry_metrics: Dict[str, float] = None,
-    ):
-        event_data = await self.fill_qna_event(
-            query_results, turn_context, telemetry_properties, telemetry_metrics
-        )
-
-        # Track the event
-        self.telemetry_client.track_event(
-            name=QnATelemetryConstants.qna_message_event,
-            properties=event_data.properties,
-            measurements=event_data.metrics,
-        )
-
-    async def fill_qna_event(
-        self,
-        query_results: [QueryResult],
-        turn_context: TurnContext,
-        telemetry_properties: Dict[str, str] = None,
-        telemetry_metrics: Dict[str, float] = None,
-    ) -> EventData:
-        """
-        Fills the event properties and metrics for the QnaMessage event for telemetry.
-
-        return:
-        -------
-        A tuple of event data properties and metrics that will be sent to the
-        BotTelemetryClient.track_event() method for the QnAMessage event.
-        The properties and metrics returned the standard properties logged
-        with any properties passed from the get_answers() method.
-
-        rtype:
-        ------
-        EventData
-        """
-
-        properties: Dict[str, str] = dict()
-        metrics: Dict[str, float] = dict()
-
-        properties[
-            QnATelemetryConstants.knowledge_base_id_property
-        ] = self._endpoint.knowledge_base_id
-
-        text: str = turn_context.activity.text
-        user_name: str = turn_context.activity.from_property.name
-
-        # Use the LogPersonalInformation flag to toggle logging PII data; text and username are common examples.
-        if self.log_personal_information:
-            if text:
-                properties[QnATelemetryConstants.question_property] = text
-
-            if user_name:
-                properties[QnATelemetryConstants.username_property] = user_name
-
-        # Fill in Qna Results (found or not).
-        if self._has_matched_answer_in_kb(query_results):
-            query_result = query_results[0]
-
-            result_properties = {
-                QnATelemetryConstants.matched_question_property: json.dumps(
-                    query_result.questions
-                ),
-                QnATelemetryConstants.question_id_property: str(query_result.id),
-                QnATelemetryConstants.answer_property: query_result.answer,
-                QnATelemetryConstants.article_found_property: "true",
-            }
-            properties.update(result_properties)
-
-            metrics[QnATelemetryConstants.score_metric] = query_result.score
-        else:
-            no_match_properties = {
-                QnATelemetryConstants.matched_question_property: "No Qna Question matched",
-                QnATelemetryConstants.question_id_property: "No Qna Question Id matched",
-                QnATelemetryConstants.answer_property: "No Qna Answer matched",
-                QnATelemetryConstants.article_found_property: "false",
-            }
-
-            properties.update(no_match_properties)
-
-        # Additional Properties can override "stock" properties.
-        if telemetry_properties:
-            properties.update(telemetry_properties)
-
-        # Additional Metrics can override "stock" metrics.
-        if telemetry_metrics:
-            metrics.update(telemetry_metrics)
-
-        return EventData(properties=properties, metrics=metrics)
-
-    def _validate_options(self, options: QnAMakerOptions):
-        if not options.score_threshold:
-            options.score_threshold = 0.3
-
-        if not options.top:
-            options.top = 1
-
-        if options.score_threshold < 0 or options.score_threshold > 1:
-            raise ValueError("Score threshold should be a value between 0 and 1")
-
-        if options.top < 1:
-            raise ValueError("QnAMakerOptions.top should be an integer greater than 0")
-
-        if not options.strict_filters:
-            options.strict_filters = []
-
-        if not options.timeout:
-            options.timeout = 100000
-
-    def _has_matched_answer_in_kb(self, query_results: [QueryResult]) -> bool:
-        if query_results:
-            if query_results[0].id != -1:
-
-                return True
-
-        return False
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import json
+from typing import Dict, List, NamedTuple, Union
+from aiohttp import ClientSession, ClientTimeout
+
+from botbuilder.schema import Activity
+from botbuilder.core import BotTelemetryClient, NullTelemetryClient, TurnContext
+
+from .models import FeedbackRecord, QueryResult, QueryResults
+from .utils import (
+    ActiveLearningUtils,
+    GenerateAnswerUtils,
+    QnATelemetryConstants,
+    TrainUtils,
+)
+from .qnamaker_endpoint import QnAMakerEndpoint
+from .qnamaker_options import QnAMakerOptions
+from .qnamaker_telemetry_client import QnAMakerTelemetryClient
+
+from .. import __title__, __version__
+
+
+class EventData(NamedTuple):
+    properties: Dict[str, str]
+    metrics: Dict[str, float]
+
+
+class QnAMaker(QnAMakerTelemetryClient):
+    """
+    Class used to query a QnA Maker knowledge base for answers.
+    """
+
+    def __init__(
+        self,
+        endpoint: QnAMakerEndpoint,
+        options: QnAMakerOptions = None,
+        http_client: ClientSession = None,
+        telemetry_client: BotTelemetryClient = None,
+        log_personal_information: bool = None,
+    ):
+        super().__init__(log_personal_information, telemetry_client)
+
+        if not isinstance(endpoint, QnAMakerEndpoint):
+            raise TypeError(
+                "QnAMaker.__init__(): endpoint is not an instance of QnAMakerEndpoint"
+            )
+
+        self._endpoint: str = endpoint
+
+        opt = options or QnAMakerOptions()
+        self._validate_options(opt)
+
+        instance_timeout = ClientTimeout(total=opt.timeout / 1000)
+        self._http_client = http_client or ClientSession(timeout=instance_timeout)
+
+        self.telemetry_client: Union[
+            BotTelemetryClient, NullTelemetryClient
+        ] = telemetry_client or NullTelemetryClient()
+
+        self.log_personal_information = log_personal_information or False
+
+        self._generate_answer_helper = GenerateAnswerUtils(
+            self.telemetry_client, self._endpoint, options, self._http_client
+        )
+        self._active_learning_train_helper = TrainUtils(
+            self._endpoint, self._http_client
+        )
+
+    async def close(self):
+        await self._http_client.close()
+
+    async def get_answers(
+        self,
+        context: TurnContext,
+        options: QnAMakerOptions = None,
+        telemetry_properties: Dict[str, str] = None,
+        telemetry_metrics: Dict[str, int] = None,
+    ) -> [QueryResult]:
+        """
+        Generates answers from the knowledge base.
+
+        :return: A list of answers for the user's query, sorted in decreasing order of ranking score.
+        :rtype: :class:`typing.List[QueryResult]`
+        """
+        result = await self.get_answers_raw(
+            context, options, telemetry_properties, telemetry_metrics
+        )
+
+        return result.answers
+
+    async def get_answers_raw(
+        self,
+        context: TurnContext,
+        options: QnAMakerOptions = None,
+        telemetry_properties: Dict[str, str] = None,
+        telemetry_metrics: Dict[str, int] = None,
+    ) -> QueryResults:
+        """
+        Generates raw answers from the knowledge base.
+
+        :return: A list of answers for the user's query, sorted in decreasing order of ranking score.
+        :rtype: :class:`QueryResult`
+        """
+        if not context:
+            raise TypeError("QnAMaker.get_answers(): context cannot be None.")
+
+        if not isinstance(context.activity, Activity):
+            raise TypeError(
+                "QnAMaker.get_answers(): TurnContext's activity must be an Activity instance."
+            )
+
+        result = await self._generate_answer_helper.get_answers_raw(context, options)
+
+        await self.on_qna_result(
+            result.answers, context, telemetry_properties, telemetry_metrics
+        )
+
+        return result
+
+    def get_low_score_variation(self, query_result: QueryResult) -> List[QueryResult]:
+        """
+        Filters the ambiguous question for active learning.
+
+        :param query_result: User query output.
+        :type query_result: :class:`QueryResult`
+        :return: Filtered array of ambiguous questions.
+        :rtype: :class:`typing.List[QueryResult]`
+        """
+        return ActiveLearningUtils.get_low_score_variation(query_result)
+
+    async def call_train(self, feedback_records: List[FeedbackRecord]):
+        """
+        Sends feedback to the knowledge base.
+
+        :param feedback_records: Feedback records.
+        :type feedback_records: :class:`typing.List[FeedbackRecord]`
+        """
+        return await self._active_learning_train_helper.call_train(feedback_records)
+
+    async def on_qna_result(
+        self,
+        query_results: [QueryResult],
+        turn_context: TurnContext,
+        telemetry_properties: Dict[str, str] = None,
+        telemetry_metrics: Dict[str, float] = None,
+    ):
+        event_data = await self.fill_qna_event(
+            query_results, turn_context, telemetry_properties, telemetry_metrics
+        )
+
+        # Track the event
+        self.telemetry_client.track_event(
+            name=QnATelemetryConstants.qna_message_event,
+            properties=event_data.properties,
+            measurements=event_data.metrics,
+        )
+
+    async def fill_qna_event(
+        self,
+        query_results: [QueryResult],
+        turn_context: TurnContext,
+        telemetry_properties: Dict[str, str] = None,
+        telemetry_metrics: Dict[str, float] = None,
+    ) -> EventData:
+        """
+        Fills the event properties and metrics for the QnaMessage event for telemetry.
+
+        :param query_results: QnA service results.
+        :type quert_results: :class:`QueryResult`
+        :param turn_context: Context object containing information for a single turn of conversation with a user.
+        :type turn_context: :class:`botbuilder.core.TurnContext`
+        :param telemetry_properties: Properties to add/override for the event.
+        :type telemetry_properties: :class:`typing.Dict[str, str]`
+        :param telemetry_metrics: Metrics to add/override for the event.
+        :type telemetry_metrics: :class:`typing.Dict[str, float]`
+        :return: Event properties and metrics for the QnaMessage event for telemetry.
+        :rtype: :class:`EventData`
+        """
+
+        properties: Dict[str, str] = dict()
+        metrics: Dict[str, float] = dict()
+
+        properties[
+            QnATelemetryConstants.knowledge_base_id_property
+        ] = self._endpoint.knowledge_base_id
+
+        text: str = turn_context.activity.text
+        user_name: str = turn_context.activity.from_property.name
+
+        # Use the LogPersonalInformation flag to toggle logging PII data; text and username are common examples.
+        if self.log_personal_information:
+            if text:
+                properties[QnATelemetryConstants.question_property] = text
+
+            if user_name:
+                properties[QnATelemetryConstants.username_property] = user_name
+
+        # Fill in Qna Results (found or not).
+        if self._has_matched_answer_in_kb(query_results):
+            query_result = query_results[0]
+
+            result_properties = {
+                QnATelemetryConstants.matched_question_property: json.dumps(
+                    query_result.questions
+                ),
+                QnATelemetryConstants.question_id_property: str(query_result.id),
+                QnATelemetryConstants.answer_property: query_result.answer,
+                QnATelemetryConstants.article_found_property: "true",
+            }
+            properties.update(result_properties)
+
+            metrics[QnATelemetryConstants.score_metric] = query_result.score
+        else:
+            no_match_properties = {
+                QnATelemetryConstants.matched_question_property: "No Qna Question matched",
+                QnATelemetryConstants.question_id_property: "No Qna Question Id matched",
+                QnATelemetryConstants.answer_property: "No Qna Answer matched",
+                QnATelemetryConstants.article_found_property: "false",
+            }
+
+            properties.update(no_match_properties)
+
+        # Additional Properties can override "stock" properties.
+        if telemetry_properties:
+            properties.update(telemetry_properties)
+
+        # Additional Metrics can override "stock" metrics.
+        if telemetry_metrics:
+            metrics.update(telemetry_metrics)
+
+        return EventData(properties=properties, metrics=metrics)
+
+    def _validate_options(self, options: QnAMakerOptions):
+        if not options.score_threshold:
+            options.score_threshold = 0.3
+
+        if not options.top:
+            options.top = 1
+
+        if options.score_threshold < 0 or options.score_threshold > 1:
+            raise ValueError("Score threshold should be a value between 0 and 1")
+
+        if options.top < 1:
+            raise ValueError("QnAMakerOptions.top should be an integer greater than 0")
+
+        if not options.strict_filters:
+            options.strict_filters = []
+
+        if not options.timeout:
+            options.timeout = 100000
+
+    def _has_matched_answer_in_kb(self, query_results: [QueryResult]) -> bool:
+        if query_results:
+            if query_results[0].id != -1:
+
+                return True
+
+        return False
diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker_options.py b/libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker_options.py
index a32f49fed..af4a4ad1c 100644
--- a/libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker_options.py
+++ b/libraries/botbuilder-ai/botbuilder/ai/qna/qnamaker_options.py
@@ -1,22 +1,64 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from .models import Metadata, QnARequestContext
-
-# figure out if 300 milliseconds is ok for python requests library...or 100000
-class QnAMakerOptions:
-    def __init__(
-        self,
-        score_threshold: float = 0.0,
-        timeout: int = 0,
-        top: int = 0,
-        strict_filters: [Metadata] = None,
-        context: [QnARequestContext] = None,
-        qna_id: int = None,
-    ):
-        self.score_threshold = score_threshold
-        self.timeout = timeout
-        self.top = top
-        self.strict_filters = strict_filters or []
-        self.context = context
-        self.qna_id = qna_id
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from .models import Metadata, QnARequestContext
+from .models.ranker_types import RankerTypes
+from .models.join_operator import JoinOperator
+
+
+class QnAMakerOptions:
+    """
+    Defines options used to configure a `QnAMaker` instance.
+
+    remarks:
+    --------
+        All parameters are optional.
+    """
+
+    def __init__(
+        self,
+        score_threshold: float = 0.0,
+        timeout: int = 0,
+        top: int = 0,
+        strict_filters: [Metadata] = None,
+        context: [QnARequestContext] = None,
+        qna_id: int = None,
+        is_test: bool = False,
+        ranker_type: str = RankerTypes.DEFAULT,
+        strict_filters_join_operator: str = JoinOperator.AND,
+    ):
+        """
+        Parameters:
+        -----------
+        score_threshold (float):
+            The minimum score threshold, used to filter returned results.
+            Values range from score of 0.0 to 1.0.
+        timeout (int):
+            The time in milliseconds to wait before the request times out.
+        top (int):
+            The number of ranked results to return.
+        strict_filters ([Metadata]):
+            Filters to use on queries to a QnA knowledge base, based on a
+            QnA pair's metadata.
+        context ([QnARequestContext]):
+            The context of the previous turn.
+        qna_id (int):
+            Id of the current question asked (if available).
+        is_test (bool):
+            A value indicating whether to call test or prod environment of a knowledge base.
+        ranker_type (str):
+            The QnA ranker type to use.
+        strict_filters_join_operator (str):
+            A value indicating how strictly you want to apply strict_filters on QnA pairs' metadata.
+            For example, when combining several metadata filters, you can determine if you are
+            concerned with all filters matching or just at least one filter matching.
+        """
+        self.score_threshold = score_threshold
+        self.timeout = timeout
+        self.top = top
+        self.strict_filters = strict_filters or []
+        self.context = context
+        self.qna_id = qna_id
+        self.is_test = is_test
+        self.ranker_type = ranker_type
+        self.strict_filters_join_operator = strict_filters_join_operator
diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/utils/__init__.py b/libraries/botbuilder-ai/botbuilder/ai/qna/utils/__init__.py
index 58d8575e0..e4669b2aa 100644
--- a/libraries/botbuilder-ai/botbuilder/ai/qna/utils/__init__.py
+++ b/libraries/botbuilder-ai/botbuilder/ai/qna/utils/__init__.py
@@ -1,20 +1,22 @@
-# coding=utf-8
-# --------------------------------------------------------------------------
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License. See License.txt in the project root for
-# license information.
-# --------------------------------------------------------------------------
-
-from .active_learning_utils import ActiveLearningUtils
-from .generate_answer_utils import GenerateAnswerUtils
-from .http_request_utils import HttpRequestUtils
-from .qna_telemetry_constants import QnATelemetryConstants
-from .train_utils import TrainUtils
-
-__all__ = [
-    "ActiveLearningUtils",
-    "GenerateAnswerUtils",
-    "HttpRequestUtils",
-    "QnATelemetryConstants",
-    "TrainUtils",
-]
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+# --------------------------------------------------------------------------
+
+from .active_learning_utils import ActiveLearningUtils
+from .generate_answer_utils import GenerateAnswerUtils
+from .http_request_utils import HttpRequestUtils
+from .qna_telemetry_constants import QnATelemetryConstants
+from .train_utils import TrainUtils
+from .qna_card_builder import QnACardBuilder
+
+__all__ = [
+    "ActiveLearningUtils",
+    "GenerateAnswerUtils",
+    "HttpRequestUtils",
+    "QnATelemetryConstants",
+    "TrainUtils",
+    "QnACardBuilder",
+]
diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/utils/active_learning_utils.py b/libraries/botbuilder-ai/botbuilder/ai/qna/utils/active_learning_utils.py
index 4fc6c536f..625d77dbc 100644
--- a/libraries/botbuilder-ai/botbuilder/ai/qna/utils/active_learning_utils.py
+++ b/libraries/botbuilder-ai/botbuilder/ai/qna/utils/active_learning_utils.py
@@ -7,8 +7,8 @@
 from ..models import QueryResult
 
 MINIMUM_SCORE_FOR_LOW_SCORE_VARIATION = 20.0
-PREVIOUS_LOW_SCORE_VARIATION_MULTIPLIER = 1.4
-MAX_LOW_SCORE_VARIATION_MULTIPLIER = 2.0
+PREVIOUS_LOW_SCORE_VARIATION_MULTIPLIER = 0.7
+MAX_LOW_SCORE_VARIATION_MULTIPLIER = 1.0
 MAX_SCORE_FOR_LOW_SCORE_VARIATION = 95.0
 
 
@@ -17,7 +17,7 @@ class ActiveLearningUtils:
 
     @staticmethod
     def get_low_score_variation(
-        qna_search_results: List[QueryResult]
+        qna_search_results: List[QueryResult],
     ) -> List[QueryResult]:
         """
         Returns a list of QnA search results, which have low score variation.
diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/utils/generate_answer_utils.py b/libraries/botbuilder-ai/botbuilder/ai/qna/utils/generate_answer_utils.py
index b683c50da..1f335f9e6 100644
--- a/libraries/botbuilder-ai/botbuilder/ai/qna/utils/generate_answer_utils.py
+++ b/libraries/botbuilder-ai/botbuilder/ai/qna/utils/generate_answer_utils.py
@@ -2,7 +2,9 @@
 # Licensed under the MIT License.
 
 from copy import copy
-from typing import List, Union
+from typing import Any, List, Union
+import json
+import requests
 
 from aiohttp import ClientResponse, ClientSession
 
@@ -109,7 +111,8 @@ def _hydrate_options(self, query_options: QnAMakerOptions) -> QnAMakerOptions:
         with the options passed as arguments into get_answers().
         Return:
         -------
-        QnAMakerOptions with options passed into constructor overwritten by new options passed into get_answers()
+        QnAMakerOptions with options passed into constructor overwritten
+        by new options passed into get_answers()
 
         rtype:
         ------
@@ -139,6 +142,11 @@ def _hydrate_options(self, query_options: QnAMakerOptions) -> QnAMakerOptions:
 
             hydrated_options.context = query_options.context
             hydrated_options.qna_id = query_options.qna_id
+            hydrated_options.is_test = query_options.is_test
+            hydrated_options.ranker_type = query_options.ranker_type
+            hydrated_options.strict_filters_join_operator = (
+                query_options.strict_filters_join_operator
+            )
 
         return hydrated_options
 
@@ -154,11 +162,14 @@ async def _query_qna_service(
             strict_filters=options.strict_filters,
             context=options.context,
             qna_id=options.qna_id,
+            is_test=options.is_test,
+            ranker_type=options.ranker_type,
+            strict_filters_join_operator=options.strict_filters_join_operator,
         )
 
         http_request_helper = HttpRequestUtils(self._http_client)
 
-        response: ClientResponse = await http_request_helper.execute_http_request(
+        response: Any = await http_request_helper.execute_http_request(
             url, question, self._endpoint, options.timeout
         )
 
@@ -178,6 +189,8 @@ async def _emit_trace_info(
             strict_filters=options.strict_filters,
             context=options.context,
             qna_id=options.qna_id,
+            is_test=options.is_test,
+            ranker_type=options.ranker_type,
         )
 
         trace_activity = Activity(
@@ -194,21 +207,26 @@ async def _format_qna_result(
         self, result, options: QnAMakerOptions
     ) -> QueryResults:
         json_res = result
+
         if isinstance(result, ClientResponse):
             json_res = await result.json()
 
+        if isinstance(result, requests.Response):
+            json_res = json.loads(result.text)
+
         answers_within_threshold = [
             {**answer, "score": answer["score"] / 100}
             for answer in json_res["answers"]
             if answer["score"] / 100 > options.score_threshold
         ]
+
         sorted_answers = sorted(
             answers_within_threshold, key=lambda ans: ans["score"], reverse=True
         )
 
-        answers_as_query_results = list(
-            map(lambda answer: QueryResult(**answer), sorted_answers)
-        )
+        answers_as_query_results = [
+            QueryResult().deserialize(answer) for answer in sorted_answers
+        ]
 
         active_learning_enabled = (
             json_res["activeLearningEnabled"]
diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/utils/http_request_utils.py b/libraries/botbuilder-ai/botbuilder/ai/qna/utils/http_request_utils.py
index d550f8ad0..977f839de 100644
--- a/libraries/botbuilder-ai/botbuilder/ai/qna/utils/http_request_utils.py
+++ b/libraries/botbuilder-ai/botbuilder/ai/qna/utils/http_request_utils.py
@@ -1,95 +1,140 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import json
-import platform
-
-from aiohttp import ClientResponse, ClientSession, ClientTimeout
-
-from ... import __title__, __version__
-
-from ..qnamaker_endpoint import QnAMakerEndpoint
-
-
-class HttpRequestUtils:
-    """ HTTP request utils class. """
-
-    def __init__(self, http_client: ClientSession):
-        self._http_client = http_client
-
-    async def execute_http_request(
-        self,
-        request_url: str,
-        payload_body: object,
-        endpoint: QnAMakerEndpoint,
-        timeout: float = None,
-    ) -> ClientResponse:
-        """
-        Execute HTTP request.
-
-        Parameters:
-        -----------
-
-        request_url: HTTP request URL.
-
-        payload_body: HTTP request body.
-
-        endpoint: QnA Maker endpoint details.
-
-        timeout: Timeout for HTTP call (milliseconds).
-        """
-        if not request_url:
-            raise TypeError(
-                "HttpRequestUtils.execute_http_request(): request_url cannot be None."
-            )
-
-        if not payload_body:
-            raise TypeError(
-                "HttpRequestUtils.execute_http_request(): question cannot be None."
-            )
-
-        if not endpoint:
-            raise TypeError(
-                "HttpRequestUtils.execute_http_request(): endpoint cannot be None."
-            )
-
-        serialized_payload_body = json.dumps(payload_body.serialize())
-
-        headers = self._get_headers(endpoint)
-
-        if timeout:
-            # Convert miliseconds to seconds (as other BotBuilder SDKs accept timeout value in miliseconds)
-            # aiohttp.ClientSession units are in seconds
-            request_timeout = ClientTimeout(total=timeout / 1000)
-
-            response: ClientResponse = await self._http_client.post(
-                request_url,
-                data=serialized_payload_body,
-                headers=headers,
-                timeout=request_timeout,
-            )
-        else:
-            response: ClientResponse = await self._http_client.post(
-                request_url, data=serialized_payload_body, headers=headers
-            )
-
-        return response
-
-    def _get_headers(self, endpoint: QnAMakerEndpoint):
-        headers = {
-            "Content-Type": "application/json",
-            "User-Agent": self._get_user_agent(),
-            "Authorization": f"EndpointKey {endpoint.endpoint_key}",
-        }
-
-        return headers
-
-    def _get_user_agent(self):
-        package_user_agent = f"{__title__}/{__version__}"
-        uname = platform.uname()
-        os_version = f"{uname.machine}-{uname.system}-{uname.version}"
-        py_version = f"Python,Version={platform.python_version()}"
-        platform_user_agent = f"({os_version}; {py_version})"
-        user_agent = f"{package_user_agent} {platform_user_agent}"
-
-        return user_agent
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import json
+import platform
+from typing import Any
+import requests
+
+from aiohttp import ClientResponse, ClientSession, ClientTimeout
+
+from ... import __title__, __version__
+
+from ..qnamaker_endpoint import QnAMakerEndpoint
+
+
+class HttpRequestUtils:
+    """ HTTP request utils class.
+
+    Parameters:
+    -----------
+
+    http_client: Client to make HTTP requests with. Default client used in the SDK is `aiohttp.ClientSession`.
+    """
+
+    def __init__(self, http_client: Any):
+        self._http_client = http_client
+
+    async def execute_http_request(
+        self,
+        request_url: str,
+        payload_body: object,
+        endpoint: QnAMakerEndpoint,
+        timeout: float = None,
+    ) -> Any:
+        """
+        Execute HTTP request.
+
+        Parameters:
+        -----------
+
+        request_url: HTTP request URL.
+
+        payload_body: HTTP request body.
+
+        endpoint: QnA Maker endpoint details.
+
+        timeout: Timeout for HTTP call (milliseconds).
+        """
+        if not request_url:
+            raise TypeError(
+                "HttpRequestUtils.execute_http_request(): request_url cannot be None."
+            )
+
+        if not payload_body:
+            raise TypeError(
+                "HttpRequestUtils.execute_http_request(): question cannot be None."
+            )
+
+        if not endpoint:
+            raise TypeError(
+                "HttpRequestUtils.execute_http_request(): endpoint cannot be None."
+            )
+
+        serialized_payload_body = json.dumps(payload_body.serialize())
+
+        headers = self._get_headers(endpoint)
+
+        if isinstance(self._http_client, ClientSession):
+            response: ClientResponse = await self._make_request_with_aiohttp(
+                request_url, serialized_payload_body, headers, timeout
+            )
+        elif self._is_using_requests_module():
+            response: requests.Response = self._make_request_with_requests(
+                request_url, serialized_payload_body, headers, timeout
+            )
+        else:
+            response = await self._http_client.post(
+                request_url, data=serialized_payload_body, headers=headers
+            )
+
+        return response
+
+    def _get_headers(self, endpoint: QnAMakerEndpoint):
+        headers = {
+            "Content-Type": "application/json",
+            "User-Agent": self._get_user_agent(),
+            "Authorization": f"EndpointKey {endpoint.endpoint_key}",
+            "Ocp-Apim-Subscription-Key": f"EndpointKey {endpoint.endpoint_key}",
+        }
+
+        return headers
+
+    def _get_user_agent(self):
+        package_user_agent = f"{__title__}/{__version__}"
+        uname = platform.uname()
+        os_version = f"{uname.machine}-{uname.system}-{uname.version}"
+        py_version = f"Python,Version={platform.python_version()}"
+        platform_user_agent = f"({os_version}; {py_version})"
+        user_agent = f"{package_user_agent} {platform_user_agent}"
+
+        return user_agent
+
+    def _is_using_requests_module(self) -> bool:
+        return (type(self._http_client).__name__ == "module") and (
+            self._http_client.__name__ == "requests"
+        )
+
+    async def _make_request_with_aiohttp(
+        self, request_url: str, payload_body: str, headers: dict, timeout: float
+    ) -> ClientResponse:
+        if timeout:
+            # aiohttp.ClientSession's timeouts are in seconds
+            timeout_in_seconds = ClientTimeout(total=timeout / 1000)
+
+            return await self._http_client.post(
+                request_url,
+                data=payload_body,
+                headers=headers,
+                timeout=timeout_in_seconds,
+            )
+
+        return await self._http_client.post(
+            request_url, data=payload_body, headers=headers
+        )
+
+    def _make_request_with_requests(
+        self, request_url: str, payload_body: str, headers: dict, timeout: float
+    ) -> requests.Response:
+        if timeout:
+            # requests' timeouts are in seconds
+            timeout_in_seconds = timeout / 1000
+
+            return self._http_client.post(
+                request_url,
+                data=payload_body,
+                headers=headers,
+                timeout=timeout_in_seconds,
+            )
+
+        return self._http_client.post(request_url, data=payload_body, headers=headers)
diff --git a/libraries/botbuilder-ai/botbuilder/ai/qna/utils/qna_card_builder.py b/libraries/botbuilder-ai/botbuilder/ai/qna/utils/qna_card_builder.py
new file mode 100644
index 000000000..75785c78c
--- /dev/null
+++ b/libraries/botbuilder-ai/botbuilder/ai/qna/utils/qna_card_builder.py
@@ -0,0 +1,81 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from typing import List
+from botbuilder.core import CardFactory
+from botbuilder.schema import Activity, ActivityTypes, CardAction, HeroCard
+
+from ..models import QueryResult
+
+
+class QnACardBuilder:
+    """
+    Message activity card builder for QnAMaker dialogs.
+    """
+
+    @staticmethod
+    def get_suggestions_card(
+        suggestions: List[str], card_title: str, card_no_match: str
+    ) -> Activity:
+        """
+        Get active learning suggestions card.
+        """
+
+        if not suggestions:
+            raise TypeError("suggestions list is required")
+
+        if not card_title:
+            raise TypeError("card_title is required")
+
+        if not card_no_match:
+            raise TypeError("card_no_match is required")
+
+        # Add all suggestions
+        button_list = [
+            CardAction(value=suggestion, type="imBack", title=suggestion)
+            for suggestion in suggestions
+        ]
+
+        # Add No match text
+        button_list.append(
+            CardAction(value=card_no_match, type="imBack", title=card_no_match)
+        )
+
+        attachment = CardFactory.hero_card(HeroCard(buttons=button_list))
+
+        return Activity(
+            type=ActivityTypes.message, text=card_title, attachments=[attachment]
+        )
+
+    @staticmethod
+    def get_qna_prompts_card(result: QueryResult, card_no_match_text: str) -> Activity:
+        """
+        Get active learning suggestions card.
+        """
+
+        if not result:
+            raise TypeError("result is required")
+
+        if not card_no_match_text:
+            raise TypeError("card_no_match_text is required")
+
+        # Add all prompts
+        button_list = [
+            CardAction(
+                value=prompt.display_text, type="imBack", title=prompt.display_text,
+            )
+            for prompt in result.context.prompts
+        ]
+
+        # Add No match text
+        button_list.append(
+            CardAction(
+                value=card_no_match_text, type="imBack", title=card_no_match_text,
+            )
+        )
+
+        attachment = CardFactory.hero_card(HeroCard(buttons=button_list))
+
+        return Activity(
+            type=ActivityTypes.message, text=result.answer, attachments=[attachment]
+        )
diff --git a/libraries/botbuilder-ai/requirements.txt b/libraries/botbuilder-ai/requirements.txt
index d5b49e330..2d1b061e8 100644
--- a/libraries/botbuilder-ai/requirements.txt
+++ b/libraries/botbuilder-ai/requirements.txt
@@ -1,6 +1,6 @@
-msrest>=0.6.6
-botbuilder-schema>=4.4.0b1
-botbuilder-core>=4.4.0b1
-requests>=2.18.1
-aiounittest>=1.1.0
-azure-cognitiveservices-language-luis>=0.2.0
\ No newline at end of file
+msrest==0.6.10
+botbuilder-schema==4.12.0
+botbuilder-core==4.12.0
+requests==2.23.0
+aiounittest==1.3.0
+azure-cognitiveservices-language-luis==0.2.0
\ No newline at end of file
diff --git a/libraries/botbuilder-ai/setup.py b/libraries/botbuilder-ai/setup.py
index ba8c272df..79f39a121 100644
--- a/libraries/botbuilder-ai/setup.py
+++ b/libraries/botbuilder-ai/setup.py
@@ -6,9 +6,9 @@
 
 REQUIRES = [
     "azure-cognitiveservices-language-luis==0.2.0",
-    "botbuilder-schema>=4.4.0b1",
-    "botbuilder-core>=4.4.0b1",
-    "aiohttp>=3.5.4",
+    "botbuilder-schema==4.12.0",
+    "botbuilder-core==4.12.0",
+    "aiohttp==3.6.2",
 ]
 
 TESTS_REQUIRES = ["aiounittest>=1.1.0"]
@@ -39,6 +39,7 @@
         "botbuilder.ai.luis",
         "botbuilder.ai.qna.models",
         "botbuilder.ai.qna.utils",
+        "botbuilder.ai.qna.dialogs",
     ],
     install_requires=REQUIRES + TESTS_REQUIRES,
     tests_require=TESTS_REQUIRES,
@@ -48,7 +49,7 @@
         "Intended Audience :: Developers",
         "License :: OSI Approved :: MIT License",
         "Operating System :: OS Independent",
-        "Development Status :: 3 - Alpha",
+        "Development Status :: 5 - Production/Stable",
         "Topic :: Scientific/Engineering :: Artificial Intelligence",
     ],
 )
diff --git a/libraries/botbuilder-ai/tests/luis/luis_recognizer_test.py b/libraries/botbuilder-ai/tests/luis/luis_recognizer_test.py
index d6bc0c4df..33a45ff59 100644
--- a/libraries/botbuilder-ai/tests/luis/luis_recognizer_test.py
+++ b/libraries/botbuilder-ai/tests/luis/luis_recognizer_test.py
@@ -10,10 +10,6 @@
 from unittest.mock import MagicMock, Mock
 
 from aiounittest import AsyncTestCase
-from azure.cognitiveservices.language.luis.runtime import LUISRuntimeClient
-from azure.cognitiveservices.language.luis.runtime.luis_runtime_client import (
-    LUISRuntimeClientConfiguration,
-)
 from msrest import Deserializer
 from requests import Session
 from requests.models import Response
@@ -70,21 +66,6 @@ def test_luis_recognizer_construction(self):
         self.assertEqual("048ec46dc58e495482b0c447cfdbd291", app.endpoint_key)
         self.assertEqual("https://westus.api.cognitive.microsoft.com", app.endpoint)
 
-    def test_luis_recognizer_timeout(self):
-        endpoint = (
-            "https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/"
-            "b31aeaf3-3511-495b-a07f-571fc873214b?verbose=true&timezoneOffset=-360"
-            "&subscription-key=048ec46dc58e495482b0c447cfdbd291&q="
-        )
-        expected_timeout = 300
-        options_with_timeout = LuisPredictionOptions(timeout=expected_timeout * 1000)
-
-        recognizer_with_timeout = LuisRecognizer(endpoint, options_with_timeout)
-
-        self.assertEqual(
-            expected_timeout, recognizer_with_timeout._runtime.config.connection.timeout
-        )
-
     def test_none_endpoint(self):
         # Arrange
         my_app = LuisApplication(
@@ -418,24 +399,6 @@ def test_top_intent_returns_top_intent_if_score_equals_min_score(self):
         )
         self.assertEqual(default_intent, "Greeting")
 
-    async def test_user_agent_contains_product_version(self):
-        utterance: str = "please book from May 5 to June 6"
-        response_path: str = "MultipleDateTimeEntities.json"  # it doesn't matter to use which file.
-
-        recognizer, _ = await LuisRecognizerTest._get_recognizer_result(
-            utterance, response_path, bot_adapter=NullAdapter()
-        )
-
-        runtime: LUISRuntimeClient = recognizer._runtime
-        config: LUISRuntimeClientConfiguration = runtime.config
-        user_agent = config.user_agent
-
-        # Verify we didn't unintentionally stamp on the user-agent from the client.
-        self.assertTrue("azure-cognitiveservices-language-luis" in user_agent)
-
-        # And that we added the bot.builder package details.
-        self.assertTrue("botbuilder-ai/4" in user_agent)
-
     def test_telemetry_construction(self):
         # Arrange
         # Note this is NOT a real LUIS application ID nor a real LUIS subscription-key
diff --git a/libraries/botbuilder-ai/tests/luis/luis_recognizer_v3_test.py b/libraries/botbuilder-ai/tests/luis/luis_recognizer_v3_test.py
new file mode 100644
index 000000000..b87252deb
--- /dev/null
+++ b/libraries/botbuilder-ai/tests/luis/luis_recognizer_v3_test.py
@@ -0,0 +1,251 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+# pylint: disable=no-value-for-parameter
+
+import json
+from os import path
+from typing import Dict, Tuple, Union
+
+import re
+from unittest import mock
+from unittest.mock import MagicMock
+from aioresponses import aioresponses
+from aiounittest import AsyncTestCase
+from botbuilder.ai.luis import LuisRecognizerOptionsV3
+from botbuilder.ai.luis import LuisApplication, LuisPredictionOptions, LuisRecognizer
+from botbuilder.ai.luis.luis_util import LuisUtil
+from botbuilder.core import (
+    BotAdapter,
+    IntentScore,
+    RecognizerResult,
+    TurnContext,
+)
+from botbuilder.core.adapters import TestAdapter
+from botbuilder.schema import (
+    Activity,
+    ActivityTypes,
+    ChannelAccount,
+    ConversationAccount,
+)
+
+
+class LuisRecognizerV3Test(AsyncTestCase):
+    _luisAppId: str = "b31aeaf3-3511-495b-a07f-571fc873214b"
+    _subscriptionKey: str = "048ec46dc58e495482b0c447cfdbd291"
+    _endpoint: str = "https://westus.api.cognitive.microsoft.com"
+
+    def __init__(self, *args, **kwargs):
+        super(LuisRecognizerV3Test, self).__init__(*args, **kwargs)
+        self._mocked_results: RecognizerResult = RecognizerResult(
+            intents={"Test": IntentScore(score=0.2), "Greeting": IntentScore(score=0.4)}
+        )
+        self._empty_luis_response: Dict[str, object] = json.loads(
+            '{ "query": null, "intents": [], "entities": [] }'
+        )
+
+    @staticmethod
+    def _remove_none_property(dictionary: Dict[str, object]) -> Dict[str, object]:
+        for key, value in list(dictionary.items()):
+            if value is None:
+                del dictionary[key]
+            elif isinstance(value, dict):
+                LuisRecognizerV3Test._remove_none_property(value)
+        return dictionary
+
+    @classmethod
+    @aioresponses()
+    async def _get_recognizer_result(
+        cls,
+        utterance: str,
+        response_json: Union[str, Dict[str, object]],
+        mock_get,
+        bot_adapter: BotAdapter = TestAdapter(),
+        options: Union[LuisRecognizerOptionsV3, LuisPredictionOptions] = None,
+        include_api_results: bool = False,
+        telemetry_properties: Dict[str, str] = None,
+        telemetry_metrics: Dict[str, float] = None,
+        recognizer_class: type = LuisRecognizer,
+    ) -> Tuple[LuisRecognizer, RecognizerResult]:
+        if isinstance(response_json, str):
+            response_json = LuisRecognizerV3Test._get_json_for_file(
+                response_file=response_json
+            )
+
+        recognizer = LuisRecognizerV3Test._get_luis_recognizer(
+            recognizer_class, include_api_results=include_api_results, options=options
+        )
+        context = LuisRecognizerV3Test._get_context(utterance, bot_adapter)
+        # mock_get.return_value.__aenter__.return_value.json = CoroutineMock(side_effect=[response_json])
+
+        pattern = re.compile(r"^https://westus.api.cognitive.microsoft.com.*$")
+        mock_get.post(pattern, payload=response_json, status=200)
+
+        result = await recognizer.recognize(
+            context, telemetry_properties, telemetry_metrics
+        )
+        return recognizer, result
+
+    @classmethod
+    def _get_json_for_file(cls, response_file: str) -> Dict[str, object]:
+        curr_dir = path.dirname(path.abspath(__file__))
+        response_path = path.join(curr_dir, "test_data", response_file)
+
+        with open(response_path, "r", encoding="utf-8-sig") as file:
+            response_str = file.read()
+        response_json = json.loads(response_str)
+        return response_json
+
+    @classmethod
+    def _get_luis_recognizer(
+        cls,
+        recognizer_class: type,
+        options: Union[LuisPredictionOptions, LuisRecognizerOptionsV3] = None,
+        include_api_results: bool = False,
+    ) -> LuisRecognizer:
+        luis_app = LuisApplication(cls._luisAppId, cls._subscriptionKey, cls._endpoint)
+
+        if isinstance(options, LuisRecognizerOptionsV3):
+            LuisRecognizerOptionsV3.include_api_results = include_api_results
+
+        return recognizer_class(
+            luis_app,
+            prediction_options=options,
+            include_api_results=include_api_results,
+        )
+
+    @staticmethod
+    def _get_context(utterance: str, bot_adapter: BotAdapter) -> TurnContext:
+        activity = Activity(
+            type=ActivityTypes.message,
+            text=utterance,
+            conversation=ConversationAccount(),
+            recipient=ChannelAccount(),
+            from_property=ChannelAccount(),
+        )
+        return TurnContext(bot_adapter, activity)
+
+    # Luis V3 endpoint tests begin here
+    async def _test_json_v3(self, response_file: str) -> None:
+        # Arrange
+        expected_json = LuisRecognizerV3Test._get_json_for_file(response_file)
+        response_json = expected_json["v3"]["response"]
+        utterance = expected_json.get("text")
+        if utterance is None:
+            utterance = expected_json.get("Text")
+
+        test_options = expected_json["v3"]["options"]
+
+        options = LuisRecognizerOptionsV3(
+            include_all_intents=test_options["includeAllIntents"],
+            include_instance_data=test_options["includeInstanceData"],
+            log=test_options["log"],
+            prefer_external_entities=test_options["preferExternalEntities"],
+            slot=test_options["slot"],
+            include_api_results=test_options["includeAPIResults"],
+        )
+
+        if "version" in test_options:
+            options.version = test_options["version"]
+
+        if "externalEntities" in test_options:
+            options.external_entities = test_options["externalEntities"]
+
+        # dynamic_lists: List = None,
+        # external_entities: List = None,
+        # telemetry_client: BotTelemetryClient = NullTelemetryClient(),
+        # log_personal_information: bool = False,)
+        # ,
+
+        # Act
+        _, result = await LuisRecognizerV3Test._get_recognizer_result(
+            utterance, response_json, options=options, include_api_results=True
+        )
+
+        # Assert
+        actual_result_json = LuisUtil.recognizer_result_as_dict(result)
+        del expected_json["v3"]
+        trimmed_expected = LuisRecognizerV3Test._remove_none_property(expected_json)
+        trimmed_actual = LuisRecognizerV3Test._remove_none_property(actual_result_json)
+
+        self.assertEqual(trimmed_expected, trimmed_actual)
+
+    async def test_composite1_v3(self):
+        await self._test_json_v3("Composite1_v3.json")
+
+    async def test_composite2_v3(self):
+        await self._test_json_v3("Composite2_v3.json")
+
+    async def test_composite3_v3(self):
+        await self._test_json_v3("Composite3_v3.json")
+
+    async def test_external_entities_and_built_in_v3(self):
+        await self._test_json_v3("ExternalEntitiesAndBuiltIn_v3.json")
+
+    async def test_external_entities_and_composite_v3(self):
+        await self._test_json_v3("ExternalEntitiesAndComposite_v3.json")
+
+    async def test_external_entities_and_list_v3(self):
+        await self._test_json_v3("ExternalEntitiesAndList_v3.json")
+
+    async def test_external_entities_and_regex_v3(self):
+        await self._test_json_v3("ExternalEntitiesAndRegex_v3.json")
+
+    async def test_external_entities_and_simple_v3(self):
+        await self._test_json_v3("ExternalEntitiesAndSimple_v3.json")
+
+    async def test_geo_people_ordinal_v3(self):
+        await self._test_json_v3("GeoPeopleOrdinal_v3.json")
+
+    async def test_minimal_v3(self):
+        await self._test_json_v3("Minimal_v3.json")
+
+    async def test_no_entities_instance_true_v3(self):
+        await self._test_json_v3("NoEntitiesInstanceTrue_v3.json")
+
+    async def test_patterns_v3(self):
+        await self._test_json_v3("Patterns_v3.json")
+
+    async def test_prebuilt_v3(self):
+        await self._test_json_v3("Prebuilt_v3.json")
+
+    async def test_roles_v3(self):
+        await self._test_json_v3("roles_v3.json")
+
+    async def test_trace_activity(self):
+        # Arrange
+        utterance: str = "fly on delta at 3pm"
+        expected_json = LuisRecognizerV3Test._get_json_for_file("Minimal_v3.json")
+        response_json = expected_json["v3"]["response"]
+
+        # add async support to magic mock.
+        async def async_magic():
+            pass
+
+        MagicMock.__await__ = lambda x: async_magic().__await__()
+
+        # Act
+        with mock.patch.object(TurnContext, "send_activity") as mock_send_activity:
+            await LuisRecognizerV3Test._get_recognizer_result(
+                utterance, response_json, options=LuisRecognizerOptionsV3()
+            )
+            trace_activity: Activity = mock_send_activity.call_args[0][0]
+
+        # Assert
+        self.assertIsNotNone(trace_activity)
+        self.assertEqual(LuisRecognizer.luis_trace_type, trace_activity.value_type)
+        self.assertEqual(LuisRecognizer.luis_trace_label, trace_activity.label)
+
+        luis_trace_info = trace_activity.value
+        self.assertIsNotNone(luis_trace_info)
+        self.assertIsNotNone(luis_trace_info["recognizerResult"])
+        self.assertIsNotNone(luis_trace_info["luisResult"])
+        self.assertIsNotNone(luis_trace_info["luisOptions"])
+        self.assertIsNotNone(luis_trace_info["luisModel"])
+
+        recognizer_result: RecognizerResult = luis_trace_info["recognizerResult"]
+        self.assertEqual(utterance, recognizer_result["text"])
+        self.assertIsNotNone(recognizer_result["intents"]["Roles"])
+        self.assertEqual(
+            LuisRecognizerV3Test._luisAppId, luis_trace_info["luisModel"]["ModelID"]
+        )
diff --git a/libraries/botbuilder-ai/tests/luis/null_adapter.py b/libraries/botbuilder-ai/tests/luis/null_adapter.py
index 8c8835c14..61c1c8931 100644
--- a/libraries/botbuilder-ai/tests/luis/null_adapter.py
+++ b/libraries/botbuilder-ai/tests/luis/null_adapter.py
@@ -12,7 +12,11 @@ class NullAdapter(BotAdapter):
     This is a BotAdapter that does nothing on the Send operation, equivalent to piping to /dev/null.
     """
 
-    async def send_activities(self, context: TurnContext, activities: List[Activity]):
+    # pylint: disable=unused-argument
+
+    async def send_activities(
+        self, context: TurnContext, activities: List[Activity]
+    ) -> List[ResourceResponse]:
         return [ResourceResponse()]
 
     async def update_activity(self, context: TurnContext, activity: Activity):
diff --git a/libraries/botbuilder-ai/tests/luis/test_data/Composite1_v3.json b/libraries/botbuilder-ai/tests/luis/test_data/Composite1_v3.json
new file mode 100644
index 000000000..5d1266497
--- /dev/null
+++ b/libraries/botbuilder-ai/tests/luis/test_data/Composite1_v3.json
@@ -0,0 +1,1285 @@
+{
+  "text": "12 years old and 3 days old and monday july 3rd, 2019 and every monday and between 3am and 5:30am and 4 acres and 4 pico meters and chrimc@hotmail.com and $4 and $4.25 and also 32 and 210.4 and first and 10% and 10.5% and 425-555-1234 and 3 degrees and -27.5 degrees c and the next one and the previous one",
+  "intents": {
+    "Cancel": {
+      "score": 0.00000156337478
+    },
+    "Delivery": {
+      "score": 0.0002846266
+    },
+    "EntityTests": {
+      "score": 0.953405857
+    },
+    "Greeting": {
+      "score": 8.20979437e-7
+    },
+    "Help": {
+      "score": 0.00000481870757
+    },
+    "None": {
+      "score": 0.01040122
+    },
+    "Roles": {
+      "score": 0.197366714
+    },
+    "search": {
+      "score": 0.14049834
+    },
+    "SpecifyName": {
+      "score": 0.000137732946
+    },
+    "Travel": {
+      "score": 0.0100996653
+    },
+    "Weather_GetForecast": {
+      "score": 0.0143940123
+    }
+  },
+  "entities": {
+    "$instance": {
+      "Composite1": [
+        {
+          "endIndex": 306,
+          "modelType": "Composite Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "score": 0.880988955,
+          "startIndex": 0,
+          "text": "12 years old and 3 days old and monday july 3rd, 2019 and every monday and between 3am and 5:30am and 4 acres and 4 pico meters and chrimc@hotmail.com and $4 and $4.25 and also 32 and 210.4 and first and 10% and 10.5% and 425-555-1234 and 3 degrees and -27.5 degrees c and the next one and the previous one",
+          "type": "Composite1"
+        }
+      ],
+      "ordinalV2": [
+        {
+          "endIndex": 47,
+          "modelType": "Prebuilt Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "startIndex": 44,
+          "text": "3rd",
+          "type": "builtin.ordinalV2"
+        },
+        {
+          "endIndex": 199,
+          "modelType": "Prebuilt Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "startIndex": 194,
+          "text": "first",
+          "type": "builtin.ordinalV2"
+        },
+        {
+          "endIndex": 285,
+          "modelType": "Prebuilt Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "startIndex": 277,
+          "text": "next one",
+          "type": "builtin.ordinalV2.relative"
+        },
+        {
+          "endIndex": 306,
+          "modelType": "Prebuilt Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "startIndex": 294,
+          "text": "previous one",
+          "type": "builtin.ordinalV2.relative"
+        }
+      ]
+    },
+    "Composite1": [
+      {
+        "$instance": {
+          "age": [
+            {
+              "endIndex": 12,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 0,
+              "text": "12 years old",
+              "type": "builtin.age"
+            },
+            {
+              "endIndex": 27,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 17,
+              "text": "3 days old",
+              "type": "builtin.age"
+            }
+          ],
+          "datetime": [
+            {
+              "endIndex": 8,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 0,
+              "text": "12 years",
+              "type": "builtin.datetimeV2.duration"
+            },
+            {
+              "endIndex": 23,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 17,
+              "text": "3 days",
+              "type": "builtin.datetimeV2.duration"
+            },
+            {
+              "endIndex": 53,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 32,
+              "text": "monday july 3rd, 2019",
+              "type": "builtin.datetimeV2.date"
+            },
+            {
+              "endIndex": 70,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 58,
+              "text": "every monday",
+              "type": "builtin.datetimeV2.set"
+            },
+            {
+              "endIndex": 97,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 75,
+              "text": "between 3am and 5:30am",
+              "type": "builtin.datetimeV2.timerange"
+            }
+          ],
+          "dimension": [
+            {
+              "endIndex": 109,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 102,
+              "text": "4 acres",
+              "type": "builtin.dimension"
+            },
+            {
+              "endIndex": 127,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 114,
+              "text": "4 pico meters",
+              "type": "builtin.dimension"
+            }
+          ],
+          "email": [
+            {
+              "endIndex": 150,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 132,
+              "text": "chrimc@hotmail.com",
+              "type": "builtin.email"
+            }
+          ],
+          "money": [
+            {
+              "endIndex": 157,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 155,
+              "text": "$4",
+              "type": "builtin.currency"
+            },
+            {
+              "endIndex": 167,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 162,
+              "text": "$4.25",
+              "type": "builtin.currency"
+            }
+          ],
+          "number": [
+            {
+              "endIndex": 2,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 0,
+              "text": "12",
+              "type": "builtin.number"
+            },
+            {
+              "endIndex": 18,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 17,
+              "text": "3",
+              "type": "builtin.number"
+            },
+            {
+              "endIndex": 53,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 49,
+              "text": "2019",
+              "type": "builtin.number"
+            },
+            {
+              "endIndex": 92,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 91,
+              "text": "5",
+              "type": "builtin.number"
+            },
+            {
+              "endIndex": 103,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 102,
+              "text": "4",
+              "type": "builtin.number"
+            },
+            {
+              "endIndex": 115,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 114,
+              "text": "4",
+              "type": "builtin.number"
+            },
+            {
+              "endIndex": 157,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 156,
+              "text": "4",
+              "type": "builtin.number"
+            },
+            {
+              "endIndex": 167,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 163,
+              "text": "4.25",
+              "type": "builtin.number"
+            },
+            {
+              "endIndex": 179,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 177,
+              "text": "32",
+              "type": "builtin.number"
+            },
+            {
+              "endIndex": 189,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 184,
+              "text": "210.4",
+              "type": "builtin.number"
+            },
+            {
+              "endIndex": 206,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 204,
+              "text": "10",
+              "type": "builtin.number"
+            },
+            {
+              "endIndex": 216,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 212,
+              "text": "10.5",
+              "type": "builtin.number"
+            },
+            {
+              "endIndex": 225,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 222,
+              "text": "425",
+              "type": "builtin.number"
+            },
+            {
+              "endIndex": 229,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 226,
+              "text": "555",
+              "type": "builtin.number"
+            },
+            {
+              "endIndex": 234,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 230,
+              "text": "1234",
+              "type": "builtin.number"
+            },
+            {
+              "endIndex": 240,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 239,
+              "text": "3",
+              "type": "builtin.number"
+            },
+            {
+              "endIndex": 258,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 253,
+              "text": "-27.5",
+              "type": "builtin.number"
+            },
+            {
+              "endIndex": 285,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 282,
+              "text": "one",
+              "type": "builtin.number"
+            },
+            {
+              "endIndex": 306,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 303,
+              "text": "one",
+              "type": "builtin.number"
+            }
+          ],
+          "percentage": [
+            {
+              "endIndex": 207,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 204,
+              "text": "10%",
+              "type": "builtin.percentage"
+            },
+            {
+              "endIndex": 217,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 212,
+              "text": "10.5%",
+              "type": "builtin.percentage"
+            }
+          ],
+          "phonenumber": [
+            {
+              "endIndex": 234,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "score": 0.9,
+              "startIndex": 222,
+              "text": "425-555-1234",
+              "type": "builtin.phonenumber"
+            }
+          ],
+          "temperature": [
+            {
+              "endIndex": 248,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 239,
+              "text": "3 degrees",
+              "type": "builtin.temperature"
+            },
+            {
+              "endIndex": 268,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 253,
+              "text": "-27.5 degrees c",
+              "type": "builtin.temperature"
+            }
+          ]
+        },
+        "age": [
+          {
+            "number": 12,
+            "units": "Year"
+          },
+          {
+            "number": 3,
+            "units": "Day"
+          }
+        ],
+        "datetime": [
+          {
+            "timex": [
+              "P12Y"
+            ],
+            "type": "duration"
+          },
+          {
+            "timex": [
+              "P3D"
+            ],
+            "type": "duration"
+          },
+          {
+            "timex": [
+              "2019-07-03"
+            ],
+            "type": "date"
+          },
+          {
+            "timex": [
+              "XXXX-WXX-1"
+            ],
+            "type": "set"
+          },
+          {
+            "timex": [
+              "(T03,T05:30,PT2H30M)"
+            ],
+            "type": "timerange"
+          }
+        ],
+        "dimension": [
+          {
+            "number": 4,
+            "units": "Acre"
+          },
+          {
+            "number": 4,
+            "units": "Picometer"
+          }
+        ],
+        "email": [
+          "chrimc@hotmail.com"
+        ],
+        "money": [
+          {
+            "number": 4,
+            "units": "Dollar"
+          },
+          {
+            "number": 4.25,
+            "units": "Dollar"
+          }
+        ],
+        "number": [
+          12,
+          3,
+          2019,
+          5,
+          4,
+          4,
+          4,
+          4.25,
+          32,
+          210.4,
+          10,
+          10.5,
+          425,
+          555,
+          1234,
+          3,
+          -27.5,
+          1,
+          1
+        ],
+        "percentage": [
+          10,
+          10.5
+        ],
+        "phonenumber": [
+          "425-555-1234"
+        ],
+        "temperature": [
+          {
+            "number": 3,
+            "units": "Degree"
+          },
+          {
+            "number": -27.5,
+            "units": "C"
+          }
+        ]
+      }
+    ],
+    "ordinalV2": [
+      {
+        "offset": 3,
+        "relativeTo": "start"
+      },
+      {
+        "offset": 1,
+        "relativeTo": "start"
+      },
+      {
+        "offset": 1,
+        "relativeTo": "current"
+      },
+      {
+        "offset": -1,
+        "relativeTo": "current"
+      }
+    ]
+  },
+  "sentiment": {
+    "label": "neutral",
+    "score": 0.5
+  },
+  "v3": {
+    "response": {
+      "prediction": {
+        "entities": {
+          "$instance": {
+            "Composite1": [
+              {
+                "length": 306,
+                "modelType": "Composite Entity Extractor",
+                "modelTypeId": 4,
+                "recognitionSources": [
+                  "model"
+                ],
+                "score": 0.880988955,
+                "startIndex": 0,
+                "text": "12 years old and 3 days old and monday july 3rd, 2019 and every monday and between 3am and 5:30am and 4 acres and 4 pico meters and chrimc@hotmail.com and $4 and $4.25 and also 32 and 210.4 and first and 10% and 10.5% and 425-555-1234 and 3 degrees and -27.5 degrees c and the next one and the previous one",
+                "type": "Composite1"
+              }
+            ],
+            "ordinalV2": [
+              {
+                "length": 3,
+                "modelType": "Prebuilt Entity Extractor",
+                "modelTypeId": 2,
+                "recognitionSources": [
+                  "model"
+                ],
+                "startIndex": 44,
+                "text": "3rd",
+                "type": "builtin.ordinalV2"
+              },
+              {
+                "length": 5,
+                "modelType": "Prebuilt Entity Extractor",
+                "modelTypeId": 2,
+                "recognitionSources": [
+                  "model"
+                ],
+                "startIndex": 194,
+                "text": "first",
+                "type": "builtin.ordinalV2"
+              },
+              {
+                "length": 8,
+                "modelType": "Prebuilt Entity Extractor",
+                "modelTypeId": 2,
+                "recognitionSources": [
+                  "model"
+                ],
+                "startIndex": 277,
+                "text": "next one",
+                "type": "builtin.ordinalV2.relative"
+              },
+              {
+                "length": 12,
+                "modelType": "Prebuilt Entity Extractor",
+                "modelTypeId": 2,
+                "recognitionSources": [
+                  "model"
+                ],
+                "startIndex": 294,
+                "text": "previous one",
+                "type": "builtin.ordinalV2.relative"
+              }
+            ]
+          },
+          "Composite1": [
+            {
+              "$instance": {
+                "age": [
+                  {
+                    "length": 12,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 0,
+                    "text": "12 years old",
+                    "type": "builtin.age"
+                  },
+                  {
+                    "length": 10,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 17,
+                    "text": "3 days old",
+                    "type": "builtin.age"
+                  }
+                ],
+                "datetimeV2": [
+                  {
+                    "length": 8,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 0,
+                    "text": "12 years",
+                    "type": "builtin.datetimeV2.duration"
+                  },
+                  {
+                    "length": 6,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 17,
+                    "text": "3 days",
+                    "type": "builtin.datetimeV2.duration"
+                  },
+                  {
+                    "length": 21,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 32,
+                    "text": "monday july 3rd, 2019",
+                    "type": "builtin.datetimeV2.date"
+                  },
+                  {
+                    "length": 12,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 58,
+                    "text": "every monday",
+                    "type": "builtin.datetimeV2.set"
+                  },
+                  {
+                    "length": 22,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 75,
+                    "text": "between 3am and 5:30am",
+                    "type": "builtin.datetimeV2.timerange"
+                  }
+                ],
+                "dimension": [
+                  {
+                    "length": 7,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 102,
+                    "text": "4 acres",
+                    "type": "builtin.dimension"
+                  },
+                  {
+                    "length": 13,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 114,
+                    "text": "4 pico meters",
+                    "type": "builtin.dimension"
+                  }
+                ],
+                "email": [
+                  {
+                    "length": 18,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 132,
+                    "text": "chrimc@hotmail.com",
+                    "type": "builtin.email"
+                  }
+                ],
+                "money": [
+                  {
+                    "length": 2,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 155,
+                    "text": "$4",
+                    "type": "builtin.currency"
+                  },
+                  {
+                    "length": 5,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 162,
+                    "text": "$4.25",
+                    "type": "builtin.currency"
+                  }
+                ],
+                "number": [
+                  {
+                    "length": 2,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 0,
+                    "text": "12",
+                    "type": "builtin.number"
+                  },
+                  {
+                    "length": 1,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 17,
+                    "text": "3",
+                    "type": "builtin.number"
+                  },
+                  {
+                    "length": 4,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 49,
+                    "text": "2019",
+                    "type": "builtin.number"
+                  },
+                  {
+                    "length": 1,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 91,
+                    "text": "5",
+                    "type": "builtin.number"
+                  },
+                  {
+                    "length": 1,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 102,
+                    "text": "4",
+                    "type": "builtin.number"
+                  },
+                  {
+                    "length": 1,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 114,
+                    "text": "4",
+                    "type": "builtin.number"
+                  },
+                  {
+                    "length": 1,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 156,
+                    "text": "4",
+                    "type": "builtin.number"
+                  },
+                  {
+                    "length": 4,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 163,
+                    "text": "4.25",
+                    "type": "builtin.number"
+                  },
+                  {
+                    "length": 2,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 177,
+                    "text": "32",
+                    "type": "builtin.number"
+                  },
+                  {
+                    "length": 5,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 184,
+                    "text": "210.4",
+                    "type": "builtin.number"
+                  },
+                  {
+                    "length": 2,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 204,
+                    "text": "10",
+                    "type": "builtin.number"
+                  },
+                  {
+                    "length": 4,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 212,
+                    "text": "10.5",
+                    "type": "builtin.number"
+                  },
+                  {
+                    "length": 3,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 222,
+                    "text": "425",
+                    "type": "builtin.number"
+                  },
+                  {
+                    "length": 3,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 226,
+                    "text": "555",
+                    "type": "builtin.number"
+                  },
+                  {
+                    "length": 4,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 230,
+                    "text": "1234",
+                    "type": "builtin.number"
+                  },
+                  {
+                    "length": 1,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 239,
+                    "text": "3",
+                    "type": "builtin.number"
+                  },
+                  {
+                    "length": 5,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 253,
+                    "text": "-27.5",
+                    "type": "builtin.number"
+                  },
+                  {
+                    "length": 3,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 282,
+                    "text": "one",
+                    "type": "builtin.number"
+                  },
+                  {
+                    "length": 3,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 303,
+                    "text": "one",
+                    "type": "builtin.number"
+                  }
+                ],
+                "percentage": [
+                  {
+                    "length": 3,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 204,
+                    "text": "10%",
+                    "type": "builtin.percentage"
+                  },
+                  {
+                    "length": 5,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 212,
+                    "text": "10.5%",
+                    "type": "builtin.percentage"
+                  }
+                ],
+                "phonenumber": [
+                  {
+                    "length": 12,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "score": 0.9,
+                    "startIndex": 222,
+                    "text": "425-555-1234",
+                    "type": "builtin.phonenumber"
+                  }
+                ],
+                "temperature": [
+                  {
+                    "length": 9,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 239,
+                    "text": "3 degrees",
+                    "type": "builtin.temperature"
+                  },
+                  {
+                    "length": 15,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 253,
+                    "text": "-27.5 degrees c",
+                    "type": "builtin.temperature"
+                  }
+                ]
+              },
+              "age": [
+                {
+                  "number": 12,
+                  "unit": "Year"
+                },
+                {
+                  "number": 3,
+                  "unit": "Day"
+                }
+              ],
+              "datetimeV2": [
+                {
+                  "type": "duration",
+                  "values": [
+                    {
+                      "timex": "P12Y",
+                      "value": "378432000"
+                    }
+                  ]
+                },
+                {
+                  "type": "duration",
+                  "values": [
+                    {
+                      "timex": "P3D",
+                      "value": "259200"
+                    }
+                  ]
+                },
+                {
+                  "type": "date",
+                  "values": [
+                    {
+                      "timex": "2019-07-03",
+                      "value": "2019-07-03"
+                    }
+                  ]
+                },
+                {
+                  "type": "set",
+                  "values": [
+                    {
+                      "timex": "XXXX-WXX-1",
+                      "value": "not resolved"
+                    }
+                  ]
+                },
+                {
+                  "type": "timerange",
+                  "values": [
+                    {
+                      "end": "05:30:00",
+                      "start": "03:00:00",
+                      "timex": "(T03,T05:30,PT2H30M)"
+                    }
+                  ]
+                }
+              ],
+              "dimension": [
+                {
+                  "number": 4,
+                  "unit": "Acre"
+                },
+                {
+                  "number": 4,
+                  "unit": "Picometer"
+                }
+              ],
+              "email": [
+                "chrimc@hotmail.com"
+              ],
+              "money": [
+                {
+                  "number": 4,
+                  "unit": "Dollar"
+                },
+                {
+                  "number": 4.25,
+                  "unit": "Dollar"
+                }
+              ],
+              "number": [
+                12,
+                3,
+                2019,
+                5,
+                4,
+                4,
+                4,
+                4.25,
+                32,
+                210.4,
+                10,
+                10.5,
+                425,
+                555,
+                1234,
+                3,
+                -27.5,
+                1,
+                1
+              ],
+              "percentage": [
+                10,
+                10.5
+              ],
+              "phonenumber": [
+                "425-555-1234"
+              ],
+              "temperature": [
+                {
+                  "number": 3,
+                  "unit": "Degree"
+                },
+                {
+                  "number": -27.5,
+                  "unit": "C"
+                }
+              ]
+            }
+          ],
+          "ordinalV2": [
+            {
+              "offset": 3,
+              "relativeTo": "start"
+            },
+            {
+              "offset": 1,
+              "relativeTo": "start"
+            },
+            {
+              "offset": 1,
+              "relativeTo": "current"
+            },
+            {
+              "offset": -1,
+              "relativeTo": "current"
+            }
+          ]
+        },
+        "intents": {
+          "Cancel": {
+            "score": 0.00000156337478
+          },
+          "Delivery": {
+            "score": 0.0002846266
+          },
+          "EntityTests": {
+            "score": 0.953405857
+          },
+          "Greeting": {
+            "score": 8.20979437e-7
+          },
+          "Help": {
+            "score": 0.00000481870757
+          },
+          "None": {
+            "score": 0.01040122
+          },
+          "Roles": {
+            "score": 0.197366714
+          },
+          "search": {
+            "score": 0.14049834
+          },
+          "SpecifyName": {
+            "score": 0.000137732946
+          },
+          "Travel": {
+            "score": 0.0100996653
+          },
+          "Weather_GetForecast": {
+            "score": 0.0143940123
+          }
+        },
+        "normalizedQuery": "12 years old and 3 days old and monday july 3rd, 2019 and every monday and between 3am and 5:30am and 4 acres and 4 pico meters and chrimc@hotmail.com and $4 and $4.25 and also 32 and 210.4 and first and 10% and 10.5% and 425-555-1234 and 3 degrees and -27.5 degrees c and the next one and the previous one",
+        "sentiment": {
+          "label": "neutral",
+          "score": 0.5
+        },
+        "topIntent": "EntityTests"
+      },
+      "query": "12 years old and 3 days old and monday july 3rd, 2019 and every monday and between 3am and 5:30am and 4 acres and 4 pico meters and chrimc@hotmail.com and $4 and $4.25 and also 32 and 210.4 and first and 10% and 10.5% and 425-555-1234 and 3 degrees and -27.5 degrees c and the next one and the previous one"
+    },
+    "options": {
+      "includeAllIntents": true,
+      "includeAPIResults": true,
+      "includeInstanceData": true,
+      "log": true,
+      "preferExternalEntities": true,
+      "slot": "production"
+    }
+  }
+}
\ No newline at end of file
diff --git a/libraries/botbuilder-ai/tests/luis/test_data/Composite2_v3.json b/libraries/botbuilder-ai/tests/luis/test_data/Composite2_v3.json
new file mode 100644
index 000000000..11fc7bb89
--- /dev/null
+++ b/libraries/botbuilder-ai/tests/luis/test_data/Composite2_v3.json
@@ -0,0 +1,312 @@
+{
+    "entities": {
+      "$instance": {
+        "Composite2": [
+          {
+            "endIndex": 69,
+            "modelType": "Composite Entity Extractor",
+            "recognitionSources": [
+              "model"
+            ],
+            "score": 0.97076714,
+            "startIndex": 0,
+            "text": "http://foo.com is where you can fly from seattle to dallas via denver",
+            "type": "Composite2"
+          }
+        ],
+        "geographyV2": [
+          {
+            "endIndex": 48,
+            "modelType": "Prebuilt Entity Extractor",
+            "recognitionSources": [
+              "model"
+            ],
+            "startIndex": 41,
+            "text": "seattle",
+            "type": "builtin.geographyV2.city"
+          }
+        ]
+      },
+      "Composite2": [
+        {
+          "$instance": {
+            "City": [
+              {
+                "endIndex": 69,
+                "modelType": "Hierarchical Entity Extractor",
+                "recognitionSources": [
+                  "model"
+                ],
+                "score": 0.984581649,
+                "startIndex": 63,
+                "text": "denver",
+                "type": "City"
+              }
+            ],
+            "From": [
+              {
+                "endIndex": 48,
+                "modelType": "Hierarchical Entity Extractor",
+                "recognitionSources": [
+                  "model"
+                ],
+                "score": 0.999511,
+                "startIndex": 41,
+                "text": "seattle",
+                "type": "City::From"
+              }
+            ],
+            "To": [
+              {
+                "endIndex": 58,
+                "modelType": "Hierarchical Entity Extractor",
+                "recognitionSources": [
+                  "model"
+                ],
+                "score": 0.9984612,
+                "startIndex": 52,
+                "text": "dallas",
+                "type": "City::To"
+              }
+            ],
+            "url": [
+              {
+                "endIndex": 14,
+                "modelType": "Prebuilt Entity Extractor",
+                "recognitionSources": [
+                  "model"
+                ],
+                "startIndex": 0,
+                "text": "http://foo.com",
+                "type": "builtin.url"
+              }
+            ]
+          },
+          "City": [
+            "denver"
+          ],
+          "From": [
+            "seattle"
+          ],
+          "To": [
+            "dallas"
+          ],
+          "url": [
+            "http://foo.com"
+          ]
+        }
+      ],
+      "geographyV2": [
+        {
+          "location": "seattle",
+          "type": "city"
+        }
+      ]
+    },
+    "intents": {
+      "Cancel": {
+        "score": 0.000227437369
+      },
+      "Delivery": {
+        "score": 0.001310123
+      },
+      "EntityTests": {
+        "score": 0.94500196
+      },
+      "Greeting": {
+        "score": 0.000152356763
+      },
+      "Help": {
+        "score": 0.000547201431
+      },
+      "None": {
+        "score": 0.004187195
+      },
+      "Roles": {
+        "score": 0.0300086979
+      },
+      "search": {
+        "score": 0.0108942846
+      },
+      "SpecifyName": {
+        "score": 0.00168467627
+      },
+      "Travel": {
+        "score": 0.0154484725
+      },
+      "Weather_GetForecast": {
+        "score": 0.0237181056
+      }
+    },
+    "sentiment": {
+      "label": "neutral",
+      "score": 0.5
+    },
+    "text": "http://foo.com is where you can fly from seattle to dallas via denver",
+    "v3": {
+      "options": {
+        "includeAllIntents": true,
+        "includeAPIResults": true,
+        "includeInstanceData": true,
+        "log": true,
+        "preferExternalEntities": true,
+        "slot": "production"
+      },
+      "response": {
+        "prediction": {
+          "entities": {
+            "$instance": {
+              "Composite2": [
+                {
+                  "length": 69,
+                  "modelType": "Composite Entity Extractor",
+                  "modelTypeId": 4,
+                  "recognitionSources": [
+                    "model"
+                  ],
+                  "score": 0.97076714,
+                  "startIndex": 0,
+                  "text": "http://foo.com is where you can fly from seattle to dallas via denver",
+                  "type": "Composite2"
+                }
+              ],
+              "geographyV2": [
+                {
+                  "length": 7,
+                  "modelType": "Prebuilt Entity Extractor",
+                  "modelTypeId": 2,
+                  "recognitionSources": [
+                    "model"
+                  ],
+                  "startIndex": 41,
+                  "text": "seattle",
+                  "type": "builtin.geographyV2.city"
+                }
+              ]
+            },
+            "Composite2": [
+              {
+                "$instance": {
+                  "City": [
+                    {
+                      "length": 6,
+                      "modelType": "Hierarchical Entity Extractor",
+                      "modelTypeId": 3,
+                      "recognitionSources": [
+                        "model"
+                      ],
+                      "score": 0.984581649,
+                      "startIndex": 63,
+                      "text": "denver",
+                      "type": "City"
+                    }
+                  ],
+                  "City::From": [
+                    {
+                      "length": 7,
+                      "modelType": "Hierarchical Entity Extractor",
+                      "modelTypeId": 3,
+                      "recognitionSources": [
+                        "model"
+                      ],
+                      "score": 0.999511,
+                      "startIndex": 41,
+                      "text": "seattle",
+                      "type": "City::From"
+                    }
+                  ],
+                  "City::To": [
+                    {
+                      "length": 6,
+                      "modelType": "Hierarchical Entity Extractor",
+                      "modelTypeId": 3,
+                      "recognitionSources": [
+                        "model"
+                      ],
+                      "score": 0.9984612,
+                      "startIndex": 52,
+                      "text": "dallas",
+                      "type": "City::To"
+                    }
+                  ],
+                  "url": [
+                    {
+                      "length": 14,
+                      "modelType": "Prebuilt Entity Extractor",
+                      "modelTypeId": 2,
+                      "recognitionSources": [
+                        "model"
+                      ],
+                      "startIndex": 0,
+                      "text": "http://foo.com",
+                      "type": "builtin.url"
+                    }
+                  ]
+                },
+                "City": [
+                  "denver"
+                ],
+                "City::From": [
+                  "seattle"
+                ],
+                "City::To": [
+                  "dallas"
+                ],
+                "url": [
+                  "http://foo.com"
+                ]
+              }
+            ],
+            "geographyV2": [
+              {
+                "type": "city",
+                "value": "seattle"
+              }
+            ]
+          },
+          "intents": {
+            "Cancel": {
+              "score": 0.000227437369
+            },
+            "Delivery": {
+              "score": 0.001310123
+            },
+            "EntityTests": {
+              "score": 0.94500196
+            },
+            "Greeting": {
+              "score": 0.000152356763
+            },
+            "Help": {
+              "score": 0.000547201431
+            },
+            "None": {
+              "score": 0.004187195
+            },
+            "Roles": {
+              "score": 0.0300086979
+            },
+            "search": {
+              "score": 0.0108942846
+            },
+            "SpecifyName": {
+              "score": 0.00168467627
+            },
+            "Travel": {
+              "score": 0.0154484725
+            },
+            "Weather.GetForecast": {
+              "score": 0.0237181056
+            }
+          },
+          "normalizedQuery": "http://foo.com is where you can fly from seattle to dallas via denver",
+          "sentiment": {
+            "label": "neutral",
+            "score": 0.5
+          },
+          "topIntent": "EntityTests"
+        },
+        "query": "http://foo.com is where you can fly from seattle to dallas via denver"
+      }
+    }
+  }
\ No newline at end of file
diff --git a/libraries/botbuilder-ai/tests/luis/test_data/Composite3_v3.json b/libraries/botbuilder-ai/tests/luis/test_data/Composite3_v3.json
new file mode 100644
index 000000000..fe55aba56
--- /dev/null
+++ b/libraries/botbuilder-ai/tests/luis/test_data/Composite3_v3.json
@@ -0,0 +1,315 @@
+{
+  "text": "Deliver from 12345 VA to 12346 WA",
+  "intents": {
+    "Cancel": {
+      "score": 1.01764708e-9
+    },
+    "Delivery": {
+      "score": 0.00238572317
+    },
+    "EntityTests": {
+      "score": 4.757576e-10
+    },
+    "Greeting": {
+      "score": 1.0875e-9
+    },
+    "Help": {
+      "score": 1.01764708e-9
+    },
+    "None": {
+      "score": 0.00000117844979
+    },
+    "Roles": {
+      "score": 0.999911964
+    },
+    "search": {
+      "score": 0.000009494859
+    },
+    "SpecifyName": {
+      "score": 3.0666667e-9
+    },
+    "Travel": {
+      "score": 0.00000309763345
+    },
+    "Weather_GetForecast": {
+      "score": 0.00000102792524
+    }
+  },
+  "entities": {
+    "$instance": {
+      "Destination": [
+        {
+          "endIndex": 33,
+          "modelType": "Composite Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "score": 0.9818366,
+          "startIndex": 25,
+          "text": "12346 WA",
+          "type": "Address"
+        }
+      ],
+      "Source": [
+        {
+          "endIndex": 21,
+          "modelType": "Composite Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "score": 0.9345161,
+          "startIndex": 13,
+          "text": "12345 VA",
+          "type": "Address"
+        }
+      ]
+    },
+    "Destination": [
+      {
+        "$instance": {
+          "number": [
+            {
+              "endIndex": 30,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 25,
+              "text": "12346",
+              "type": "builtin.number"
+            }
+          ],
+          "State": [
+            {
+              "endIndex": 33,
+              "modelType": "Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "score": 0.9893861,
+              "startIndex": 31,
+              "text": "WA",
+              "type": "State"
+            }
+          ]
+        },
+        "number": [
+          12346
+        ],
+        "State": [
+          "WA"
+        ]
+      }
+    ],
+    "Source": [
+      {
+        "$instance": {
+          "number": [
+            {
+              "endIndex": 18,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 13,
+              "text": "12345",
+              "type": "builtin.number"
+            }
+          ],
+          "State": [
+            {
+              "endIndex": 21,
+              "modelType": "Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "score": 0.941649556,
+              "startIndex": 19,
+              "text": "VA",
+              "type": "State"
+            }
+          ]
+        },
+        "number": [
+          12345
+        ],
+        "State": [
+          "VA"
+        ]
+      }
+    ]
+  },
+  "sentiment": {
+    "label": "neutral",
+    "score": 0.5
+  },
+  "v3": {
+    "response": {
+      "prediction": {
+        "entities": {
+          "$instance": {
+            "Destination": [
+              {
+                "length": 8,
+                "modelType": "Composite Entity Extractor",
+                "modelTypeId": 4,
+                "recognitionSources": [
+                  "model"
+                ],
+                "role": "Destination",
+                "score": 0.9818366,
+                "startIndex": 25,
+                "text": "12346 WA",
+                "type": "Address"
+              }
+            ],
+            "Source": [
+              {
+                "length": 8,
+                "modelType": "Composite Entity Extractor",
+                "modelTypeId": 4,
+                "recognitionSources": [
+                  "model"
+                ],
+                "role": "Source",
+                "score": 0.9345161,
+                "startIndex": 13,
+                "text": "12345 VA",
+                "type": "Address"
+              }
+            ]
+          },
+          "Destination": [
+            {
+              "$instance": {
+                "number": [
+                  {
+                    "length": 5,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 25,
+                    "text": "12346",
+                    "type": "builtin.number"
+                  }
+                ],
+                "State": [
+                  {
+                    "length": 2,
+                    "modelType": "Entity Extractor",
+                    "modelTypeId": 1,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "score": 0.9893861,
+                    "startIndex": 31,
+                    "text": "WA",
+                    "type": "State"
+                  }
+                ]
+              },
+              "number": [
+                12346
+              ],
+              "State": [
+                "WA"
+              ]
+            }
+          ],
+          "Source": [
+            {
+              "$instance": {
+                "number": [
+                  {
+                    "length": 5,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 13,
+                    "text": "12345",
+                    "type": "builtin.number"
+                  }
+                ],
+                "State": [
+                  {
+                    "length": 2,
+                    "modelType": "Entity Extractor",
+                    "modelTypeId": 1,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "score": 0.941649556,
+                    "startIndex": 19,
+                    "text": "VA",
+                    "type": "State"
+                  }
+                ]
+              },
+              "number": [
+                12345
+              ],
+              "State": [
+                "VA"
+              ]
+            }
+          ]
+        },
+        "intents": {
+          "Cancel": {
+            "score": 1.01764708e-9
+          },
+          "Delivery": {
+            "score": 0.00238572317
+          },
+          "EntityTests": {
+            "score": 4.757576e-10
+          },
+          "Greeting": {
+            "score": 1.0875e-9
+          },
+          "Help": {
+            "score": 1.01764708e-9
+          },
+          "None": {
+            "score": 0.00000117844979
+          },
+          "Roles": {
+            "score": 0.999911964
+          },
+          "search": {
+            "score": 0.000009494859
+          },
+          "SpecifyName": {
+            "score": 3.0666667e-9
+          },
+          "Travel": {
+            "score": 0.00000309763345
+          },
+          "Weather_GetForecast": {
+            "score": 0.00000102792524
+          }
+        },
+        "normalizedQuery": "deliver from 12345 va to 12346 wa",
+        "sentiment": {
+          "label": "neutral",
+          "score": 0.5
+        },
+        "topIntent": "Roles"
+      },
+      "query": "Deliver from 12345 VA to 12346 WA"
+    },
+    "options": {
+      "includeAllIntents": true,
+      "includeAPIResults": true,
+      "includeInstanceData": true,
+      "log": true,
+      "preferExternalEntities": true,
+      "slot": "production",
+      "version": "GeoPeople"
+    }
+  }
+}
\ No newline at end of file
diff --git a/libraries/botbuilder-ai/tests/luis/test_data/ExternalEntitiesAndBuiltIn_v3.json b/libraries/botbuilder-ai/tests/luis/test_data/ExternalEntitiesAndBuiltIn_v3.json
new file mode 100644
index 000000000..a451ebbb2
--- /dev/null
+++ b/libraries/botbuilder-ai/tests/luis/test_data/ExternalEntitiesAndBuiltIn_v3.json
@@ -0,0 +1,168 @@
+{
+  "text": "buy hul and 2 items",
+  "intents": {
+    "Cancel": {
+      "score": 0.006906527
+    },
+    "Delivery": {
+      "score": 0.00567273
+    },
+    "EntityTests": {
+      "score": 0.128755629
+    },
+    "Greeting": {
+      "score": 0.00450348156
+    },
+    "Help": {
+      "score": 0.00583425
+    },
+    "None": {
+      "score": 0.0135525977
+    },
+    "Roles": {
+      "score": 0.04635598
+    },
+    "search": {
+      "score": 0.008885799
+    },
+    "SpecifyName": {
+      "score": 0.00721160974
+    },
+    "Travel": {
+      "score": 0.005146626
+    },
+    "Weather_GetForecast": {
+      "score": 0.00913477
+    }
+  },
+  "entities": {
+    "$instance": {
+      "number": [
+        {
+          "endIndex": 7,
+          "modelType": "Prebuilt Entity Extractor",
+          "recognitionSources": [
+            "externalEntities"
+          ],
+          "startIndex": 4,
+          "text": "hul",
+          "type": "builtin.number"
+        },
+        {
+          "endIndex": 13,
+          "modelType": "Prebuilt Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "startIndex": 12,
+          "text": "2",
+          "type": "builtin.number"
+        }
+      ]
+    },
+    "number": [
+      8,
+      2
+    ]
+  },
+  "sentiment": {
+    "label": "positive",
+    "score": 0.7149857
+  },
+  "v3": {
+    "response": {
+      "prediction": {
+        "entities": {
+          "$instance": {
+            "number": [
+              {
+                "length": 3,
+                "modelType": "Prebuilt Entity Extractor",
+                "modelTypeId": 2,
+                "recognitionSources": [
+                  "externalEntities"
+                ],
+                "startIndex": 4,
+                "text": "hul",
+                "type": "builtin.number"
+              },
+              {
+                "length": 1,
+                "modelType": "Prebuilt Entity Extractor",
+                "modelTypeId": 2,
+                "recognitionSources": [
+                  "model"
+                ],
+                "startIndex": 12,
+                "text": "2",
+                "type": "builtin.number"
+              }
+            ]
+          },
+          "number": [
+            8,
+            2
+          ]
+        },
+        "intents": {
+          "Cancel": {
+            "score": 0.006906527
+          },
+          "Delivery": {
+            "score": 0.00567273
+          },
+          "EntityTests": {
+            "score": 0.128755629
+          },
+          "Greeting": {
+            "score": 0.00450348156
+          },
+          "Help": {
+            "score": 0.00583425
+          },
+          "None": {
+            "score": 0.0135525977
+          },
+          "Roles": {
+            "score": 0.04635598
+          },
+          "search": {
+            "score": 0.008885799
+          },
+          "SpecifyName": {
+            "score": 0.00721160974
+          },
+          "Travel": {
+            "score": 0.005146626
+          },
+          "Weather.GetForecast": {
+            "score": 0.00913477
+          }
+        },
+        "normalizedQuery": "buy hul and 2 items",
+        "sentiment": {
+          "label": "positive",
+          "score": 0.7149857
+        },
+        "topIntent": "EntityTests"
+      },
+      "query": "buy hul and 2 items"
+    },
+    "options": {
+      "externalEntities": [
+        {
+          "entityLength": 3,
+          "entityName": "number",
+          "resolution": 8,
+          "startIndex": 4
+        }
+      ],
+      "includeAllIntents": true,
+      "includeAPIResults": true,
+      "includeInstanceData": true,
+      "log": true,
+      "preferExternalEntities": true,
+      "slot": "production"
+    }
+  }
+}
\ No newline at end of file
diff --git a/libraries/botbuilder-ai/tests/luis/test_data/ExternalEntitiesAndComposite_v3.json b/libraries/botbuilder-ai/tests/luis/test_data/ExternalEntitiesAndComposite_v3.json
new file mode 100644
index 000000000..33c5d7342
--- /dev/null
+++ b/libraries/botbuilder-ai/tests/luis/test_data/ExternalEntitiesAndComposite_v3.json
@@ -0,0 +1,261 @@
+{
+    "entities": {
+      "$instance": {
+        "Address": [
+          {
+            "endIndex": 13,
+            "modelType": "Composite Entity Extractor",
+            "recognitionSources": [
+              "model"
+            ],
+            "score": 0.7160641,
+            "startIndex": 8,
+            "text": "35 WA",
+            "type": "Address"
+          },
+          {
+            "endIndex": 33,
+            "modelType": "Composite Entity Extractor",
+            "recognitionSources": [
+              "externalEntities"
+            ],
+            "startIndex": 17,
+            "text": "repent harelquin",
+            "type": "Address"
+          }
+        ]
+      },
+      "Address": [
+        {
+          "$instance": {
+            "number": [
+              {
+                "endIndex": 10,
+                "modelType": "Prebuilt Entity Extractor",
+                "recognitionSources": [
+                  "model"
+                ],
+                "startIndex": 8,
+                "text": "35",
+                "type": "builtin.number"
+              }
+            ],
+            "State": [
+              {
+                "endIndex": 13,
+                "modelType": "Entity Extractor",
+                "recognitionSources": [
+                  "model"
+                ],
+                "score": 0.614376,
+                "startIndex": 11,
+                "text": "WA",
+                "type": "State"
+              }
+            ]
+          },
+          "number": [
+            35
+          ],
+          "State": [
+            "WA"
+          ]
+        },
+        {
+          "number": [
+            3
+          ],
+          "State": [
+            "France"
+          ]
+        }
+      ]
+    },
+    "intents": {
+      "Cancel": {
+        "score": 0.00325984019
+      },
+      "Delivery": {
+        "score": 0.482009649
+      },
+      "EntityTests": {
+        "score": 0.00372873852
+      },
+      "Greeting": {
+        "score": 0.00283122621
+      },
+      "Help": {
+        "score": 0.00292110164
+      },
+      "None": {
+        "score": 0.0208108239
+      },
+      "Roles": {
+        "score": 0.069060266
+      },
+      "search": {
+        "score": 0.009682492
+      },
+      "SpecifyName": {
+        "score": 0.00586992875
+      },
+      "Travel": {
+        "score": 0.007831623
+      },
+      "Weather_GetForecast": {
+        "score": 0.009580207
+      }
+    },
+    "sentiment": {
+      "label": "neutral",
+      "score": 0.5
+    },
+    "text": "deliver 35 WA to repent harelquin",
+    "v3": {
+      "options": {
+        "externalEntities": [
+          {
+            "entityLength": 16,
+            "entityName": "Address",
+            "resolution": {
+              "number": [
+                3
+              ],
+              "State": [
+                "France"
+              ]
+            },
+            "startIndex": 17
+          }
+        ],
+        "includeAllIntents": true,
+        "includeAPIResults": true,
+        "includeInstanceData": true,
+        "log": true,
+        "preferExternalEntities": true,
+        "slot": "production"
+      },
+      "response": {
+        "prediction": {
+          "entities": {
+            "$instance": {
+              "Address": [
+                {
+                  "length": 5,
+                  "modelType": "Composite Entity Extractor",
+                  "modelTypeId": 4,
+                  "recognitionSources": [
+                    "model"
+                  ],
+                  "score": 0.7160641,
+                  "startIndex": 8,
+                  "text": "35 WA",
+                  "type": "Address"
+                },
+                {
+                  "length": 16,
+                  "modelType": "Composite Entity Extractor",
+                  "modelTypeId": 4,
+                  "recognitionSources": [
+                    "externalEntities"
+                  ],
+                  "startIndex": 17,
+                  "text": "repent harelquin",
+                  "type": "Address"
+                }
+              ]
+            },
+            "Address": [
+              {
+                "$instance": {
+                  "number": [
+                    {
+                      "length": 2,
+                      "modelType": "Prebuilt Entity Extractor",
+                      "modelTypeId": 2,
+                      "recognitionSources": [
+                        "model"
+                      ],
+                      "startIndex": 8,
+                      "text": "35",
+                      "type": "builtin.number"
+                    }
+                  ],
+                  "State": [
+                    {
+                      "length": 2,
+                      "modelType": "Entity Extractor",
+                      "modelTypeId": 1,
+                      "recognitionSources": [
+                        "model"
+                      ],
+                      "score": 0.614376,
+                      "startIndex": 11,
+                      "text": "WA",
+                      "type": "State"
+                    }
+                  ]
+                },
+                "number": [
+                  35
+                ],
+                "State": [
+                  "WA"
+                ]
+              },
+              {
+                "number": [
+                  3
+                ],
+                "State": [
+                  "France"
+                ]
+              }
+            ]
+          },
+          "intents": {
+            "Cancel": {
+              "score": 0.00325984019
+            },
+            "Delivery": {
+              "score": 0.482009649
+            },
+            "EntityTests": {
+              "score": 0.00372873852
+            },
+            "Greeting": {
+              "score": 0.00283122621
+            },
+            "Help": {
+              "score": 0.00292110164
+            },
+            "None": {
+              "score": 0.0208108239
+            },
+            "Roles": {
+              "score": 0.069060266
+            },
+            "search": {
+              "score": 0.009682492
+            },
+            "SpecifyName": {
+              "score": 0.00586992875
+            },
+            "Travel": {
+              "score": 0.007831623
+            },
+            "Weather.GetForecast": {
+              "score": 0.009580207
+            }
+          },
+          "normalizedQuery": "deliver 35 wa to repent harelquin",
+          "sentiment": {
+            "label": "neutral",
+            "score": 0.5
+          },
+          "topIntent": "Delivery"
+        },
+        "query": "deliver 35 WA to repent harelquin"
+      }
+    }
+  }
\ No newline at end of file
diff --git a/libraries/botbuilder-ai/tests/luis/test_data/ExternalEntitiesAndList_v3.json b/libraries/botbuilder-ai/tests/luis/test_data/ExternalEntitiesAndList_v3.json
new file mode 100644
index 000000000..e2cf8eb63
--- /dev/null
+++ b/libraries/botbuilder-ai/tests/luis/test_data/ExternalEntitiesAndList_v3.json
@@ -0,0 +1,178 @@
+{
+    "entities": {
+      "$instance": {
+        "Airline": [
+          {
+            "endIndex": 23,
+            "modelType": "List Entity Extractor",
+            "recognitionSources": [
+              "externalEntities"
+            ],
+            "startIndex": 7,
+            "text": "humberg airlines",
+            "type": "Airline"
+          },
+          {
+            "endIndex": 32,
+            "modelType": "List Entity Extractor",
+            "recognitionSources": [
+              "model"
+            ],
+            "startIndex": 27,
+            "text": "Delta",
+            "type": "Airline"
+          }
+        ]
+      },
+      "Airline": [
+        [
+          "HumAir"
+        ],
+        [
+          "Delta"
+        ]
+      ]
+    },
+    "intents": {
+      "Cancel": {
+        "score": 0.00330878259
+      },
+      "Delivery": {
+        "score": 0.00452178251
+      },
+      "EntityTests": {
+        "score": 0.052175343
+      },
+      "Greeting": {
+        "score": 0.002769983
+      },
+      "Help": {
+        "score": 0.002995687
+      },
+      "None": {
+        "score": 0.0302589461
+      },
+      "Roles": {
+        "score": 0.132316783
+      },
+      "search": {
+        "score": 0.007362695
+      },
+      "SpecifyName": {
+        "score": 0.00500302855
+      },
+      "Travel": {
+        "score": 0.0146034053
+      },
+      "Weather_GetForecast": {
+        "score": 0.005048246
+      }
+    },
+    "sentiment": {
+      "label": "neutral",
+      "score": 0.5
+    },
+    "text": "fly on humberg airlines or Delta",
+    "v3": {
+      "options": {
+        "externalEntities": [
+          {
+            "entityLength": 16,
+            "entityName": "Airline",
+            "resolution": [
+              "HumAir"
+            ],
+            "startIndex": 7
+          }
+        ],
+        "includeAllIntents": true,
+        "includeAPIResults": true,
+        "includeInstanceData": true,
+        "log": true,
+        "preferExternalEntities": true,
+        "slot": "production"
+      },
+      "response": {
+        "prediction": {
+          "entities": {
+            "$instance": {
+              "Airline": [
+                {
+                  "length": 16,
+                  "modelType": "List Entity Extractor",
+                  "modelTypeId": 5,
+                  "recognitionSources": [
+                    "externalEntities"
+                  ],
+                  "startIndex": 7,
+                  "text": "humberg airlines",
+                  "type": "Airline"
+                },
+                {
+                  "length": 5,
+                  "modelType": "List Entity Extractor",
+                  "modelTypeId": 5,
+                  "recognitionSources": [
+                    "model"
+                  ],
+                  "startIndex": 27,
+                  "text": "Delta",
+                  "type": "Airline"
+                }
+              ]
+            },
+            "Airline": [
+              [
+                "HumAir"
+              ],
+              [
+                "Delta"
+              ]
+            ]
+          },
+          "intents": {
+            "Cancel": {
+              "score": 0.00330878259
+            },
+            "Delivery": {
+              "score": 0.00452178251
+            },
+            "EntityTests": {
+              "score": 0.052175343
+            },
+            "Greeting": {
+              "score": 0.002769983
+            },
+            "Help": {
+              "score": 0.002995687
+            },
+            "None": {
+              "score": 0.0302589461
+            },
+            "Roles": {
+              "score": 0.132316783
+            },
+            "search": {
+              "score": 0.007362695
+            },
+            "SpecifyName": {
+              "score": 0.00500302855
+            },
+            "Travel": {
+              "score": 0.0146034053
+            },
+            "Weather.GetForecast": {
+              "score": 0.005048246
+            }
+          },
+          "normalizedQuery": "fly on humberg airlines or delta",
+          "sentiment": {
+            "label": "neutral",
+            "score": 0.5
+          },
+          "topIntent": "Roles"
+        },
+        "query": "fly on humberg airlines or Delta"
+      }
+    }
+  }
\ No newline at end of file
diff --git a/libraries/botbuilder-ai/tests/luis/test_data/ExternalEntitiesAndRegex_v3.json b/libraries/botbuilder-ai/tests/luis/test_data/ExternalEntitiesAndRegex_v3.json
new file mode 100644
index 000000000..fa8566eb3
--- /dev/null
+++ b/libraries/botbuilder-ai/tests/luis/test_data/ExternalEntitiesAndRegex_v3.json
@@ -0,0 +1,167 @@
+{
+    "entities": {
+      "$instance": {
+        "Part": [
+          {
+            "endIndex": 5,
+            "modelType": "Regex Entity Extractor",
+            "recognitionSources": [
+              "externalEntities"
+            ],
+            "startIndex": 0,
+            "text": "42ski",
+            "type": "Part"
+          },
+          {
+            "endIndex": 26,
+            "modelType": "Regex Entity Extractor",
+            "recognitionSources": [
+              "model"
+            ],
+            "startIndex": 21,
+            "text": "kb423",
+            "type": "Part"
+          }
+        ]
+      },
+      "Part": [
+        "42ski",
+        "kb423"
+      ]
+    },
+    "intents": {
+      "Cancel": {
+        "score": 0.0127721056
+      },
+      "Delivery": {
+        "score": 0.004578639
+      },
+      "EntityTests": {
+        "score": 0.008811761
+      },
+      "Greeting": {
+        "score": 0.00256775436
+      },
+      "Help": {
+        "score": 0.00214677141
+      },
+      "None": {
+        "score": 0.27875194
+      },
+      "Roles": {
+        "score": 0.0273685548
+      },
+      "search": {
+        "score": 0.0084077
+      },
+      "SpecifyName": {
+        "score": 0.0148377549
+      },
+      "Travel": {
+        "score": 0.0039825947
+      },
+      "Weather_GetForecast": {
+        "score": 0.009611839
+      }
+    },
+    "sentiment": {
+      "label": "neutral",
+      "score": 0.5
+    },
+    "text": "42ski is a part like kb423",
+    "v3": {
+      "options": {
+        "externalEntities": [
+          {
+            "entityLength": 5,
+            "entityName": "Part",
+            "startIndex": 0
+          }
+        ],
+        "includeAllIntents": true,
+        "includeAPIResults": true,
+        "includeInstanceData": true,
+        "log": true,
+        "preferExternalEntities": true,
+        "slot": "production"
+      },
+      "response": {
+        "prediction": {
+          "entities": {
+            "$instance": {
+              "Part": [
+                {
+                  "length": 5,
+                  "modelType": "Regex Entity Extractor",
+                  "modelTypeId": 8,
+                  "recognitionSources": [
+                    "externalEntities"
+                  ],
+                  "startIndex": 0,
+                  "text": "42ski",
+                  "type": "Part"
+                },
+                {
+                  "length": 5,
+                  "modelType": "Regex Entity Extractor",
+                  "modelTypeId": 8,
+                  "recognitionSources": [
+                    "model"
+                  ],
+                  "startIndex": 21,
+                  "text": "kb423",
+                  "type": "Part"
+                }
+              ]
+            },
+            "Part": [
+              "42ski",
+              "kb423"
+            ]
+          },
+          "intents": {
+            "Cancel": {
+              "score": 0.0127721056
+            },
+            "Delivery": {
+              "score": 0.004578639
+            },
+            "EntityTests": {
+              "score": 0.008811761
+            },
+            "Greeting": {
+              "score": 0.00256775436
+            },
+            "Help": {
+              "score": 0.00214677141
+            },
+            "None": {
+              "score": 0.27875194
+            },
+            "Roles": {
+              "score": 0.0273685548
+            },
+            "search": {
+              "score": 0.0084077
+            },
+            "SpecifyName": {
+              "score": 0.0148377549
+            },
+            "Travel": {
+              "score": 0.0039825947
+            },
+            "Weather.GetForecast": {
+              "score": 0.009611839
+            }
+          },
+          "normalizedQuery": "42ski is a part like kb423",
+          "sentiment": {
+            "label": "neutral",
+            "score": 0.5
+          },
+          "topIntent": "None"
+        },
+        "query": "42ski is a part like kb423"
+      }
+    }
+  }
\ No newline at end of file
diff --git a/libraries/botbuilder-ai/tests/luis/test_data/ExternalEntitiesAndSimpleOverride_v3.json b/libraries/botbuilder-ai/tests/luis/test_data/ExternalEntitiesAndSimpleOverride_v3.json
new file mode 100644
index 000000000..8f48817dd
--- /dev/null
+++ b/libraries/botbuilder-ai/tests/luis/test_data/ExternalEntitiesAndSimpleOverride_v3.json
@@ -0,0 +1,299 @@
+{
+    "entities": {
+      "$instance": {
+        "Address": [
+          {
+            "endIndex": 13,
+            "modelType": "Composite Entity Extractor",
+            "recognitionSources": [
+              "model"
+            ],
+            "score": 0.7033113,
+            "startIndex": 8,
+            "text": "37 wa",
+            "type": "Address"
+          }
+        ],
+        "number": [
+          {
+            "endIndex": 19,
+            "modelType": "Prebuilt Entity Extractor",
+            "recognitionSources": [
+              "model"
+            ],
+            "startIndex": 17,
+            "text": "82",
+            "type": "builtin.number"
+          }
+        ],
+        "State": [
+          {
+            "endIndex": 22,
+            "modelType": "Entity Extractor",
+            "recognitionSources": [
+              "externalEntities"
+            ],
+            "startIndex": 20,
+            "text": "co",
+            "type": "State"
+          }
+        ]
+      },
+      "Address": [
+        {
+          "$instance": {
+            "number": [
+              {
+                "endIndex": 10,
+                "modelType": "Prebuilt Entity Extractor",
+                "recognitionSources": [
+                  "model"
+                ],
+                "startIndex": 8,
+                "text": "37",
+                "type": "builtin.number"
+              }
+            ],
+            "State": [
+              {
+                "endIndex": 13,
+                "modelType": "Entity Extractor",
+                "recognitionSources": [
+                  "model",
+                  "externalEntities"
+                ],
+                "score": 0.5987082,
+                "startIndex": 11,
+                "text": "wa",
+                "type": "State"
+              }
+            ]
+          },
+          "number": [
+            37
+          ],
+          "State": [
+            {
+              "state": "Washington"
+            }
+          ]
+        }
+      ],
+      "number": [
+        82
+      ],
+      "State": [
+        {
+          "state": "Colorado"
+        }
+      ]
+    },
+    "intents": {
+      "Cancel": {
+        "score": 0.004045653
+      },
+      "Delivery": {
+        "score": 0.511144161
+      },
+      "EntityTests": {
+        "score": 0.004197402
+      },
+      "Greeting": {
+        "score": 0.00286332145
+      },
+      "Help": {
+        "score": 0.00351834856
+      },
+      "None": {
+        "score": 0.01229356
+      },
+      "Roles": {
+        "score": 0.08465987
+      },
+      "search": {
+        "score": 0.009909824
+      },
+      "SpecifyName": {
+        "score": 0.006426142
+      },
+      "Travel": {
+        "score": 0.008369388
+      },
+      "Weather_GetForecast": {
+        "score": 0.0112502193
+      }
+    },
+    "sentiment": {
+      "label": "neutral",
+      "score": 0.5
+    },
+    "text": "deliver 37 wa to 82 co",
+    "v3": {
+      "options": {
+        "externalEntities": [
+          {
+            "entityLength": 2,
+            "entityName": "State",
+            "resolution": {
+              "state": "Washington"
+            },
+            "startIndex": 11
+          },
+          {
+            "entityLength": 2,
+            "entityName": "State",
+            "resolution": {
+              "state": "Colorado"
+            },
+            "startIndex": 20
+          }
+        ],
+        "includeAllIntents": true,
+        "includeAPIResults": true,
+        "includeInstanceData": true,
+        "log": true,
+        "preferExternalEntities": true,
+        "slot": "production"
+      },
+      "response": {
+        "prediction": {
+          "entities": {
+            "$instance": {
+              "Address": [
+                {
+                  "length": 5,
+                  "modelType": "Composite Entity Extractor",
+                  "modelTypeId": 4,
+                  "recognitionSources": [
+                    "model"
+                  ],
+                  "score": 0.7033113,
+                  "startIndex": 8,
+                  "text": "37 wa",
+                  "type": "Address"
+                }
+              ],
+              "number": [
+                {
+                  "length": 2,
+                  "modelType": "Prebuilt Entity Extractor",
+                  "modelTypeId": 2,
+                  "recognitionSources": [
+                    "model"
+                  ],
+                  "startIndex": 17,
+                  "text": "82",
+                  "type": "builtin.number"
+                }
+              ],
+              "State": [
+                {
+                  "length": 2,
+                  "modelType": "Entity Extractor",
+                  "modelTypeId": 1,
+                  "recognitionSources": [
+                    "externalEntities"
+                  ],
+                  "startIndex": 20,
+                  "text": "co",
+                  "type": "State"
+                }
+              ]
+            },
+            "Address": [
+              {
+                "$instance": {
+                  "number": [
+                    {
+                      "length": 2,
+                      "modelType": "Prebuilt Entity Extractor",
+                      "modelTypeId": 2,
+                      "recognitionSources": [
+                        "model"
+                      ],
+                      "startIndex": 8,
+                      "text": "37",
+                      "type": "builtin.number"
+                    }
+                  ],
+                  "State": [
+                    {
+                      "length": 2,
+                      "modelType": "Entity Extractor",
+                      "modelTypeId": 1,
+                      "recognitionSources": [
+                        "model",
+                        "externalEntities"
+                      ],
+                      "score": 0.5987082,
+                      "startIndex": 11,
+                      "text": "wa",
+                      "type": "State"
+                    }
+                  ]
+                },
+                "number": [
+                  37
+                ],
+                "State": [
+                  {
+                    "state": "Washington"
+                  }
+                ]
+              }
+            ],
+            "number": [
+              82
+            ],
+            "State": [
+              {
+                "state": "Colorado"
+              }
+            ]
+          },
+          "intents": {
+            "Cancel": {
+              "score": 0.004045653
+            },
+            "Delivery": {
+              "score": 0.511144161
+            },
+            "EntityTests": {
+              "score": 0.004197402
+            },
+            "Greeting": {
+              "score": 0.00286332145
+            },
+            "Help": {
+              "score": 0.00351834856
+            },
+            "None": {
+              "score": 0.01229356
+            },
+            "Roles": {
+              "score": 0.08465987
+            },
+            "search": {
+              "score": 0.009909824
+            },
+            "SpecifyName": {
+              "score": 0.006426142
+            },
+            "Travel": {
+              "score": 0.008369388
+            },
+            "Weather.GetForecast": {
+              "score": 0.0112502193
+            }
+          },
+          "normalizedQuery": "deliver 37 wa to 82 co",
+          "sentiment": {
+            "label": "neutral",
+            "score": 0.5
+          },
+          "topIntent": "Delivery"
+        },
+        "query": "deliver 37 wa to 82 co"
+      }
+    }
+  }
\ No newline at end of file
diff --git a/libraries/botbuilder-ai/tests/luis/test_data/ExternalEntitiesAndSimple_v3.json b/libraries/botbuilder-ai/tests/luis/test_data/ExternalEntitiesAndSimple_v3.json
new file mode 100644
index 000000000..e7073627d
--- /dev/null
+++ b/libraries/botbuilder-ai/tests/luis/test_data/ExternalEntitiesAndSimple_v3.json
@@ -0,0 +1,292 @@
+{
+    "entities": {
+      "$instance": {
+        "Address": [
+          {
+            "endIndex": 13,
+            "modelType": "Composite Entity Extractor",
+            "recognitionSources": [
+              "model"
+            ],
+            "score": 0.7033113,
+            "startIndex": 8,
+            "text": "37 wa",
+            "type": "Address"
+          }
+        ],
+        "number": [
+          {
+            "endIndex": 19,
+            "modelType": "Prebuilt Entity Extractor",
+            "recognitionSources": [
+              "model"
+            ],
+            "startIndex": 17,
+            "text": "82",
+            "type": "builtin.number"
+          }
+        ],
+        "State": [
+          {
+            "endIndex": 22,
+            "modelType": "Entity Extractor",
+            "recognitionSources": [
+              "externalEntities"
+            ],
+            "startIndex": 20,
+            "text": "co",
+            "type": "State"
+          }
+        ]
+      },
+      "Address": [
+        {
+          "$instance": {
+            "number": [
+              {
+                "endIndex": 10,
+                "modelType": "Prebuilt Entity Extractor",
+                "recognitionSources": [
+                  "model"
+                ],
+                "startIndex": 8,
+                "text": "37",
+                "type": "builtin.number"
+              }
+            ],
+            "State": [
+              {
+                "endIndex": 13,
+                "modelType": "Entity Extractor",
+                "recognitionSources": [
+                  "model",
+                  "externalEntities"
+                ],
+                "score": 0.5987082,
+                "startIndex": 11,
+                "text": "wa",
+                "type": "State"
+              }
+            ]
+          },
+          "number": [
+            37
+          ],
+          "State": [
+            "wa"
+          ]
+        }
+      ],
+      "number": [
+        82
+      ],
+      "State": [
+        {
+          "state": "Colorado"
+        }
+      ]
+    },
+    "intents": {
+      "Cancel": {
+        "score": 0.004045653
+      },
+      "Delivery": {
+        "score": 0.511144161
+      },
+      "EntityTests": {
+        "score": 0.004197402
+      },
+      "Greeting": {
+        "score": 0.00286332145
+      },
+      "Help": {
+        "score": 0.00351834856
+      },
+      "None": {
+        "score": 0.01229356
+      },
+      "Roles": {
+        "score": 0.08465987
+      },
+      "search": {
+        "score": 0.009909824
+      },
+      "SpecifyName": {
+        "score": 0.006426142
+      },
+      "Travel": {
+        "score": 0.008369388
+      },
+      "Weather_GetForecast": {
+        "score": 0.0112502193
+      }
+    },
+    "sentiment": {
+      "label": "neutral",
+      "score": 0.5
+    },
+    "text": "deliver 37 wa to 82 co",
+    "v3": {
+      "options": {
+        "externalEntities": [
+          {
+            "entityLength": 2,
+            "entityName": "State",
+            "startIndex": 11
+          },
+          {
+            "entityLength": 2,
+            "entityName": "State",
+            "resolution": {
+              "state": "Colorado"
+            },
+            "startIndex": 20
+          }
+        ],
+        "includeAllIntents": true,
+        "includeAPIResults": true,
+        "includeInstanceData": true,
+        "log": true,
+        "preferExternalEntities": true,
+        "slot": "production"
+      },
+      "response": {
+        "prediction": {
+          "entities": {
+            "$instance": {
+              "Address": [
+                {
+                  "length": 5,
+                  "modelType": "Composite Entity Extractor",
+                  "modelTypeId": 4,
+                  "recognitionSources": [
+                    "model"
+                  ],
+                  "score": 0.7033113,
+                  "startIndex": 8,
+                  "text": "37 wa",
+                  "type": "Address"
+                }
+              ],
+              "number": [
+                {
+                  "length": 2,
+                  "modelType": "Prebuilt Entity Extractor",
+                  "modelTypeId": 2,
+                  "recognitionSources": [
+                    "model"
+                  ],
+                  "startIndex": 17,
+                  "text": "82",
+                  "type": "builtin.number"
+                }
+              ],
+              "State": [
+                {
+                  "length": 2,
+                  "modelType": "Entity Extractor",
+                  "modelTypeId": 1,
+                  "recognitionSources": [
+                    "externalEntities"
+                  ],
+                  "startIndex": 20,
+                  "text": "co",
+                  "type": "State"
+                }
+              ]
+            },
+            "Address": [
+              {
+                "$instance": {
+                  "number": [
+                    {
+                      "length": 2,
+                      "modelType": "Prebuilt Entity Extractor",
+                      "modelTypeId": 2,
+                      "recognitionSources": [
+                        "model"
+                      ],
+                      "startIndex": 8,
+                      "text": "37",
+                      "type": "builtin.number"
+                    }
+                  ],
+                  "State": [
+                    {
+                      "length": 2,
+                      "modelType": "Entity Extractor",
+                      "modelTypeId": 1,
+                      "recognitionSources": [
+                        "model",
+                        "externalEntities"
+                      ],
+                      "score": 0.5987082,
+                      "startIndex": 11,
+                      "text": "wa",
+                      "type": "State"
+                    }
+                  ]
+                },
+                "number": [
+                  37
+                ],
+                "State": [
+                  "wa"
+                ]
+              }
+            ],
+            "number": [
+              82
+            ],
+            "State": [
+              {
+                "state": "Colorado"
+              }
+            ]
+          },
+          "intents": {
+            "Cancel": {
+              "score": 0.004045653
+            },
+            "Delivery": {
+              "score": 0.511144161
+            },
+            "EntityTests": {
+              "score": 0.004197402
+            },
+            "Greeting": {
+              "score": 0.00286332145
+            },
+            "Help": {
+              "score": 0.00351834856
+            },
+            "None": {
+              "score": 0.01229356
+            },
+            "Roles": {
+              "score": 0.08465987
+            },
+            "search": {
+              "score": 0.009909824
+            },
+            "SpecifyName": {
+              "score": 0.006426142
+            },
+            "Travel": {
+              "score": 0.008369388
+            },
+            "Weather.GetForecast": {
+              "score": 0.0112502193
+            }
+          },
+          "normalizedQuery": "deliver 37 wa to 82 co",
+          "sentiment": {
+            "label": "neutral",
+            "score": 0.5
+          },
+          "topIntent": "Delivery"
+        },
+        "query": "deliver 37 wa to 82 co"
+      }
+    }
+  }
\ No newline at end of file
diff --git a/libraries/botbuilder-ai/tests/luis/test_data/GeoPeopleOrdinal_v3.json b/libraries/botbuilder-ai/tests/luis/test_data/GeoPeopleOrdinal_v3.json
new file mode 100644
index 000000000..4ac3ed4ff
--- /dev/null
+++ b/libraries/botbuilder-ai/tests/luis/test_data/GeoPeopleOrdinal_v3.json
@@ -0,0 +1,321 @@
+{
+    "entities": {
+      "$instance": {
+        "child": [
+          {
+            "endIndex": 99,
+            "modelType": "Prebuilt Entity Extractor",
+            "recognitionSources": [
+              "model"
+            ],
+            "startIndex": 87,
+            "text": "lisa simpson",
+            "type": "builtin.personName"
+          }
+        ],
+        "endloc": [
+          {
+            "endIndex": 51,
+            "modelType": "Prebuilt Entity Extractor",
+            "recognitionSources": [
+              "model"
+            ],
+            "startIndex": 44,
+            "text": "jakarta",
+            "type": "builtin.geographyV2.city"
+          }
+        ],
+        "ordinalV2": [
+          {
+            "endIndex": 28,
+            "modelType": "Prebuilt Entity Extractor",
+            "recognitionSources": [
+              "model"
+            ],
+            "startIndex": 24,
+            "text": "last",
+            "type": "builtin.ordinalV2.relative"
+          }
+        ],
+        "parent": [
+          {
+            "endIndex": 69,
+            "modelType": "Prebuilt Entity Extractor",
+            "recognitionSources": [
+              "model"
+            ],
+            "startIndex": 56,
+            "text": "homer simpson",
+            "type": "builtin.personName"
+          }
+        ],
+        "startloc": [
+          {
+            "endIndex": 40,
+            "modelType": "Prebuilt Entity Extractor",
+            "recognitionSources": [
+              "model"
+            ],
+            "startIndex": 34,
+            "text": "london",
+            "type": "builtin.geographyV2.city"
+          }
+        ],
+        "startpos": [
+          {
+            "endIndex": 20,
+            "modelType": "Prebuilt Entity Extractor",
+            "recognitionSources": [
+              "model"
+            ],
+            "startIndex": 8,
+            "text": "next to last",
+            "type": "builtin.ordinalV2.relative"
+          }
+        ]
+      },
+      "child": [
+        "lisa simpson"
+      ],
+      "endloc": [
+        {
+          "location": "jakarta",
+          "type": "city"
+        }
+      ],
+      "ordinalV2": [
+        {
+          "offset": 0,
+          "relativeTo": "end"
+        }
+      ],
+      "parent": [
+        "homer simpson"
+      ],
+      "startloc": [
+        {
+          "location": "london",
+          "type": "city"
+        }
+      ],
+      "startpos": [
+        {
+          "offset": -1,
+          "relativeTo": "end"
+        }
+      ]
+    },
+    "intents": {
+      "Cancel": {
+        "score": 0.000107549029
+      },
+      "Delivery": {
+        "score": 0.00123035291
+      },
+      "EntityTests": {
+        "score": 0.0009487789
+      },
+      "Greeting": {
+        "score": 5.293933E-05
+      },
+      "Help": {
+        "score": 0.0001358991
+      },
+      "None": {
+        "score": 0.0109820236
+      },
+      "Roles": {
+        "score": 0.999204934
+      },
+      "search": {
+        "score": 0.0263254233
+      },
+      "SpecifyName": {
+        "score": 0.00104324089
+      },
+      "Travel": {
+        "score": 0.01043327
+      },
+      "Weather_GetForecast": {
+        "score": 0.0106523167
+      }
+    },
+    "sentiment": {
+      "label": "neutral",
+      "score": 0.5
+    },
+    "text": "go from next to last to last move london to jakarta and homer simpson is the parent of lisa simpson",
+    "v3": {
+      "options": {
+        "includeAllIntents": true,
+        "includeAPIResults": true,
+        "includeInstanceData": true,
+        "log": true,
+        "preferExternalEntities": true,
+        "slot": "production"
+      },
+      "response": {
+        "prediction": {
+          "entities": {
+            "$instance": {
+              "child": [
+                {
+                  "length": 12,
+                  "modelType": "Prebuilt Entity Extractor",
+                  "modelTypeId": 2,
+                  "recognitionSources": [
+                    "model"
+                  ],
+                  "role": "child",
+                  "startIndex": 87,
+                  "text": "lisa simpson",
+                  "type": "builtin.personName"
+                }
+              ],
+              "endloc": [
+                {
+                  "length": 7,
+                  "modelType": "Prebuilt Entity Extractor",
+                  "modelTypeId": 2,
+                  "recognitionSources": [
+                    "model"
+                  ],
+                  "role": "endloc",
+                  "startIndex": 44,
+                  "text": "jakarta",
+                  "type": "builtin.geographyV2.city"
+                }
+              ],
+              "ordinalV2": [
+                {
+                  "length": 4,
+                  "modelType": "Prebuilt Entity Extractor",
+                  "modelTypeId": 2,
+                  "recognitionSources": [
+                    "model"
+                  ],
+                  "startIndex": 24,
+                  "text": "last",
+                  "type": "builtin.ordinalV2.relative"
+                }
+              ],
+              "parent": [
+                {
+                  "length": 13,
+                  "modelType": "Prebuilt Entity Extractor",
+                  "modelTypeId": 2,
+                  "recognitionSources": [
+                    "model"
+                  ],
+                  "role": "parent",
+                  "startIndex": 56,
+                  "text": "homer simpson",
+                  "type": "builtin.personName"
+                }
+              ],
+              "startloc": [
+                {
+                  "length": 6,
+                  "modelType": "Prebuilt Entity Extractor",
+                  "modelTypeId": 2,
+                  "recognitionSources": [
+                    "model"
+                  ],
+                  "role": "startloc",
+                  "startIndex": 34,
+                  "text": "london",
+                  "type": "builtin.geographyV2.city"
+                }
+              ],
+              "startpos": [
+                {
+                  "length": 12,
+                  "modelType": "Prebuilt Entity Extractor",
+                  "modelTypeId": 2,
+                  "recognitionSources": [
+                    "model"
+                  ],
+                  "role": "startpos",
+                  "startIndex": 8,
+                  "text": "next to last",
+                  "type": "builtin.ordinalV2.relative"
+                }
+              ]
+            },
+            "child": [
+              "lisa simpson"
+            ],
+            "endloc": [
+                {
+                    "value": "jakarta",
+                    "type": "city"
+                }
+            ],
+            "ordinalV2": [
+              {
+                "offset": 0,
+                "relativeTo": "end"
+              }
+            ],
+            "parent": [
+              "homer simpson"
+            ],
+            "startloc": [
+                {
+                    "value": "london",
+                    "type": "city"
+                }
+            ],
+            "startpos": [
+              {
+                "offset": -1,
+                "relativeTo": "end"
+              }
+            ]
+          },
+          "intents": {
+            "Cancel": {
+              "score": 0.000107549029
+            },
+            "Delivery": {
+              "score": 0.00123035291
+            },
+            "EntityTests": {
+              "score": 0.0009487789
+            },
+            "Greeting": {
+              "score": 5.293933E-05
+            },
+            "Help": {
+              "score": 0.0001358991
+            },
+            "None": {
+              "score": 0.0109820236
+            },
+            "Roles": {
+              "score": 0.999204934
+            },
+            "search": {
+              "score": 0.0263254233
+            },
+            "SpecifyName": {
+              "score": 0.00104324089
+            },
+            "Travel": {
+              "score": 0.01043327
+            },
+            "Weather.GetForecast": {
+              "score": 0.0106523167
+            }
+          },
+          "normalizedQuery": "go from next to last to last move london to jakarta and homer simpson is the parent of lisa simpson",
+          "sentiment": {
+            "label": "neutral",
+            "score": 0.5
+          },
+          "topIntent": "Roles"
+        },
+        "query": "go from next to last to last move london to jakarta and homer simpson is the parent of lisa simpson"
+      }
+    }
+  }
\ No newline at end of file
diff --git a/libraries/botbuilder-ai/tests/luis/test_data/Minimal_v3.json b/libraries/botbuilder-ai/tests/luis/test_data/Minimal_v3.json
new file mode 100644
index 000000000..b810446ad
--- /dev/null
+++ b/libraries/botbuilder-ai/tests/luis/test_data/Minimal_v3.json
@@ -0,0 +1,83 @@
+{
+    "entities": {
+      "Airline": [
+        [
+          "Delta"
+        ]
+      ],
+      "datetime": [
+        {
+          "timex": [
+            "T15"
+          ],
+          "type": "time"
+        }
+      ],
+      "dimension": [
+        {
+          "number": 3,
+          "units": "Picometer"
+        }
+      ]
+    },
+    "intents": {
+      "Roles": {
+        "score": 0.446264923
+      }
+    },
+    "sentiment": {
+      "label": "neutral",
+      "score": 0.5
+    },
+    "text": "fly on delta at 3pm",
+    "v3": {
+      "options": {
+        "includeAllIntents": false,
+        "includeAPIResults": true,
+        "includeInstanceData": false,
+        "log": true,
+        "preferExternalEntities": true,
+        "slot": "production"
+      },
+      "response": {
+        "prediction": {
+          "entities": {
+            "Airline": [
+              [
+                "Delta"
+              ]
+            ],
+            "datetimeV2": [
+              {
+                "type": "time",
+                "values": [
+                  {
+                    "timex": "T15",
+                    "value": "15:00:00"
+                  }
+                ]
+              }
+            ],
+            "dimension": [
+              {
+                "number": 3,
+                "unit": "Picometer"
+              }
+            ]
+          },
+          "intents": {
+            "Roles": {
+              "score": 0.446264923
+            }
+          },
+          "normalizedQuery": "fly on delta at 3pm",
+          "sentiment": {
+            "label": "neutral",
+            "score": 0.5
+          },
+          "topIntent": "Roles"
+        },
+        "query": "fly on delta at 3pm"
+      }
+    }
+  }
\ No newline at end of file
diff --git a/libraries/botbuilder-ai/tests/luis/test_data/NoEntitiesInstanceTrue_v3.json b/libraries/botbuilder-ai/tests/luis/test_data/NoEntitiesInstanceTrue_v3.json
new file mode 100644
index 000000000..10a268338
--- /dev/null
+++ b/libraries/botbuilder-ai/tests/luis/test_data/NoEntitiesInstanceTrue_v3.json
@@ -0,0 +1,33 @@
+{
+    "entities": {
+      "$instance": {}
+    },
+    "intents": {
+      "Greeting": {
+        "score": 0.9589885
+      }
+    },
+    "text": "Hi",
+    "v3": {
+      "options": {
+        "includeAllIntents": false,
+        "includeAPIResults": true,
+        "includeInstanceData": true,
+        "log": true,
+        "preferExternalEntities": true,
+        "slot": "production"
+      },
+      "response": {
+        "query": "Hi",
+        "prediction": {
+          "topIntent": "Greeting",
+          "intents": {
+            "Greeting": {
+              "score": 0.9589885
+            }
+          },
+          "entities": {}
+        }
+      }
+    }
+  }
diff --git a/libraries/botbuilder-ai/tests/luis/test_data/Patterns_v3.json b/libraries/botbuilder-ai/tests/luis/test_data/Patterns_v3.json
new file mode 100644
index 000000000..824bf5f54
--- /dev/null
+++ b/libraries/botbuilder-ai/tests/luis/test_data/Patterns_v3.json
@@ -0,0 +1,262 @@
+{
+    "entities": {
+      "$instance": {
+        "extra": [
+          {
+            "endIndex": 76,
+            "modelType": "Pattern.Any Entity Extractor",
+            "recognitionSources": [
+              "model"
+            ],
+            "startIndex": 71,
+            "text": "kb435",
+            "type": "subject"
+          }
+        ],
+        "Part": [
+          {
+            "endIndex": 76,
+            "modelType": "Regex Entity Extractor",
+            "recognitionSources": [
+              "model"
+            ],
+            "startIndex": 71,
+            "text": "kb435",
+            "type": "Part"
+          }
+        ],
+        "person": [
+          {
+            "endIndex": 61,
+            "modelType": "Pattern.Any Entity Extractor",
+            "recognitionSources": [
+              "model"
+            ],
+            "startIndex": 49,
+            "text": "bart simpson",
+            "type": "person"
+          }
+        ],
+        "personName": [
+          {
+            "endIndex": 61,
+            "modelType": "Prebuilt Entity Extractor",
+            "recognitionSources": [
+              "model"
+            ],
+            "startIndex": 49,
+            "text": "bart simpson",
+            "type": "builtin.personName"
+          }
+        ],
+        "subject": [
+          {
+            "endIndex": 43,
+            "modelType": "Pattern.Any Entity Extractor",
+            "recognitionSources": [
+              "model"
+            ],
+            "startIndex": 12,
+            "text": "something wicked this way comes",
+            "type": "subject"
+          }
+        ]
+      },
+      "extra": [
+        "kb435"
+      ],
+      "Part": [
+        "kb435"
+      ],
+      "person": [
+        "bart simpson"
+      ],
+      "personName": [
+        "bart simpson"
+      ],
+      "subject": [
+        "something wicked this way comes"
+      ]
+    },
+    "intents": {
+      "Cancel": {
+        "score": 1.01764708E-09
+      },
+      "Delivery": {
+        "score": 1.8E-09
+      },
+      "EntityTests": {
+        "score": 1.044335E-05
+      },
+      "Greeting": {
+        "score": 1.0875E-09
+      },
+      "Help": {
+        "score": 1.01764708E-09
+      },
+      "None": {
+        "score": 2.38094663E-06
+      },
+      "Roles": {
+        "score": 5.98274755E-06
+      },
+      "search": {
+        "score": 0.9999993
+      },
+      "SpecifyName": {
+        "score": 3.0666667E-09
+      },
+      "Travel": {
+        "score": 3.09763345E-06
+      },
+      "Weather_GetForecast": {
+        "score": 1.02792524E-06
+      }
+    },
+    "sentiment": {
+      "label": "negative",
+      "score": 0.210341513
+    },
+    "text": "email about something wicked this way comes from bart simpson and also kb435",
+    "v3": {
+      "options": {
+        "includeAllIntents": true,
+        "includeAPIResults": true,
+        "includeInstanceData": true,
+        "log": true,
+        "preferExternalEntities": true,
+        "slot": "production"
+      },
+      "response": {
+        "prediction": {
+          "entities": {
+            "$instance": {
+              "extra": [
+                {
+                  "length": 5,
+                  "modelType": "Pattern.Any Entity Extractor",
+                  "modelTypeId": 7,
+                  "recognitionSources": [
+                    "model"
+                  ],
+                  "role": "extra",
+                  "startIndex": 71,
+                  "text": "kb435",
+                  "type": "subject"
+                }
+              ],
+              "Part": [
+                {
+                  "length": 5,
+                  "modelType": "Regex Entity Extractor",
+                  "modelTypeId": 8,
+                  "recognitionSources": [
+                    "model"
+                  ],
+                  "startIndex": 71,
+                  "text": "kb435",
+                  "type": "Part"
+                }
+              ],
+              "person": [
+                {
+                  "length": 12,
+                  "modelType": "Pattern.Any Entity Extractor",
+                  "modelTypeId": 7,
+                  "recognitionSources": [
+                    "model"
+                  ],
+                  "startIndex": 49,
+                  "text": "bart simpson",
+                  "type": "person"
+                }
+              ],
+              "personName": [
+                {
+                  "length": 12,
+                  "modelType": "Prebuilt Entity Extractor",
+                  "modelTypeId": 2,
+                  "recognitionSources": [
+                    "model"
+                  ],
+                  "startIndex": 49,
+                  "text": "bart simpson",
+                  "type": "builtin.personName"
+                }
+              ],
+              "subject": [
+                {
+                  "length": 31,
+                  "modelType": "Pattern.Any Entity Extractor",
+                  "modelTypeId": 7,
+                  "recognitionSources": [
+                    "model"
+                  ],
+                  "startIndex": 12,
+                  "text": "something wicked this way comes",
+                  "type": "subject"
+                }
+              ]
+            },
+            "extra": [
+              "kb435"
+            ],
+            "Part": [
+              "kb435"
+            ],
+            "person": [
+              "bart simpson"
+            ],
+            "personName": [
+              "bart simpson"
+            ],
+            "subject": [
+              "something wicked this way comes"
+            ]
+          },
+          "intents": {
+            "Cancel": {
+              "score": 1.01764708E-09
+            },
+            "Delivery": {
+              "score": 1.8E-09
+            },
+            "EntityTests": {
+              "score": 1.044335E-05
+            },
+            "Greeting": {
+              "score": 1.0875E-09
+            },
+            "Help": {
+              "score": 1.01764708E-09
+            },
+            "None": {
+              "score": 2.38094663E-06
+            },
+            "Roles": {
+              "score": 5.98274755E-06
+            },
+            "search": {
+              "score": 0.9999993
+            },
+            "SpecifyName": {
+              "score": 3.0666667E-09
+            },
+            "Travel": {
+              "score": 3.09763345E-06
+            },
+            "Weather.GetForecast": {
+              "score": 1.02792524E-06
+            }
+          },
+          "normalizedQuery": "email about something wicked this way comes from bart simpson and also kb435",
+          "sentiment": {
+            "label": "negative",
+            "score": 0.210341513
+          },
+          "topIntent": "search"
+        },
+        "query": "email about something wicked this way comes from bart simpson and also kb435"
+      }
+    }
+  }
\ No newline at end of file
diff --git a/libraries/botbuilder-ai/tests/luis/test_data/Prebuilt_v3.json b/libraries/botbuilder-ai/tests/luis/test_data/Prebuilt_v3.json
new file mode 100644
index 000000000..9cb4ab134
--- /dev/null
+++ b/libraries/botbuilder-ai/tests/luis/test_data/Prebuilt_v3.json
@@ -0,0 +1,246 @@
+{
+    "entities": {
+      "$instance": {
+        "Composite2": [
+          {
+            "endIndex": 66,
+            "modelType": "Composite Entity Extractor",
+            "recognitionSources": [
+              "model"
+            ],
+            "score": 0.7077416,
+            "startIndex": 0,
+            "text": "http://foo.com is where you can get a weather forecast for seattle",
+            "type": "Composite2"
+          }
+        ],
+        "geographyV2": [
+          {
+            "endIndex": 66,
+            "modelType": "Prebuilt Entity Extractor",
+            "recognitionSources": [
+              "model"
+            ],
+            "startIndex": 59,
+            "text": "seattle",
+            "type": "builtin.geographyV2.city"
+          }
+        ]
+      },
+      "Composite2": [
+        {
+          "$instance": {
+            "url": [
+              {
+                "endIndex": 14,
+                "modelType": "Prebuilt Entity Extractor",
+                "recognitionSources": [
+                  "model"
+                ],
+                "startIndex": 0,
+                "text": "http://foo.com",
+                "type": "builtin.url"
+              }
+            ],
+            "Weather_Location": [
+              {
+                "endIndex": 66,
+                "modelType": "Entity Extractor",
+                "recognitionSources": [
+                  "model"
+                ],
+                "score": 0.76184386,
+                "startIndex": 59,
+                "text": "seattle",
+                "type": "Weather.Location"
+              }
+            ]
+          },
+          "url": [
+            "http://foo.com"
+          ],
+          "Weather_Location": [
+            "seattle"
+          ]
+        }
+      ],
+      "geographyV2": [
+        {
+          "location": "seattle",
+          "type": "city"
+        }
+      ]
+    },
+    "intents": {
+      "Cancel": {
+        "score": 0.000171828113
+      },
+      "Delivery": {
+        "score": 0.0011408634
+      },
+      "EntityTests": {
+        "score": 0.342939854
+      },
+      "Greeting": {
+        "score": 0.0001518702
+      },
+      "Help": {
+        "score": 0.0005502715
+      },
+      "None": {
+        "score": 0.0175834317
+      },
+      "Roles": {
+        "score": 0.0432791822
+      },
+      "search": {
+        "score": 0.01050759
+      },
+      "SpecifyName": {
+        "score": 0.001833231
+      },
+      "Travel": {
+        "score": 0.004430798
+      },
+      "Weather_GetForecast": {
+        "score": 0.669524968
+      }
+    },
+    "sentiment": {
+      "label": "neutral",
+      "score": 0.5
+    },
+    "text": "http://foo.com is where you can get a weather forecast for seattle",
+    "v3": {
+      "options": {
+        "includeAllIntents": true,
+        "includeAPIResults": true,
+        "includeInstanceData": true,
+        "log": true,
+        "preferExternalEntities": true,
+        "slot": "production"
+      },
+      "response": {
+        "prediction": {
+          "entities": {
+            "$instance": {
+              "Composite2": [
+                {
+                  "length": 66,
+                  "modelType": "Composite Entity Extractor",
+                  "modelTypeId": 4,
+                  "recognitionSources": [
+                    "model"
+                  ],
+                  "score": 0.7077416,
+                  "startIndex": 0,
+                  "text": "http://foo.com is where you can get a weather forecast for seattle",
+                  "type": "Composite2"
+                }
+              ],
+              "geographyV2": [
+                {
+                  "length": 7,
+                  "modelType": "Prebuilt Entity Extractor",
+                  "modelTypeId": 2,
+                  "recognitionSources": [
+                    "model"
+                  ],
+                  "startIndex": 59,
+                  "text": "seattle",
+                  "type": "builtin.geographyV2.city"
+                }
+              ]
+            },
+            "Composite2": [
+              {
+                "$instance": {
+                  "url": [
+                    {
+                      "length": 14,
+                      "modelType": "Prebuilt Entity Extractor",
+                      "modelTypeId": 2,
+                      "recognitionSources": [
+                        "model"
+                      ],
+                      "startIndex": 0,
+                      "text": "http://foo.com",
+                      "type": "builtin.url"
+                    }
+                  ],
+                  "Weather.Location": [
+                    {
+                      "length": 7,
+                      "modelType": "Entity Extractor",
+                      "modelTypeId": 1,
+                      "recognitionSources": [
+                        "model"
+                      ],
+                      "score": 0.76184386,
+                      "startIndex": 59,
+                      "text": "seattle",
+                      "type": "Weather.Location"
+                    }
+                  ]
+                },
+                "url": [
+                  "http://foo.com"
+                ],
+                "Weather.Location": [
+                  "seattle"
+                ]
+              }
+            ],
+            "geographyV2": [
+              {
+                "type": "city",
+                "value": "seattle"
+              }
+            ]
+          },
+          "intents": {
+            "Cancel": {
+              "score": 0.000171828113
+            },
+            "Delivery": {
+              "score": 0.0011408634
+            },
+            "EntityTests": {
+              "score": 0.342939854
+            },
+            "Greeting": {
+              "score": 0.0001518702
+            },
+            "Help": {
+              "score": 0.0005502715
+            },
+            "None": {
+              "score": 0.0175834317
+            },
+            "Roles": {
+              "score": 0.0432791822
+            },
+            "search": {
+              "score": 0.01050759
+            },
+            "SpecifyName": {
+              "score": 0.001833231
+            },
+            "Travel": {
+              "score": 0.004430798
+            },
+            "Weather.GetForecast": {
+              "score": 0.669524968
+            }
+          },
+          "normalizedQuery": "http://foo.com is where you can get a weather forecast for seattle",
+          "sentiment": {
+            "label": "neutral",
+            "score": 0.5
+          },
+          "topIntent": "Weather.GetForecast"
+        },
+        "query": "http://foo.com is where you can get a weather forecast for seattle"
+      }
+    }
+  }
\ No newline at end of file
diff --git a/libraries/botbuilder-ai/tests/luis/test_data/roles_v3.json b/libraries/botbuilder-ai/tests/luis/test_data/roles_v3.json
new file mode 100644
index 000000000..15ee58ac4
--- /dev/null
+++ b/libraries/botbuilder-ai/tests/luis/test_data/roles_v3.json
@@ -0,0 +1,1759 @@
+{
+  "text": "3 inches long by 2 inches wide and 5% to 10% and are you between 6 years old and 8 years old and can i trade kb457 for kb922 and change 425-777-1212 to 206-666-4123 and did delta buy virgin and did the rain from hawaii get to redmond and http://foo.com changed to http://blah.com and i like between 68 degrees and 72 degrees and john likes mary and leave 3pm and arrive 5pm and pay between $400 and $500 and send chrimc@hotmail.com from emad@gmail.com",
+  "intents": {
+    "Cancel": {
+      "score": 4.50860341e-7
+    },
+    "Delivery": {
+      "score": 0.00007978094
+    },
+    "EntityTests": {
+      "score": 0.0046325135
+    },
+    "Greeting": {
+      "score": 4.73494453e-7
+    },
+    "Help": {
+      "score": 7.622754e-7
+    },
+    "None": {
+      "score": 0.00093744183
+    },
+    "Roles": {
+      "score": 1
+    },
+    "search": {
+      "score": 0.07635335
+    },
+    "SpecifyName": {
+      "score": 0.00009136085
+    },
+    "Travel": {
+      "score": 0.00771805458
+    },
+    "Weather_GetForecast": {
+      "score": 0.0100867962
+    }
+  },
+  "entities": {
+    "$instance": {
+      "a": [
+        {
+          "endIndex": 309,
+          "modelType": "Prebuilt Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "startIndex": 299,
+          "text": "68 degrees",
+          "type": "builtin.temperature"
+        }
+      ],
+      "arrive": [
+        {
+          "endIndex": 373,
+          "modelType": "Prebuilt Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "startIndex": 370,
+          "text": "5pm",
+          "type": "builtin.datetimeV2.time"
+        }
+      ],
+      "b": [
+        {
+          "endIndex": 324,
+          "modelType": "Prebuilt Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "startIndex": 314,
+          "text": "72 degrees",
+          "type": "builtin.temperature"
+        }
+      ],
+      "begin": [
+        {
+          "endIndex": 76,
+          "modelType": "Prebuilt Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "startIndex": 65,
+          "text": "6 years old",
+          "type": "builtin.age"
+        }
+      ],
+      "buy": [
+        {
+          "endIndex": 124,
+          "modelType": "Regex Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "startIndex": 119,
+          "text": "kb922",
+          "type": "Part"
+        }
+      ],
+      "Buyer": [
+        {
+          "endIndex": 178,
+          "modelType": "List Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "startIndex": 173,
+          "text": "delta",
+          "type": "Airline"
+        }
+      ],
+      "Composite1": [
+        {
+          "endIndex": 172,
+          "modelType": "Composite Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "score": 0.01107535,
+          "startIndex": 0,
+          "text": "3 inches long by 2 inches wide and 5% to 10% and are you between 6 years old and 8 years old and can i trade kb457 for kb922 and change 425-777-1212 to 206-666-4123 and did",
+          "type": "Composite1"
+        }
+      ],
+      "Composite2": [
+        {
+          "endIndex": 283,
+          "modelType": "Composite Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "score": 0.15191336,
+          "startIndex": 238,
+          "text": "http://foo.com changed to http://blah.com and",
+          "type": "Composite2"
+        }
+      ],
+      "destination": [
+        {
+          "endIndex": 233,
+          "modelType": "Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "score": 0.985884964,
+          "startIndex": 226,
+          "text": "redmond",
+          "type": "Weather.Location"
+        }
+      ],
+      "dimension": [
+        {
+          "endIndex": 358,
+          "modelType": "Prebuilt Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "startIndex": 355,
+          "text": "3pm",
+          "type": "builtin.dimension"
+        },
+        {
+          "endIndex": 373,
+          "modelType": "Prebuilt Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "startIndex": 370,
+          "text": "5pm",
+          "type": "builtin.dimension"
+        }
+      ],
+      "end": [
+        {
+          "endIndex": 92,
+          "modelType": "Prebuilt Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "startIndex": 81,
+          "text": "8 years old",
+          "type": "builtin.age"
+        }
+      ],
+      "geographyV2": [
+        {
+          "endIndex": 218,
+          "modelType": "Prebuilt Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "startIndex": 212,
+          "text": "hawaii",
+          "type": "builtin.geographyV2.state"
+        },
+        {
+          "endIndex": 233,
+          "modelType": "Prebuilt Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "startIndex": 226,
+          "text": "redmond",
+          "type": "builtin.geographyV2.city"
+        }
+      ],
+      "leave": [
+        {
+          "endIndex": 358,
+          "modelType": "Prebuilt Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "startIndex": 355,
+          "text": "3pm",
+          "type": "builtin.datetimeV2.time"
+        }
+      ],
+      "length": [
+        {
+          "endIndex": 8,
+          "modelType": "Prebuilt Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "startIndex": 0,
+          "text": "3 inches",
+          "type": "builtin.dimension"
+        }
+      ],
+      "likee": [
+        {
+          "endIndex": 344,
+          "modelType": "Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "score": 0.9900547,
+          "startIndex": 340,
+          "text": "mary",
+          "type": "Name"
+        }
+      ],
+      "liker": [
+        {
+          "endIndex": 333,
+          "modelType": "Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "score": 0.992201567,
+          "startIndex": 329,
+          "text": "john",
+          "type": "Name"
+        }
+      ],
+      "max": [
+        {
+          "endIndex": 403,
+          "modelType": "Prebuilt Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "startIndex": 399,
+          "text": "$500",
+          "type": "builtin.currency"
+        }
+      ],
+      "maximum": [
+        {
+          "endIndex": 44,
+          "modelType": "Prebuilt Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "startIndex": 41,
+          "text": "10%",
+          "type": "builtin.percentage"
+        }
+      ],
+      "min": [
+        {
+          "endIndex": 394,
+          "modelType": "Prebuilt Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "startIndex": 390,
+          "text": "$400",
+          "type": "builtin.currency"
+        }
+      ],
+      "minimum": [
+        {
+          "endIndex": 37,
+          "modelType": "Prebuilt Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "startIndex": 35,
+          "text": "5%",
+          "type": "builtin.percentage"
+        }
+      ],
+      "newPhone": [
+        {
+          "endIndex": 164,
+          "modelType": "Prebuilt Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "score": 0.9,
+          "startIndex": 152,
+          "text": "206-666-4123",
+          "type": "builtin.phonenumber"
+        }
+      ],
+      "number": [
+        {
+          "endIndex": 301,
+          "modelType": "Prebuilt Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "startIndex": 299,
+          "text": "68",
+          "type": "builtin.number"
+        },
+        {
+          "endIndex": 316,
+          "modelType": "Prebuilt Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "startIndex": 314,
+          "text": "72",
+          "type": "builtin.number"
+        },
+        {
+          "endIndex": 394,
+          "modelType": "Prebuilt Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "startIndex": 391,
+          "text": "400",
+          "type": "builtin.number"
+        },
+        {
+          "endIndex": 403,
+          "modelType": "Prebuilt Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "startIndex": 400,
+          "text": "500",
+          "type": "builtin.number"
+        }
+      ],
+      "old": [
+        {
+          "endIndex": 148,
+          "modelType": "Prebuilt Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "score": 0.9,
+          "startIndex": 136,
+          "text": "425-777-1212",
+          "type": "builtin.phonenumber"
+        }
+      ],
+      "oldURL": [
+        {
+          "endIndex": 252,
+          "modelType": "Prebuilt Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "startIndex": 238,
+          "text": "http://foo.com",
+          "type": "builtin.url"
+        }
+      ],
+      "personName": [
+        {
+          "endIndex": 333,
+          "modelType": "Prebuilt Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "startIndex": 329,
+          "text": "john",
+          "type": "builtin.personName"
+        },
+        {
+          "endIndex": 344,
+          "modelType": "Prebuilt Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "startIndex": 340,
+          "text": "mary",
+          "type": "builtin.personName"
+        }
+      ],
+      "receiver": [
+        {
+          "endIndex": 431,
+          "modelType": "Prebuilt Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "startIndex": 413,
+          "text": "chrimc@hotmail.com",
+          "type": "builtin.email"
+        }
+      ],
+      "sell": [
+        {
+          "endIndex": 114,
+          "modelType": "Regex Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "startIndex": 109,
+          "text": "kb457",
+          "type": "Part"
+        }
+      ],
+      "Seller": [
+        {
+          "endIndex": 189,
+          "modelType": "List Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "startIndex": 183,
+          "text": "virgin",
+          "type": "Airline"
+        }
+      ],
+      "sender": [
+        {
+          "endIndex": 451,
+          "modelType": "Prebuilt Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "startIndex": 437,
+          "text": "emad@gmail.com",
+          "type": "builtin.email"
+        }
+      ],
+      "source": [
+        {
+          "endIndex": 218,
+          "modelType": "Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "score": 0.9713092,
+          "startIndex": 212,
+          "text": "hawaii",
+          "type": "Weather.Location"
+        }
+      ],
+      "width": [
+        {
+          "endIndex": 25,
+          "modelType": "Prebuilt Entity Extractor",
+          "recognitionSources": [
+            "model"
+          ],
+          "startIndex": 17,
+          "text": "2 inches",
+          "type": "builtin.dimension"
+        }
+      ]
+    },
+    "a": [
+      {
+        "number": 68,
+        "units": "Degree"
+      }
+    ],
+    "arrive": [
+      {
+        "timex": [
+          "T17"
+        ],
+        "type": "time"
+      }
+    ],
+    "b": [
+      {
+        "number": 72,
+        "units": "Degree"
+      }
+    ],
+    "begin": [
+      {
+        "number": 6,
+        "units": "Year"
+      }
+    ],
+    "buy": [
+      "kb922"
+    ],
+    "Buyer": [
+      [
+        "Delta"
+      ]
+    ],
+    "Composite1": [
+      {
+        "$instance": {
+          "datetime": [
+            {
+              "endIndex": 72,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 65,
+              "text": "6 years",
+              "type": "builtin.datetimeV2.duration"
+            },
+            {
+              "endIndex": 88,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 81,
+              "text": "8 years",
+              "type": "builtin.datetimeV2.duration"
+            }
+          ],
+          "number": [
+            {
+              "endIndex": 1,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 0,
+              "text": "3",
+              "type": "builtin.number"
+            },
+            {
+              "endIndex": 18,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 17,
+              "text": "2",
+              "type": "builtin.number"
+            },
+            {
+              "endIndex": 36,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 35,
+              "text": "5",
+              "type": "builtin.number"
+            },
+            {
+              "endIndex": 43,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 41,
+              "text": "10",
+              "type": "builtin.number"
+            },
+            {
+              "endIndex": 66,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 65,
+              "text": "6",
+              "type": "builtin.number"
+            },
+            {
+              "endIndex": 82,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 81,
+              "text": "8",
+              "type": "builtin.number"
+            },
+            {
+              "endIndex": 139,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 136,
+              "text": "425",
+              "type": "builtin.number"
+            },
+            {
+              "endIndex": 143,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 140,
+              "text": "777",
+              "type": "builtin.number"
+            },
+            {
+              "endIndex": 148,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 144,
+              "text": "1212",
+              "type": "builtin.number"
+            },
+            {
+              "endIndex": 155,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 152,
+              "text": "206",
+              "type": "builtin.number"
+            },
+            {
+              "endIndex": 159,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 156,
+              "text": "666",
+              "type": "builtin.number"
+            },
+            {
+              "endIndex": 164,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 160,
+              "text": "4123",
+              "type": "builtin.number"
+            }
+          ]
+        },
+        "datetime": [
+          {
+            "timex": [
+              "P6Y"
+            ],
+            "type": "duration"
+          },
+          {
+            "timex": [
+              "P8Y"
+            ],
+            "type": "duration"
+          }
+        ],
+        "number": [
+          3,
+          2,
+          5,
+          10,
+          6,
+          8,
+          425,
+          777,
+          1212,
+          206,
+          666,
+          4123
+        ]
+      }
+    ],
+    "Composite2": [
+      {
+        "$instance": {
+          "url": [
+            {
+              "endIndex": 279,
+              "modelType": "Prebuilt Entity Extractor",
+              "recognitionSources": [
+                "model"
+              ],
+              "startIndex": 264,
+              "text": "http://blah.com",
+              "type": "builtin.url"
+            }
+          ]
+        },
+        "url": [
+          "http://blah.com"
+        ]
+      }
+    ],
+    "destination": [
+      "redmond"
+    ],
+    "dimension": [
+      {
+        "number": 3,
+        "units": "Picometer"
+      },
+      {
+        "number": 5,
+        "units": "Picometer"
+      }
+    ],
+    "end": [
+      {
+        "number": 8,
+        "units": "Year"
+      }
+    ],
+    "geographyV2": [
+      {
+        "location": "hawaii",
+        "type": "state"
+      },
+      {
+        "location": "redmond",
+        "type": "city"
+      }
+    ],
+    "leave": [
+      {
+        "timex": [
+          "T15"
+        ],
+        "type": "time"
+      }
+    ],
+    "length": [
+      {
+        "number": 3,
+        "units": "Inch"
+      }
+    ],
+    "likee": [
+      "mary"
+    ],
+    "liker": [
+      "john"
+    ],
+    "max": [
+      {
+        "number": 500,
+        "units": "Dollar"
+      }
+    ],
+    "maximum": [
+      10
+    ],
+    "min": [
+      {
+        "number": 400,
+        "units": "Dollar"
+      }
+    ],
+    "minimum": [
+      5
+    ],
+    "newPhone": [
+      "206-666-4123"
+    ],
+    "number": [
+      68,
+      72,
+      400,
+      500
+    ],
+    "old": [
+      "425-777-1212"
+    ],
+    "oldURL": [
+      "http://foo.com"
+    ],
+    "personName": [
+      "john",
+      "mary"
+    ],
+    "receiver": [
+      "chrimc@hotmail.com"
+    ],
+    "sell": [
+      "kb457"
+    ],
+    "Seller": [
+      [
+        "Virgin"
+      ]
+    ],
+    "sender": [
+      "emad@gmail.com"
+    ],
+    "source": [
+      "hawaii"
+    ],
+    "width": [
+      {
+        "number": 2,
+        "units": "Inch"
+      }
+    ]
+  },
+  "sentiment": {
+    "label": "neutral",
+    "score": 0.5
+  },
+  "v3": {
+    "response": {
+      "prediction": {
+        "entities": {
+          "$instance": {
+            "a": [
+              {
+                "length": 10,
+                "modelType": "Prebuilt Entity Extractor",
+                "modelTypeId": 2,
+                "recognitionSources": [
+                  "model"
+                ],
+                "role": "a",
+                "startIndex": 299,
+                "text": "68 degrees",
+                "type": "builtin.temperature"
+              }
+            ],
+            "arrive": [
+              {
+                "length": 3,
+                "modelType": "Prebuilt Entity Extractor",
+                "modelTypeId": 2,
+                "recognitionSources": [
+                  "model"
+                ],
+                "role": "arrive",
+                "startIndex": 370,
+                "text": "5pm",
+                "type": "builtin.datetimeV2.time"
+              }
+            ],
+            "b": [
+              {
+                "length": 10,
+                "modelType": "Prebuilt Entity Extractor",
+                "modelTypeId": 2,
+                "recognitionSources": [
+                  "model"
+                ],
+                "role": "b",
+                "startIndex": 314,
+                "text": "72 degrees",
+                "type": "builtin.temperature"
+              }
+            ],
+            "begin": [
+              {
+                "length": 11,
+                "modelType": "Prebuilt Entity Extractor",
+                "modelTypeId": 2,
+                "recognitionSources": [
+                  "model"
+                ],
+                "role": "begin",
+                "startIndex": 65,
+                "text": "6 years old",
+                "type": "builtin.age"
+              }
+            ],
+            "buy": [
+              {
+                "length": 5,
+                "modelType": "Regex Entity Extractor",
+                "modelTypeId": 8,
+                "recognitionSources": [
+                  "model"
+                ],
+                "role": "buy",
+                "startIndex": 119,
+                "text": "kb922",
+                "type": "Part"
+              }
+            ],
+            "Buyer": [
+              {
+                "length": 5,
+                "modelType": "List Entity Extractor",
+                "modelTypeId": 5,
+                "recognitionSources": [
+                  "model"
+                ],
+                "role": "Buyer",
+                "startIndex": 173,
+                "text": "delta",
+                "type": "Airline"
+              }
+            ],
+            "Composite1": [
+              {
+                "length": 172,
+                "modelType": "Composite Entity Extractor",
+                "modelTypeId": 4,
+                "recognitionSources": [
+                  "model"
+                ],
+                "score": 0.01107535,
+                "startIndex": 0,
+                "text": "3 inches long by 2 inches wide and 5% to 10% and are you between 6 years old and 8 years old and can i trade kb457 for kb922 and change 425-777-1212 to 206-666-4123 and did",
+                "type": "Composite1"
+              }
+            ],
+            "Composite2": [
+              {
+                "length": 45,
+                "modelType": "Composite Entity Extractor",
+                "modelTypeId": 4,
+                "recognitionSources": [
+                  "model"
+                ],
+                "score": 0.15191336,
+                "startIndex": 238,
+                "text": "http://foo.com changed to http://blah.com and",
+                "type": "Composite2"
+              }
+            ],
+            "destination": [
+              {
+                "length": 7,
+                "modelType": "Entity Extractor",
+                "modelTypeId": 1,
+                "recognitionSources": [
+                  "model"
+                ],
+                "role": "destination",
+                "score": 0.985884964,
+                "startIndex": 226,
+                "text": "redmond",
+                "type": "Weather.Location"
+              }
+            ],
+            "dimension": [
+              {
+                "length": 3,
+                "modelType": "Prebuilt Entity Extractor",
+                "modelTypeId": 2,
+                "recognitionSources": [
+                  "model"
+                ],
+                "startIndex": 355,
+                "text": "3pm",
+                "type": "builtin.dimension"
+              },
+              {
+                "length": 3,
+                "modelType": "Prebuilt Entity Extractor",
+                "modelTypeId": 2,
+                "recognitionSources": [
+                  "model"
+                ],
+                "startIndex": 370,
+                "text": "5pm",
+                "type": "builtin.dimension"
+              }
+            ],
+            "end": [
+              {
+                "length": 11,
+                "modelType": "Prebuilt Entity Extractor",
+                "modelTypeId": 2,
+                "recognitionSources": [
+                  "model"
+                ],
+                "role": "end",
+                "startIndex": 81,
+                "text": "8 years old",
+                "type": "builtin.age"
+              }
+            ],
+            "geographyV2": [
+              {
+                "length": 6,
+                "modelType": "Prebuilt Entity Extractor",
+                "modelTypeId": 2,
+                "recognitionSources": [
+                  "model"
+                ],
+                "startIndex": 212,
+                "text": "hawaii",
+                "type": "builtin.geographyV2.state"
+              },
+              {
+                "length": 7,
+                "modelType": "Prebuilt Entity Extractor",
+                "modelTypeId": 2,
+                "recognitionSources": [
+                  "model"
+                ],
+                "startIndex": 226,
+                "text": "redmond",
+                "type": "builtin.geographyV2.city"
+              }
+            ],
+            "leave": [
+              {
+                "length": 3,
+                "modelType": "Prebuilt Entity Extractor",
+                "modelTypeId": 2,
+                "recognitionSources": [
+                  "model"
+                ],
+                "role": "leave",
+                "startIndex": 355,
+                "text": "3pm",
+                "type": "builtin.datetimeV2.time"
+              }
+            ],
+            "length": [
+              {
+                "length": 8,
+                "modelType": "Prebuilt Entity Extractor",
+                "modelTypeId": 2,
+                "recognitionSources": [
+                  "model"
+                ],
+                "role": "length",
+                "startIndex": 0,
+                "text": "3 inches",
+                "type": "builtin.dimension"
+              }
+            ],
+            "likee": [
+              {
+                "length": 4,
+                "modelType": "Entity Extractor",
+                "modelTypeId": 1,
+                "recognitionSources": [
+                  "model"
+                ],
+                "role": "likee",
+                "score": 0.9900547,
+                "startIndex": 340,
+                "text": "mary",
+                "type": "Name"
+              }
+            ],
+            "liker": [
+              {
+                "length": 4,
+                "modelType": "Entity Extractor",
+                "modelTypeId": 1,
+                "recognitionSources": [
+                  "model"
+                ],
+                "role": "liker",
+                "score": 0.992201567,
+                "startIndex": 329,
+                "text": "john",
+                "type": "Name"
+              }
+            ],
+            "max": [
+              {
+                "length": 4,
+                "modelType": "Prebuilt Entity Extractor",
+                "modelTypeId": 2,
+                "recognitionSources": [
+                  "model"
+                ],
+                "role": "max",
+                "startIndex": 399,
+                "text": "$500",
+                "type": "builtin.currency"
+              }
+            ],
+            "maximum": [
+              {
+                "length": 3,
+                "modelType": "Prebuilt Entity Extractor",
+                "modelTypeId": 2,
+                "recognitionSources": [
+                  "model"
+                ],
+                "role": "maximum",
+                "startIndex": 41,
+                "text": "10%",
+                "type": "builtin.percentage"
+              }
+            ],
+            "min": [
+              {
+                "length": 4,
+                "modelType": "Prebuilt Entity Extractor",
+                "modelTypeId": 2,
+                "recognitionSources": [
+                  "model"
+                ],
+                "role": "min",
+                "startIndex": 390,
+                "text": "$400",
+                "type": "builtin.currency"
+              }
+            ],
+            "minimum": [
+              {
+                "length": 2,
+                "modelType": "Prebuilt Entity Extractor",
+                "modelTypeId": 2,
+                "recognitionSources": [
+                  "model"
+                ],
+                "role": "minimum",
+                "startIndex": 35,
+                "text": "5%",
+                "type": "builtin.percentage"
+              }
+            ],
+            "newPhone": [
+              {
+                "length": 12,
+                "modelType": "Prebuilt Entity Extractor",
+                "modelTypeId": 2,
+                "recognitionSources": [
+                  "model"
+                ],
+                "role": "newPhone",
+                "score": 0.9,
+                "startIndex": 152,
+                "text": "206-666-4123",
+                "type": "builtin.phonenumber"
+              }
+            ],
+            "number": [
+              {
+                "length": 2,
+                "modelType": "Prebuilt Entity Extractor",
+                "modelTypeId": 2,
+                "recognitionSources": [
+                  "model"
+                ],
+                "startIndex": 299,
+                "text": "68",
+                "type": "builtin.number"
+              },
+              {
+                "length": 2,
+                "modelType": "Prebuilt Entity Extractor",
+                "modelTypeId": 2,
+                "recognitionSources": [
+                  "model"
+                ],
+                "startIndex": 314,
+                "text": "72",
+                "type": "builtin.number"
+              },
+              {
+                "length": 3,
+                "modelType": "Prebuilt Entity Extractor",
+                "modelTypeId": 2,
+                "recognitionSources": [
+                  "model"
+                ],
+                "startIndex": 391,
+                "text": "400",
+                "type": "builtin.number"
+              },
+              {
+                "length": 3,
+                "modelType": "Prebuilt Entity Extractor",
+                "modelTypeId": 2,
+                "recognitionSources": [
+                  "model"
+                ],
+                "startIndex": 400,
+                "text": "500",
+                "type": "builtin.number"
+              }
+            ],
+            "old": [
+              {
+                "length": 12,
+                "modelType": "Prebuilt Entity Extractor",
+                "modelTypeId": 2,
+                "recognitionSources": [
+                  "model"
+                ],
+                "role": "old",
+                "score": 0.9,
+                "startIndex": 136,
+                "text": "425-777-1212",
+                "type": "builtin.phonenumber"
+              }
+            ],
+            "oldURL": [
+              {
+                "length": 14,
+                "modelType": "Prebuilt Entity Extractor",
+                "modelTypeId": 2,
+                "recognitionSources": [
+                  "model"
+                ],
+                "role": "oldURL",
+                "startIndex": 238,
+                "text": "http://foo.com",
+                "type": "builtin.url"
+              }
+            ],
+            "personName": [
+              {
+                "length": 4,
+                "modelType": "Prebuilt Entity Extractor",
+                "modelTypeId": 2,
+                "recognitionSources": [
+                  "model"
+                ],
+                "startIndex": 329,
+                "text": "john",
+                "type": "builtin.personName"
+              },
+              {
+                "length": 4,
+                "modelType": "Prebuilt Entity Extractor",
+                "modelTypeId": 2,
+                "recognitionSources": [
+                  "model"
+                ],
+                "startIndex": 340,
+                "text": "mary",
+                "type": "builtin.personName"
+              }
+            ],
+            "receiver": [
+              {
+                "length": 18,
+                "modelType": "Prebuilt Entity Extractor",
+                "modelTypeId": 2,
+                "recognitionSources": [
+                  "model"
+                ],
+                "role": "receiver",
+                "startIndex": 413,
+                "text": "chrimc@hotmail.com",
+                "type": "builtin.email"
+              }
+            ],
+            "sell": [
+              {
+                "length": 5,
+                "modelType": "Regex Entity Extractor",
+                "modelTypeId": 8,
+                "recognitionSources": [
+                  "model"
+                ],
+                "role": "sell",
+                "startIndex": 109,
+                "text": "kb457",
+                "type": "Part"
+              }
+            ],
+            "Seller": [
+              {
+                "length": 6,
+                "modelType": "List Entity Extractor",
+                "modelTypeId": 5,
+                "recognitionSources": [
+                  "model"
+                ],
+                "role": "Seller",
+                "startIndex": 183,
+                "text": "virgin",
+                "type": "Airline"
+              }
+            ],
+            "sender": [
+              {
+                "length": 14,
+                "modelType": "Prebuilt Entity Extractor",
+                "modelTypeId": 2,
+                "recognitionSources": [
+                  "model"
+                ],
+                "role": "sender",
+                "startIndex": 437,
+                "text": "emad@gmail.com",
+                "type": "builtin.email"
+              }
+            ],
+            "source": [
+              {
+                "length": 6,
+                "modelType": "Entity Extractor",
+                "modelTypeId": 1,
+                "recognitionSources": [
+                  "model"
+                ],
+                "role": "source",
+                "score": 0.9713092,
+                "startIndex": 212,
+                "text": "hawaii",
+                "type": "Weather.Location"
+              }
+            ],
+            "width": [
+              {
+                "length": 8,
+                "modelType": "Prebuilt Entity Extractor",
+                "modelTypeId": 2,
+                "recognitionSources": [
+                  "model"
+                ],
+                "role": "width",
+                "startIndex": 17,
+                "text": "2 inches",
+                "type": "builtin.dimension"
+              }
+            ]
+          },
+          "a": [
+            {
+              "number": 68,
+              "unit": "Degree"
+            }
+          ],
+          "arrive": [
+            {
+              "type": "time",
+              "values": [
+                {
+                  "timex": "T17",
+                  "value": "17:00:00"
+                }
+              ]
+            }
+          ],
+          "b": [
+            {
+              "number": 72,
+              "unit": "Degree"
+            }
+          ],
+          "begin": [
+            {
+              "number": 6,
+              "unit": "Year"
+            }
+          ],
+          "buy": [
+            "kb922"
+          ],
+          "Buyer": [
+            [
+              "Delta"
+            ]
+          ],
+          "Composite1": [
+            {
+              "$instance": {
+                "datetimeV2": [
+                  {
+                    "length": 7,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 65,
+                    "text": "6 years",
+                    "type": "builtin.datetimeV2.duration"
+                  },
+                  {
+                    "length": 7,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 81,
+                    "text": "8 years",
+                    "type": "builtin.datetimeV2.duration"
+                  }
+                ],
+                "number": [
+                  {
+                    "length": 1,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 0,
+                    "text": "3",
+                    "type": "builtin.number"
+                  },
+                  {
+                    "length": 1,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 17,
+                    "text": "2",
+                    "type": "builtin.number"
+                  },
+                  {
+                    "length": 1,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 35,
+                    "text": "5",
+                    "type": "builtin.number"
+                  },
+                  {
+                    "length": 2,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 41,
+                    "text": "10",
+                    "type": "builtin.number"
+                  },
+                  {
+                    "length": 1,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 65,
+                    "text": "6",
+                    "type": "builtin.number"
+                  },
+                  {
+                    "length": 1,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 81,
+                    "text": "8",
+                    "type": "builtin.number"
+                  },
+                  {
+                    "length": 3,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 136,
+                    "text": "425",
+                    "type": "builtin.number"
+                  },
+                  {
+                    "length": 3,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 140,
+                    "text": "777",
+                    "type": "builtin.number"
+                  },
+                  {
+                    "length": 4,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 144,
+                    "text": "1212",
+                    "type": "builtin.number"
+                  },
+                  {
+                    "length": 3,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 152,
+                    "text": "206",
+                    "type": "builtin.number"
+                  },
+                  {
+                    "length": 3,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 156,
+                    "text": "666",
+                    "type": "builtin.number"
+                  },
+                  {
+                    "length": 4,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 160,
+                    "text": "4123",
+                    "type": "builtin.number"
+                  }
+                ]
+              },
+              "datetimeV2": [
+                {
+                  "type": "duration",
+                  "values": [
+                    {
+                      "timex": "P6Y",
+                      "value": "189216000"
+                    }
+                  ]
+                },
+                {
+                  "type": "duration",
+                  "values": [
+                    {
+                      "timex": "P8Y",
+                      "value": "252288000"
+                    }
+                  ]
+                }
+              ],
+              "number": [
+                3,
+                2,
+                5,
+                10,
+                6,
+                8,
+                425,
+                777,
+                1212,
+                206,
+                666,
+                4123
+              ]
+            }
+          ],
+          "Composite2": [
+            {
+              "$instance": {
+                "url": [
+                  {
+                    "length": 15,
+                    "modelType": "Prebuilt Entity Extractor",
+                    "modelTypeId": 2,
+                    "recognitionSources": [
+                      "model"
+                    ],
+                    "startIndex": 264,
+                    "text": "http://blah.com",
+                    "type": "builtin.url"
+                  }
+                ]
+              },
+              "url": [
+                "http://blah.com"
+              ]
+            }
+          ],
+          "destination": [
+            "redmond"
+          ],
+          "dimension": [
+            {
+              "number": 3,
+              "unit": "Picometer"
+            },
+            {
+              "number": 5,
+              "unit": "Picometer"
+            }
+          ],
+          "end": [
+            {
+              "number": 8,
+              "unit": "Year"
+            }
+          ],
+          "geographyV2": [
+            {
+              "type": "state",
+              "value": "hawaii"
+            },
+            {
+              "type": "city",
+              "value": "redmond"
+            }
+          ],
+          "leave": [
+            {
+              "type": "time",
+              "values": [
+                {
+                  "timex": "T15",
+                  "value": "15:00:00"
+                }
+              ]
+            }
+          ],
+          "length": [
+            {
+              "number": 3,
+              "unit": "Inch"
+            }
+          ],
+          "likee": [
+            "mary"
+          ],
+          "liker": [
+            "john"
+          ],
+          "max": [
+            {
+              "number": 500,
+              "unit": "Dollar"
+            }
+          ],
+          "maximum": [
+            10
+          ],
+          "min": [
+            {
+              "number": 400,
+              "unit": "Dollar"
+            }
+          ],
+          "minimum": [
+            5
+          ],
+          "newPhone": [
+            "206-666-4123"
+          ],
+          "number": [
+            68,
+            72,
+            400,
+            500
+          ],
+          "old": [
+            "425-777-1212"
+          ],
+          "oldURL": [
+            "http://foo.com"
+          ],
+          "personName": [
+            "john",
+            "mary"
+          ],
+          "receiver": [
+            "chrimc@hotmail.com"
+          ],
+          "sell": [
+            "kb457"
+          ],
+          "Seller": [
+            [
+              "Virgin"
+            ]
+          ],
+          "sender": [
+            "emad@gmail.com"
+          ],
+          "source": [
+            "hawaii"
+          ],
+          "width": [
+            {
+              "number": 2,
+              "unit": "Inch"
+            }
+          ]
+        },
+        "intents": {
+          "Cancel": {
+            "score": 4.50860341e-7
+          },
+          "Delivery": {
+            "score": 0.00007978094
+          },
+          "EntityTests": {
+            "score": 0.0046325135
+          },
+          "Greeting": {
+            "score": 4.73494453e-7
+          },
+          "Help": {
+            "score": 7.622754e-7
+          },
+          "None": {
+            "score": 0.00093744183
+          },
+          "Roles": {
+            "score": 1
+          },
+          "search": {
+            "score": 0.07635335
+          },
+          "SpecifyName": {
+            "score": 0.00009136085
+          },
+          "Travel": {
+            "score": 0.00771805458
+          },
+          "Weather.GetForecast": {
+            "score": 0.0100867962
+          }
+        },
+        "normalizedQuery": "3 inches long by 2 inches wide and 5% to 10% and are you between 6 years old and 8 years old and can i trade kb457 for kb922 and change 425-777-1212 to 206-666-4123 and did delta buy virgin and did the rain from hawaii get to redmond and http://foo.com changed to http://blah.com and i like between 68 degrees and 72 degrees and john likes mary and leave 3pm and arrive 5pm and pay between $400 and $500 and send chrimc@hotmail.com from emad@gmail.com",
+        "sentiment": {
+          "label": "neutral",
+          "score": 0.5
+        },
+        "topIntent": "Roles"
+      },
+      "query": "3 inches long by 2 inches wide and 5% to 10% and are you between 6 years old and 8 years old and can i trade kb457 for kb922 and change 425-777-1212 to 206-666-4123 and did delta buy virgin and did the rain from hawaii get to redmond and http://foo.com changed to http://blah.com and i like between 68 degrees and 72 degrees and john likes mary and leave 3pm and arrive 5pm and pay between $400 and $500 and send chrimc@hotmail.com from emad@gmail.com"
+    },
+    "options": {
+      "includeAllIntents": true,
+      "includeAPIResults": true,
+      "includeInstanceData": true,
+      "log": true,
+      "preferExternalEntities": true,
+      "slot": "production"
+    }
+  }
+}
\ No newline at end of file
diff --git a/libraries/botbuilder-ai/tests/qna/test_data/QnaMaker_IsTest_true.json b/libraries/botbuilder-ai/tests/qna/test_data/QnaMaker_IsTest_true.json
new file mode 100644
index 000000000..4723ee95e
--- /dev/null
+++ b/libraries/botbuilder-ai/tests/qna/test_data/QnaMaker_IsTest_true.json
@@ -0,0 +1,13 @@
+{
+    "activeLearningEnabled": true,
+    "answers": [
+      {
+        "questions": [],
+        "answer": "No good match found in KB.",
+        "score": 0,
+        "id": -1,
+        "source": null,
+        "metadata": []
+      }
+    ]
+  }
\ No newline at end of file
diff --git a/libraries/botbuilder-ai/tests/qna/test_data/QnaMaker_RankerType_QuestionOnly.json b/libraries/botbuilder-ai/tests/qna/test_data/QnaMaker_RankerType_QuestionOnly.json
new file mode 100644
index 000000000..c3df1eb40
--- /dev/null
+++ b/libraries/botbuilder-ai/tests/qna/test_data/QnaMaker_RankerType_QuestionOnly.json
@@ -0,0 +1,35 @@
+{
+    "activeLearningEnabled": false,
+    "answers": [
+      {
+        "questions": [
+          "Q1"
+        ],
+        "answer": "A1",
+        "score": 80,
+        "id": 15,
+        "source": "Editorial",
+        "metadata": [
+          {
+            "name": "topic",
+            "value": "value"
+          }
+        ]
+      },
+      {
+        "questions": [
+          "Q2"
+        ],
+        "answer": "A2",
+        "score": 78,
+        "id": 16,
+        "source": "Editorial",
+        "metadata": [
+          {
+            "name": "topic",
+            "value": "value"
+          }
+        ]
+      }
+    ]
+  }
\ No newline at end of file
diff --git a/libraries/botbuilder-ai/tests/qna/test_data/QnaMaker_TopNAnswer.json b/libraries/botbuilder-ai/tests/qna/test_data/QnaMaker_TopNAnswer.json
new file mode 100644
index 000000000..6c09c9b8d
--- /dev/null
+++ b/libraries/botbuilder-ai/tests/qna/test_data/QnaMaker_TopNAnswer.json
@@ -0,0 +1,65 @@
+{
+  "activeLearningEnabled": true,
+  "answers": [
+    {
+      "questions": [
+        "Q1"
+      ],
+      "answer": "A1",
+      "score": 80,
+      "id": 15,
+      "source": "Editorial",
+      "metadata": [
+        {
+          "name": "topic",
+          "value": "value"
+        }
+      ]
+    },
+    {
+      "questions": [
+        "Q2"
+      ],
+      "answer": "A2",
+      "score": 78,
+      "id": 16,
+      "source": "Editorial",
+      "metadata": [
+        {
+          "name": "topic",
+          "value": "value"
+        }
+      ]
+    },
+    {
+      "questions": [
+        "Q3"
+      ],
+      "answer": "A3",
+      "score": 75,
+      "id": 17,
+      "source": "Editorial",
+      "metadata": [
+        {
+          "name": "topic",
+          "value": "value"
+        }
+      ]
+    },
+    {
+      "questions": [
+        "Q4"
+      ],
+      "answer": "A4",
+      "score": 50,
+      "id": 18,
+      "source": "Editorial",
+      "metadata": [
+        {
+          "name": "topic",
+          "value": "value"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/libraries/botbuilder-ai/tests/qna/test_data/QnaMaker_TopNAnswer_DisableActiveLearning.json b/libraries/botbuilder-ai/tests/qna/test_data/QnaMaker_TopNAnswer_DisableActiveLearning.json
new file mode 100644
index 000000000..f4fa91d57
--- /dev/null
+++ b/libraries/botbuilder-ai/tests/qna/test_data/QnaMaker_TopNAnswer_DisableActiveLearning.json
@@ -0,0 +1,65 @@
+{
+  "activeLearningEnabled": false,
+  "answers": [
+    {
+      "questions": [
+        "Q1"
+      ],
+      "answer": "A1",
+      "score": 80,
+      "id": 15,
+      "source": "Editorial",
+      "metadata": [
+        {
+          "name": "topic",
+          "value": "value"
+        }
+      ]
+    },
+    {
+      "questions": [
+        "Q2"
+      ],
+      "answer": "A2",
+      "score": 78,
+      "id": 16,
+      "source": "Editorial",
+      "metadata": [
+        {
+          "name": "topic",
+          "value": "value"
+        }
+      ]
+    },
+    {
+      "questions": [
+        "Q3"
+      ],
+      "answer": "A3",
+      "score": 75,
+      "id": 17,
+      "source": "Editorial",
+      "metadata": [
+        {
+          "name": "topic",
+          "value": "value"
+        }
+      ]
+    },
+    {
+      "questions": [
+        "Q4"
+      ],
+      "answer": "A4",
+      "score": 50,
+      "id": 18,
+      "source": "Editorial",
+      "metadata": [
+        {
+          "name": "topic",
+          "value": "value"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/libraries/botbuilder-ai/tests/qna/test_data/RetrunsAnswer_WithStrictFilter_And_Operator.json b/libraries/botbuilder-ai/tests/qna/test_data/RetrunsAnswer_WithStrictFilter_And_Operator.json
new file mode 100644
index 000000000..1bb54754a
--- /dev/null
+++ b/libraries/botbuilder-ai/tests/qna/test_data/RetrunsAnswer_WithStrictFilter_And_Operator.json
@@ -0,0 +1,29 @@
+{
+    "answers": [
+        {
+            "questions": [
+                "Where can you find Misty",
+                "Misty"
+            ],
+            "answer": "Wherever people are having a swimming good time",
+            "score": 74.51,
+            "id": 27,
+            "source": "Editorial",
+            "metadata": [
+                {
+                    "name": "species",
+                    "value": "human"
+                },
+                {
+                    "name": "type",
+                    "value": "water"
+                }
+            ],
+            "context": {
+                "isContextOnly": false,
+                "prompts": []
+            }
+        }
+    ],
+    "activeLearningEnabled": true
+}
diff --git a/libraries/botbuilder-ai/tests/qna/test_data/RetrunsAnswer_WithStrictFilter_Or_Operator.json b/libraries/botbuilder-ai/tests/qna/test_data/RetrunsAnswer_WithStrictFilter_Or_Operator.json
new file mode 100644
index 000000000..3346464fc
--- /dev/null
+++ b/libraries/botbuilder-ai/tests/qna/test_data/RetrunsAnswer_WithStrictFilter_Or_Operator.json
@@ -0,0 +1,76 @@
+{
+    "answers": [
+        {
+            "questions": [
+                "Where can you find Squirtle"
+            ],
+            "answer": "Did you not see him in the first three balls?",
+            "score": 80.22,
+            "id": 28,
+            "source": "Editorial",
+            "metadata": [
+                {
+                    "name": "species",
+                    "value": "turtle"
+                },
+                {
+                    "name": "type",
+                    "value": "water"
+                }
+            ],
+            "context": {
+                "isContextOnly": false,
+                "prompts": []
+            }
+        },
+        {
+            "questions": [
+                "Where can you find Ash",
+                "Ash"
+            ],
+            "answer": "I don't know. Maybe ask your little electric mouse friend?",
+            "score": 63.74,
+            "id": 26,
+            "source": "Editorial",
+            "metadata": [
+                {
+                    "name": "species",
+                    "value": "human"
+                },
+                {
+                    "name": "type",
+                    "value": "miscellaneous"
+                }
+            ],
+            "context": {
+                "isContextOnly": false,
+                "prompts": []
+            }
+        },
+        {
+            "questions": [
+                "Where can you find Misty",
+                "Misty"
+            ],
+            "answer": "Wherever people are having a swimming good time",
+            "score": 31.13,
+            "id": 27,
+            "source": "Editorial",
+            "metadata": [
+                {
+                    "name": "species",
+                    "value": "human"
+                },
+                {
+                    "name": "type",
+                    "value": "water"
+                }
+            ],
+            "context": {
+                "isContextOnly": false,
+                "prompts": []
+            }
+        }
+    ],
+    "activeLearningEnabled": true
+}
diff --git a/libraries/botbuilder-ai/tests/qna/test_qna.py b/libraries/botbuilder-ai/tests/qna/test_qna.py
index fa1643612..236594ac0 100644
--- a/libraries/botbuilder-ai/tests/qna/test_qna.py
+++ b/libraries/botbuilder-ai/tests/qna/test_qna.py
@@ -1,931 +1,1149 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-# pylint: disable=protected-access
-
-import json
-from os import path
-from typing import List, Dict
-import unittest
-from unittest.mock import patch
-from aiohttp import ClientSession
-
-import aiounittest
-from botbuilder.ai.qna import QnAMakerEndpoint, QnAMaker, QnAMakerOptions
-from botbuilder.ai.qna.models import (
-    FeedbackRecord,
-    Metadata,
-    QueryResult,
-    QnARequestContext,
-)
-from botbuilder.ai.qna.utils import QnATelemetryConstants
-from botbuilder.core import BotAdapter, BotTelemetryClient, TurnContext
-from botbuilder.core.adapters import TestAdapter
-from botbuilder.schema import (
-    Activity,
-    ActivityTypes,
-    ChannelAccount,
-    ConversationAccount,
-)
-
-
-class TestContext(TurnContext):
-    def __init__(self, request):
-        super().__init__(TestAdapter(), request)
-        self.sent: List[Activity] = list()
-
-        self.on_send_activities(self.capture_sent_activities)
-
-    async def capture_sent_activities(
-        self, context: TurnContext, activities, next
-    ):  # pylint: disable=unused-argument
-        self.sent += activities
-        context.responded = True
-
-
-class QnaApplicationTest(aiounittest.AsyncTestCase):
-    # Note this is NOT a real QnA Maker application ID nor a real QnA Maker subscription-key
-    # theses are GUIDs edited to look right to the parsing and validation code.
-
-    _knowledge_base_id: str = "f028d9k3-7g9z-11d3-d300-2b8x98227q8w"
-    _endpoint_key: str = "1k997n7w-207z-36p3-j2u1-09tas20ci6011"
-    _host: str = "https://dummyqnahost.azurewebsites.net/qnamaker"
-
-    tests_endpoint = QnAMakerEndpoint(_knowledge_base_id, _endpoint_key, _host)
-
-    def test_qnamaker_construction(self):
-        # Arrange
-        endpoint = self.tests_endpoint
-
-        # Act
-        qna = QnAMaker(endpoint)
-        endpoint = qna._endpoint
-
-        # Assert
-        self.assertEqual(
-            "f028d9k3-7g9z-11d3-d300-2b8x98227q8w", endpoint.knowledge_base_id
-        )
-        self.assertEqual("1k997n7w-207z-36p3-j2u1-09tas20ci6011", endpoint.endpoint_key)
-        self.assertEqual(
-            "https://dummyqnahost.azurewebsites.net/qnamaker", endpoint.host
-        )
-
-    def test_endpoint_with_empty_kbid(self):
-        empty_kbid = ""
-
-        with self.assertRaises(TypeError):
-            QnAMakerEndpoint(empty_kbid, self._endpoint_key, self._host)
-
-    def test_endpoint_with_empty_endpoint_key(self):
-        empty_endpoint_key = ""
-
-        with self.assertRaises(TypeError):
-            QnAMakerEndpoint(self._knowledge_base_id, empty_endpoint_key, self._host)
-
-    def test_endpoint_with_emptyhost(self):
-        with self.assertRaises(TypeError):
-            QnAMakerEndpoint(self._knowledge_base_id, self._endpoint_key, "")
-
-    def test_qnamaker_with_none_endpoint(self):
-        with self.assertRaises(TypeError):
-            QnAMaker(None)
-
-    def test_set_default_options_with_no_options_arg(self):
-        qna_without_options = QnAMaker(self.tests_endpoint)
-        options = qna_without_options._generate_answer_helper.options
-
-        default_threshold = 0.3
-        default_top = 1
-        default_strict_filters = []
-
-        self.assertEqual(default_threshold, options.score_threshold)
-        self.assertEqual(default_top, options.top)
-        self.assertEqual(default_strict_filters, options.strict_filters)
-
-    def test_options_passed_to_ctor(self):
-        options = QnAMakerOptions(
-            score_threshold=0.8,
-            timeout=9000,
-            top=5,
-            strict_filters=[Metadata("movie", "disney")],
-        )
-
-        qna_with_options = QnAMaker(self.tests_endpoint, options)
-        actual_options = qna_with_options._generate_answer_helper.options
-
-        expected_threshold = 0.8
-        expected_timeout = 9000
-        expected_top = 5
-        expected_strict_filters = [Metadata("movie", "disney")]
-
-        self.assertEqual(expected_threshold, actual_options.score_threshold)
-        self.assertEqual(expected_timeout, actual_options.timeout)
-        self.assertEqual(expected_top, actual_options.top)
-        self.assertEqual(
-            expected_strict_filters[0].name, actual_options.strict_filters[0].name
-        )
-        self.assertEqual(
-            expected_strict_filters[0].value, actual_options.strict_filters[0].value
-        )
-
-    async def test_returns_answer(self):
-        # Arrange
-        question: str = "how do I clean the stove?"
-        response_path: str = "ReturnsAnswer.json"
-
-        # Act
-        result = await QnaApplicationTest._get_service_result(question, response_path)
-
-        first_answer = result[0]
-
-        # Assert
-        self.assertIsNotNone(result)
-        self.assertEqual(1, len(result))
-        self.assertEqual(
-            "BaseCamp: You can use a damp rag to clean around the Power Pack",
-            first_answer.answer,
-        )
-
-    async def test_active_learning_enabled_status(self):
-        # Arrange
-        question: str = "how do I clean the stove?"
-        response_path: str = "ReturnsAnswer.json"
-
-        # Act
-        result = await QnaApplicationTest._get_service_result_raw(
-            question, response_path
-        )
-
-        # Assert
-        self.assertIsNotNone(result)
-        self.assertEqual(1, len(result.answers))
-        self.assertFalse(result.active_learning_enabled)
-
-    async def test_returns_answer_using_options(self):
-        # Arrange
-        question: str = "up"
-        response_path: str = "AnswerWithOptions.json"
-        options = QnAMakerOptions(
-            score_threshold=0.8, top=5, strict_filters=[Metadata("movie", "disney")]
-        )
-
-        # Act
-        result = await QnaApplicationTest._get_service_result(
-            question, response_path, options=options
-        )
-
-        first_answer = result[0]
-        has_at_least_1_ans = True
-        first_metadata = first_answer.metadata[0]
-
-        # Assert
-        self.assertIsNotNone(result)
-        self.assertEqual(has_at_least_1_ans, len(result) >= 1)
-        self.assertTrue(first_answer.answer[0])
-        self.assertEqual("is a movie", first_answer.answer)
-        self.assertTrue(first_answer.score >= options.score_threshold)
-        self.assertEqual("movie", first_metadata.name)
-        self.assertEqual("disney", first_metadata.value)
-
-    async def test_trace_test(self):
-        activity = Activity(
-            type=ActivityTypes.message,
-            text="how do I clean the stove?",
-            conversation=ConversationAccount(),
-            recipient=ChannelAccount(),
-            from_property=ChannelAccount(),
-        )
-
-        response_json = QnaApplicationTest._get_json_for_file("ReturnsAnswer.json")
-        qna = QnAMaker(QnaApplicationTest.tests_endpoint)
-
-        context = TestContext(activity)
-
-        with patch(
-            "aiohttp.ClientSession.post",
-            return_value=aiounittest.futurized(response_json),
-        ):
-            result = await qna.get_answers(context)
-
-            qna_trace_activities = list(
-                filter(
-                    lambda act: act.type == "trace" and act.name == "QnAMaker",
-                    context.sent,
-                )
-            )
-            trace_activity = qna_trace_activities[0]
-
-            self.assertEqual("trace", trace_activity.type)
-            self.assertEqual("QnAMaker", trace_activity.name)
-            self.assertEqual("QnAMaker Trace", trace_activity.label)
-            self.assertEqual(
-                "https://www.qnamaker.ai/schemas/trace", trace_activity.value_type
-            )
-            self.assertEqual(True, hasattr(trace_activity, "value"))
-            self.assertEqual(True, hasattr(trace_activity.value, "message"))
-            self.assertEqual(True, hasattr(trace_activity.value, "query_results"))
-            self.assertEqual(True, hasattr(trace_activity.value, "score_threshold"))
-            self.assertEqual(True, hasattr(trace_activity.value, "top"))
-            self.assertEqual(True, hasattr(trace_activity.value, "strict_filters"))
-            self.assertEqual(
-                self._knowledge_base_id, trace_activity.value.knowledge_base_id
-            )
-
-            return result
-
-    async def test_returns_answer_with_timeout(self):
-        question: str = "how do I clean the stove?"
-        options = QnAMakerOptions(timeout=999999)
-        qna = QnAMaker(QnaApplicationTest.tests_endpoint, options)
-        context = QnaApplicationTest._get_context(question, TestAdapter())
-        response_json = QnaApplicationTest._get_json_for_file("ReturnsAnswer.json")
-
-        with patch(
-            "aiohttp.ClientSession.post",
-            return_value=aiounittest.futurized(response_json),
-        ):
-            result = await qna.get_answers(context, options)
-
-            self.assertIsNotNone(result)
-            self.assertEqual(
-                options.timeout, qna._generate_answer_helper.options.timeout
-            )
-
-    async def test_telemetry_returns_answer(self):
-        # Arrange
-        question: str = "how do I clean the stove?"
-        response_json = QnaApplicationTest._get_json_for_file("ReturnsAnswer.json")
-        telemetry_client = unittest.mock.create_autospec(BotTelemetryClient)
-        log_personal_information = True
-        context = QnaApplicationTest._get_context(question, TestAdapter())
-        qna = QnAMaker(
-            QnaApplicationTest.tests_endpoint,
-            telemetry_client=telemetry_client,
-            log_personal_information=log_personal_information,
-        )
-
-        # Act
-        with patch(
-            "aiohttp.ClientSession.post",
-            return_value=aiounittest.futurized(response_json),
-        ):
-            results = await qna.get_answers(context)
-
-            telemetry_args = telemetry_client.track_event.call_args_list[0][1]
-            telemetry_properties = telemetry_args["properties"]
-            telemetry_metrics = telemetry_args["measurements"]
-            number_of_args = len(telemetry_args)
-            first_answer = telemetry_args["properties"][
-                QnATelemetryConstants.answer_property
-            ]
-            expected_answer = (
-                "BaseCamp: You can use a damp rag to clean around the Power Pack"
-            )
-
-            # Assert - Check Telemetry logged.
-            self.assertEqual(1, telemetry_client.track_event.call_count)
-            self.assertEqual(3, number_of_args)
-            self.assertEqual("QnaMessage", telemetry_args["name"])
-            self.assertTrue("answer" in telemetry_properties)
-            self.assertTrue("knowledgeBaseId" in telemetry_properties)
-            self.assertTrue("matchedQuestion" in telemetry_properties)
-            self.assertTrue("question" in telemetry_properties)
-            self.assertTrue("questionId" in telemetry_properties)
-            self.assertTrue("articleFound" in telemetry_properties)
-            self.assertEqual(expected_answer, first_answer)
-            self.assertTrue("score" in telemetry_metrics)
-            self.assertEqual(1, telemetry_metrics["score"])
-
-            # Assert - Validate we didn't break QnA functionality.
-            self.assertIsNotNone(results)
-            self.assertEqual(1, len(results))
-            self.assertEqual(expected_answer, results[0].answer)
-            self.assertEqual("Editorial", results[0].source)
-
-    async def test_telemetry_returns_answer_when_no_answer_found_in_kb(self):
-        # Arrange
-        question: str = "gibberish question"
-        response_json = QnaApplicationTest._get_json_for_file("NoAnswerFoundInKb.json")
-        telemetry_client = unittest.mock.create_autospec(BotTelemetryClient)
-        qna = QnAMaker(
-            QnaApplicationTest.tests_endpoint,
-            telemetry_client=telemetry_client,
-            log_personal_information=True,
-        )
-        context = QnaApplicationTest._get_context(question, TestAdapter())
-
-        # Act
-        with patch(
-            "aiohttp.ClientSession.post",
-            return_value=aiounittest.futurized(response_json),
-        ):
-            results = await qna.get_answers(context)
-
-            telemetry_args = telemetry_client.track_event.call_args_list[0][1]
-            telemetry_properties = telemetry_args["properties"]
-            number_of_args = len(telemetry_args)
-            first_answer = telemetry_args["properties"][
-                QnATelemetryConstants.answer_property
-            ]
-            expected_answer = "No Qna Answer matched"
-            expected_matched_question = "No Qna Question matched"
-
-            # Assert - Check Telemetry logged.
-            self.assertEqual(1, telemetry_client.track_event.call_count)
-            self.assertEqual(3, number_of_args)
-            self.assertEqual("QnaMessage", telemetry_args["name"])
-            self.assertTrue("answer" in telemetry_properties)
-            self.assertTrue("knowledgeBaseId" in telemetry_properties)
-            self.assertTrue("matchedQuestion" in telemetry_properties)
-            self.assertEqual(
-                expected_matched_question,
-                telemetry_properties[QnATelemetryConstants.matched_question_property],
-            )
-            self.assertTrue("question" in telemetry_properties)
-            self.assertTrue("questionId" in telemetry_properties)
-            self.assertTrue("articleFound" in telemetry_properties)
-            self.assertEqual(expected_answer, first_answer)
-
-            # Assert - Validate we didn't break QnA functionality.
-            self.assertIsNotNone(results)
-            self.assertEqual(0, len(results))
-
-    async def test_telemetry_pii(self):
-        # Arrange
-        question: str = "how do I clean the stove?"
-        response_json = QnaApplicationTest._get_json_for_file("ReturnsAnswer.json")
-        telemetry_client = unittest.mock.create_autospec(BotTelemetryClient)
-        log_personal_information = False
-        context = QnaApplicationTest._get_context(question, TestAdapter())
-        qna = QnAMaker(
-            QnaApplicationTest.tests_endpoint,
-            telemetry_client=telemetry_client,
-            log_personal_information=log_personal_information,
-        )
-
-        # Act
-        with patch(
-            "aiohttp.ClientSession.post",
-            return_value=aiounittest.futurized(response_json),
-        ):
-            results = await qna.get_answers(context)
-
-            telemetry_args = telemetry_client.track_event.call_args_list[0][1]
-            telemetry_properties = telemetry_args["properties"]
-            telemetry_metrics = telemetry_args["measurements"]
-            number_of_args = len(telemetry_args)
-            first_answer = telemetry_args["properties"][
-                QnATelemetryConstants.answer_property
-            ]
-            expected_answer = (
-                "BaseCamp: You can use a damp rag to clean around the Power Pack"
-            )
-
-            # Assert - Validate PII properties not logged.
-            self.assertEqual(1, telemetry_client.track_event.call_count)
-            self.assertEqual(3, number_of_args)
-            self.assertEqual("QnaMessage", telemetry_args["name"])
-            self.assertTrue("answer" in telemetry_properties)
-            self.assertTrue("knowledgeBaseId" in telemetry_properties)
-            self.assertTrue("matchedQuestion" in telemetry_properties)
-            self.assertTrue("question" not in telemetry_properties)
-            self.assertTrue("questionId" in telemetry_properties)
-            self.assertTrue("articleFound" in telemetry_properties)
-            self.assertEqual(expected_answer, first_answer)
-            self.assertTrue("score" in telemetry_metrics)
-            self.assertEqual(1, telemetry_metrics["score"])
-
-            # Assert - Validate we didn't break QnA functionality.
-            self.assertIsNotNone(results)
-            self.assertEqual(1, len(results))
-            self.assertEqual(expected_answer, results[0].answer)
-            self.assertEqual("Editorial", results[0].source)
-
-    async def test_telemetry_override(self):
-        # Arrange
-        question: str = "how do I clean the stove?"
-        response_json = QnaApplicationTest._get_json_for_file("ReturnsAnswer.json")
-        context = QnaApplicationTest._get_context(question, TestAdapter())
-        options = QnAMakerOptions(top=1)
-        telemetry_client = unittest.mock.create_autospec(BotTelemetryClient)
-        log_personal_information = False
-
-        # Act - Override the QnAMaker object to log custom stuff and honor params passed in.
-        telemetry_properties: Dict[str, str] = {"id": "MyId"}
-        qna = QnaApplicationTest.OverrideTelemetry(
-            QnaApplicationTest.tests_endpoint,
-            options,
-            None,
-            telemetry_client,
-            log_personal_information,
-        )
-        with patch(
-            "aiohttp.ClientSession.post",
-            return_value=aiounittest.futurized(response_json),
-        ):
-            results = await qna.get_answers(context, options, telemetry_properties)
-
-            telemetry_args = telemetry_client.track_event.call_args_list
-            first_call_args = telemetry_args[0][0]
-            first_call_properties = first_call_args[1]
-            second_call_args = telemetry_args[1][0]
-            second_call_properties = second_call_args[1]
-            expected_answer = (
-                "BaseCamp: You can use a damp rag to clean around the Power Pack"
-            )
-
-            # Assert
-            self.assertEqual(2, telemetry_client.track_event.call_count)
-            self.assertEqual(2, len(first_call_args))
-            self.assertEqual("QnaMessage", first_call_args[0])
-            self.assertEqual(2, len(first_call_properties))
-            self.assertTrue("my_important_property" in first_call_properties)
-            self.assertEqual(
-                "my_important_value", first_call_properties["my_important_property"]
-            )
-            self.assertTrue("id" in first_call_properties)
-            self.assertEqual("MyId", first_call_properties["id"])
-
-            self.assertEqual("my_second_event", second_call_args[0])
-            self.assertTrue("my_important_property2" in second_call_properties)
-            self.assertEqual(
-                "my_important_value2", second_call_properties["my_important_property2"]
-            )
-
-            # Validate we didn't break QnA functionality.
-            self.assertIsNotNone(results)
-            self.assertEqual(1, len(results))
-            self.assertEqual(expected_answer, results[0].answer)
-            self.assertEqual("Editorial", results[0].source)
-
-    async def test_telemetry_additional_props_metrics(self):
-        # Arrange
-        question: str = "how do I clean the stove?"
-        response_json = QnaApplicationTest._get_json_for_file("ReturnsAnswer.json")
-        context = QnaApplicationTest._get_context(question, TestAdapter())
-        options = QnAMakerOptions(top=1)
-        telemetry_client = unittest.mock.create_autospec(BotTelemetryClient)
-        log_personal_information = False
-
-        # Act
-        with patch(
-            "aiohttp.ClientSession.post",
-            return_value=aiounittest.futurized(response_json),
-        ):
-            qna = QnAMaker(
-                QnaApplicationTest.tests_endpoint,
-                options,
-                None,
-                telemetry_client,
-                log_personal_information,
-            )
-            telemetry_properties: Dict[str, str] = {
-                "my_important_property": "my_important_value"
-            }
-            telemetry_metrics: Dict[str, float] = {"my_important_metric": 3.14159}
-
-            results = await qna.get_answers(
-                context, None, telemetry_properties, telemetry_metrics
-            )
-
-            # Assert - Added properties were added.
-            telemetry_args = telemetry_client.track_event.call_args_list[0][1]
-            telemetry_properties = telemetry_args["properties"]
-            expected_answer = (
-                "BaseCamp: You can use a damp rag to clean around the Power Pack"
-            )
-
-            self.assertEqual(1, telemetry_client.track_event.call_count)
-            self.assertEqual(3, len(telemetry_args))
-            self.assertEqual("QnaMessage", telemetry_args["name"])
-            self.assertTrue("knowledgeBaseId" in telemetry_properties)
-            self.assertTrue("question" not in telemetry_properties)
-            self.assertTrue("matchedQuestion" in telemetry_properties)
-            self.assertTrue("questionId" in telemetry_properties)
-            self.assertTrue("answer" in telemetry_properties)
-            self.assertTrue(expected_answer, telemetry_properties["answer"])
-            self.assertTrue("my_important_property" in telemetry_properties)
-            self.assertEqual(
-                "my_important_value", telemetry_properties["my_important_property"]
-            )
-
-            tracked_metrics = telemetry_args["measurements"]
-
-            self.assertEqual(2, len(tracked_metrics))
-            self.assertTrue("score" in tracked_metrics)
-            self.assertTrue("my_important_metric" in tracked_metrics)
-            self.assertEqual(3.14159, tracked_metrics["my_important_metric"])
-
-            # Assert - Validate we didn't break QnA functionality.
-            self.assertIsNotNone(results)
-            self.assertEqual(1, len(results))
-            self.assertEqual(expected_answer, results[0].answer)
-            self.assertEqual("Editorial", results[0].source)
-
-    async def test_telemetry_additional_props_override(self):
-        question: str = "how do I clean the stove?"
-        response_json = QnaApplicationTest._get_json_for_file("ReturnsAnswer.json")
-        context = QnaApplicationTest._get_context(question, TestAdapter())
-        options = QnAMakerOptions(top=1)
-        telemetry_client = unittest.mock.create_autospec(BotTelemetryClient)
-        log_personal_information = False
-
-        # Act - Pass in properties during QnA invocation that override default properties
-        # NOTE: We are invoking this with PII turned OFF, and passing a PII property (originalQuestion).
-        qna = QnAMaker(
-            QnaApplicationTest.tests_endpoint,
-            options,
-            None,
-            telemetry_client,
-            log_personal_information,
-        )
-        telemetry_properties = {
-            "knowledge_base_id": "my_important_value",
-            "original_question": "my_important_value2",
-        }
-        telemetry_metrics = {"score": 3.14159}
-
-        with patch(
-            "aiohttp.ClientSession.post",
-            return_value=aiounittest.futurized(response_json),
-        ):
-            results = await qna.get_answers(
-                context, None, telemetry_properties, telemetry_metrics
-            )
-
-            # Assert - Added properties were added.
-            tracked_args = telemetry_client.track_event.call_args_list[0][1]
-            tracked_properties = tracked_args["properties"]
-            expected_answer = (
-                "BaseCamp: You can use a damp rag to clean around the Power Pack"
-            )
-            tracked_metrics = tracked_args["measurements"]
-
-            self.assertEqual(1, telemetry_client.track_event.call_count)
-            self.assertEqual(3, len(tracked_args))
-            self.assertEqual("QnaMessage", tracked_args["name"])
-            self.assertTrue("knowledge_base_id" in tracked_properties)
-            self.assertEqual(
-                "my_important_value", tracked_properties["knowledge_base_id"]
-            )
-            self.assertTrue("original_question" in tracked_properties)
-            self.assertTrue("matchedQuestion" in tracked_properties)
-            self.assertEqual(
-                "my_important_value2", tracked_properties["original_question"]
-            )
-            self.assertTrue("question" not in tracked_properties)
-            self.assertTrue("questionId" in tracked_properties)
-            self.assertTrue("answer" in tracked_properties)
-            self.assertEqual(expected_answer, tracked_properties["answer"])
-            self.assertTrue("my_important_property" not in tracked_properties)
-            self.assertEqual(1, len(tracked_metrics))
-            self.assertTrue("score" in tracked_metrics)
-            self.assertEqual(3.14159, tracked_metrics["score"])
-
-            # Assert - Validate we didn't break QnA functionality.
-            self.assertIsNotNone(results)
-            self.assertEqual(1, len(results))
-            self.assertEqual(expected_answer, results[0].answer)
-            self.assertEqual("Editorial", results[0].source)
-
-    async def test_telemetry_fill_props_override(self):
-        # Arrange
-        question: str = "how do I clean the stove?"
-        response_json = QnaApplicationTest._get_json_for_file("ReturnsAnswer.json")
-        context: TurnContext = QnaApplicationTest._get_context(question, TestAdapter())
-        options = QnAMakerOptions(top=1)
-        telemetry_client = unittest.mock.create_autospec(BotTelemetryClient)
-        log_personal_information = False
-
-        # Act - Pass in properties during QnA invocation that override default properties
-        #       In addition Override with derivation.  This presents an interesting question of order of setting
-        #       properties.
-        #       If I want to override "originalQuestion" property:
-        #           - Set in "Stock" schema
-        #           - Set in derived QnAMaker class
-        #           - Set in GetAnswersAsync
-        #       Logically, the GetAnswersAync should win.  But ultimately OnQnaResultsAsync decides since it is the last
-        #       code to touch the properties before logging (since it actually logs the event).
-        qna = QnaApplicationTest.OverrideFillTelemetry(
-            QnaApplicationTest.tests_endpoint,
-            options,
-            None,
-            telemetry_client,
-            log_personal_information,
-        )
-        telemetry_properties: Dict[str, str] = {
-            "knowledgeBaseId": "my_important_value",
-            "matchedQuestion": "my_important_value2",
-        }
-        telemetry_metrics: Dict[str, float] = {"score": 3.14159}
-
-        with patch(
-            "aiohttp.ClientSession.post",
-            return_value=aiounittest.futurized(response_json),
-        ):
-            results = await qna.get_answers(
-                context, None, telemetry_properties, telemetry_metrics
-            )
-
-            # Assert - Added properties were added.
-            first_call_args = telemetry_client.track_event.call_args_list[0][0]
-            first_properties = first_call_args[1]
-            expected_answer = (
-                "BaseCamp: You can use a damp rag to clean around the Power Pack"
-            )
-            first_metrics = first_call_args[2]
-
-            self.assertEqual(2, telemetry_client.track_event.call_count)
-            self.assertEqual(3, len(first_call_args))
-            self.assertEqual("QnaMessage", first_call_args[0])
-            self.assertEqual(6, len(first_properties))
-            self.assertTrue("knowledgeBaseId" in first_properties)
-            self.assertEqual("my_important_value", first_properties["knowledgeBaseId"])
-            self.assertTrue("matchedQuestion" in first_properties)
-            self.assertEqual("my_important_value2", first_properties["matchedQuestion"])
-            self.assertTrue("questionId" in first_properties)
-            self.assertTrue("answer" in first_properties)
-            self.assertEqual(expected_answer, first_properties["answer"])
-            self.assertTrue("articleFound" in first_properties)
-            self.assertTrue("my_important_property" in first_properties)
-            self.assertEqual(
-                "my_important_value", first_properties["my_important_property"]
-            )
-
-            self.assertEqual(1, len(first_metrics))
-            self.assertTrue("score" in first_metrics)
-            self.assertEqual(3.14159, first_metrics["score"])
-
-            # Assert - Validate we didn't break QnA functionality.
-            self.assertIsNotNone(results)
-            self.assertEqual(1, len(results))
-            self.assertEqual(expected_answer, results[0].answer)
-            self.assertEqual("Editorial", results[0].source)
-
-    async def test_call_train(self):
-        feedback_records = []
-
-        feedback1 = FeedbackRecord(
-            qna_id=1, user_id="test", user_question="How are you?"
-        )
-
-        feedback2 = FeedbackRecord(qna_id=2, user_id="test", user_question="What up??")
-
-        feedback_records.extend([feedback1, feedback2])
-
-        with patch.object(
-            QnAMaker, "call_train", return_value=None
-        ) as mocked_call_train:
-            qna = QnAMaker(QnaApplicationTest.tests_endpoint)
-            qna.call_train(feedback_records)
-
-            mocked_call_train.assert_called_once_with(feedback_records)
-
-    async def test_should_filter_low_score_variation(self):
-        options = QnAMakerOptions(top=5)
-        qna = QnAMaker(QnaApplicationTest.tests_endpoint, options)
-        question: str = "Q11"
-        context = QnaApplicationTest._get_context(question, TestAdapter())
-        response_json = QnaApplicationTest._get_json_for_file("TopNAnswer.json")
-
-        with patch(
-            "aiohttp.ClientSession.post",
-            return_value=aiounittest.futurized(response_json),
-        ):
-            results = await qna.get_answers(context)
-            self.assertEqual(4, len(results), "Should have received 4 answers.")
-
-            filtered_results = qna.get_low_score_variation(results)
-            self.assertEqual(
-                3,
-                len(filtered_results),
-                "Should have 3 filtered answers after low score variation.",
-            )
-
-    async def test_should_answer_with_prompts(self):
-        options = QnAMakerOptions(top=2)
-        qna = QnAMaker(QnaApplicationTest.tests_endpoint, options)
-        question: str = "how do I clean the stove?"
-        turn_context = QnaApplicationTest._get_context(question, TestAdapter())
-        response_json = QnaApplicationTest._get_json_for_file("AnswerWithPrompts.json")
-
-        with patch(
-            "aiohttp.ClientSession.post",
-            return_value=aiounittest.futurized(response_json),
-        ):
-            results = await qna.get_answers(turn_context, options)
-            self.assertEqual(1, len(results), "Should have received 1 answers.")
-            self.assertEqual(
-                1, len(results[0].context.prompts), "Should have received 1 prompt."
-            )
-
-    async def test_should_answer_with_high_score_provided_context(self):
-        qna = QnAMaker(QnaApplicationTest.tests_endpoint)
-        question: str = "where can I buy?"
-        context = QnARequestContext(
-            previous_qna_id=5, prvious_user_query="how do I clean the stove?"
-        )
-        options = QnAMakerOptions(top=2, qna_id=55, context=context)
-        turn_context = QnaApplicationTest._get_context(question, TestAdapter())
-        response_json = QnaApplicationTest._get_json_for_file(
-            "AnswerWithHighScoreProvidedContext.json"
-        )
-
-        with patch(
-            "aiohttp.ClientSession.post",
-            return_value=aiounittest.futurized(response_json),
-        ):
-            results = await qna.get_answers(turn_context, options)
-            self.assertEqual(1, len(results), "Should have received 1 answers.")
-            self.assertEqual(1, results[0].score, "Score should be high.")
-
-    async def test_should_answer_with_high_score_provided_qna_id(self):
-        qna = QnAMaker(QnaApplicationTest.tests_endpoint)
-        question: str = "where can I buy?"
-
-        options = QnAMakerOptions(top=2, qna_id=55)
-        turn_context = QnaApplicationTest._get_context(question, TestAdapter())
-        response_json = QnaApplicationTest._get_json_for_file(
-            "AnswerWithHighScoreProvidedContext.json"
-        )
-
-        with patch(
-            "aiohttp.ClientSession.post",
-            return_value=aiounittest.futurized(response_json),
-        ):
-            results = await qna.get_answers(turn_context, options)
-            self.assertEqual(1, len(results), "Should have received 1 answers.")
-            self.assertEqual(1, results[0].score, "Score should be high.")
-
-    async def test_should_answer_with_low_score_without_provided_context(self):
-        qna = QnAMaker(QnaApplicationTest.tests_endpoint)
-        question: str = "where can I buy?"
-        options = QnAMakerOptions(top=2, context=None)
-
-        turn_context = QnaApplicationTest._get_context(question, TestAdapter())
-        response_json = QnaApplicationTest._get_json_for_file(
-            "AnswerWithLowScoreProvidedWithoutContext.json"
-        )
-
-        with patch(
-            "aiohttp.ClientSession.post",
-            return_value=aiounittest.futurized(response_json),
-        ):
-            results = await qna.get_answers(turn_context, options)
-            self.assertEqual(
-                2, len(results), "Should have received more than one answers."
-            )
-            self.assertEqual(True, results[0].score < 1, "Score should be low.")
-
-    @classmethod
-    async def _get_service_result(
-        cls,
-        utterance: str,
-        response_file: str,
-        bot_adapter: BotAdapter = TestAdapter(),
-        options: QnAMakerOptions = None,
-    ) -> [dict]:
-        response_json = QnaApplicationTest._get_json_for_file(response_file)
-
-        qna = QnAMaker(QnaApplicationTest.tests_endpoint)
-        context = QnaApplicationTest._get_context(utterance, bot_adapter)
-
-        with patch(
-            "aiohttp.ClientSession.post",
-            return_value=aiounittest.futurized(response_json),
-        ):
-            result = await qna.get_answers(context, options)
-
-            return result
-
-    @classmethod
-    async def _get_service_result_raw(
-        cls,
-        utterance: str,
-        response_file: str,
-        bot_adapter: BotAdapter = TestAdapter(),
-        options: QnAMakerOptions = None,
-    ) -> [dict]:
-        response_json = QnaApplicationTest._get_json_for_file(response_file)
-
-        qna = QnAMaker(QnaApplicationTest.tests_endpoint)
-        context = QnaApplicationTest._get_context(utterance, bot_adapter)
-
-        with patch(
-            "aiohttp.ClientSession.post",
-            return_value=aiounittest.futurized(response_json),
-        ):
-            result = await qna.get_answers_raw(context, options)
-
-            return result
-
-    @classmethod
-    def _get_json_for_file(cls, response_file: str) -> object:
-        curr_dir = path.dirname(path.abspath(__file__))
-        response_path = path.join(curr_dir, "test_data", response_file)
-
-        with open(response_path, "r", encoding="utf-8-sig") as file:
-            response_str = file.read()
-        response_json = json.loads(response_str)
-
-        return response_json
-
-    @staticmethod
-    def _get_context(question: str, bot_adapter: BotAdapter) -> TurnContext:
-        test_adapter = bot_adapter or TestAdapter()
-        activity = Activity(
-            type=ActivityTypes.message,
-            text=question,
-            conversation=ConversationAccount(),
-            recipient=ChannelAccount(),
-            from_property=ChannelAccount(),
-        )
-
-        return TurnContext(test_adapter, activity)
-
-    class OverrideTelemetry(QnAMaker):
-        def __init__(  # pylint: disable=useless-super-delegation
-            self,
-            endpoint: QnAMakerEndpoint,
-            options: QnAMakerOptions,
-            http_client: ClientSession,
-            telemetry_client: BotTelemetryClient,
-            log_personal_information: bool,
-        ):
-            super().__init__(
-                endpoint,
-                options,
-                http_client,
-                telemetry_client,
-                log_personal_information,
-            )
-
-        async def on_qna_result(  # pylint: disable=unused-argument
-            self,
-            query_results: [QueryResult],
-            turn_context: TurnContext,
-            telemetry_properties: Dict[str, str] = None,
-            telemetry_metrics: Dict[str, float] = None,
-        ):
-            properties = telemetry_properties or {}
-
-            # get_answers overrides derived class
-            properties["my_important_property"] = "my_important_value"
-
-            # Log event
-            self.telemetry_client.track_event(
-                QnATelemetryConstants.qna_message_event, properties
-            )
-
-            # Create 2nd event.
-            second_event_properties = {"my_important_property2": "my_important_value2"}
-            self.telemetry_client.track_event(
-                "my_second_event", second_event_properties
-            )
-
-    class OverrideFillTelemetry(QnAMaker):
-        def __init__(  # pylint: disable=useless-super-delegation
-            self,
-            endpoint: QnAMakerEndpoint,
-            options: QnAMakerOptions,
-            http_client: ClientSession,
-            telemetry_client: BotTelemetryClient,
-            log_personal_information: bool,
-        ):
-            super().__init__(
-                endpoint,
-                options,
-                http_client,
-                telemetry_client,
-                log_personal_information,
-            )
-
-        async def on_qna_result(
-            self,
-            query_results: [QueryResult],
-            turn_context: TurnContext,
-            telemetry_properties: Dict[str, str] = None,
-            telemetry_metrics: Dict[str, float] = None,
-        ):
-            event_data = await self.fill_qna_event(
-                query_results, turn_context, telemetry_properties, telemetry_metrics
-            )
-
-            # Add my property.
-            event_data.properties.update(
-                {"my_important_property": "my_important_value"}
-            )
-
-            # Log QnaMessage event.
-            self.telemetry_client.track_event(
-                QnATelemetryConstants.qna_message_event,
-                event_data.properties,
-                event_data.metrics,
-            )
-
-            # Create second event.
-            second_event_properties: Dict[str, str] = {
-                "my_important_property2": "my_important_value2"
-            }
-
-            self.telemetry_client.track_event("MySecondEvent", second_event_properties)
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+# pylint: disable=protected-access
+# pylint: disable=too-many-lines
+
+import unittest
+from os import path
+from typing import List, Dict
+from unittest.mock import patch
+
+import json
+import requests
+from aiohttp import ClientSession
+
+import aiounittest
+from botbuilder.ai.qna import QnAMakerEndpoint, QnAMaker, QnAMakerOptions
+from botbuilder.ai.qna.models import (
+    FeedbackRecord,
+    JoinOperator,
+    Metadata,
+    QueryResult,
+    QnARequestContext,
+)
+from botbuilder.ai.qna.utils import HttpRequestUtils, QnATelemetryConstants
+from botbuilder.ai.qna.models import GenerateAnswerRequestBody
+from botbuilder.core import BotAdapter, BotTelemetryClient, TurnContext
+from botbuilder.core.adapters import TestAdapter
+from botbuilder.schema import (
+    Activity,
+    ActivityTypes,
+    ChannelAccount,
+    ConversationAccount,
+)
+
+
+class TestContext(TurnContext):
+    __test__ = False
+
+    def __init__(self, request):
+        super().__init__(TestAdapter(), request)
+        self.sent: List[Activity] = list()
+
+        self.on_send_activities(self.capture_sent_activities)
+
+    async def capture_sent_activities(
+        self, context: TurnContext, activities, next
+    ):  # pylint: disable=unused-argument
+        self.sent += activities
+        context.responded = True
+
+
+class QnaApplicationTest(aiounittest.AsyncTestCase):
+    # Note this is NOT a real QnA Maker application ID nor a real QnA Maker subscription-key
+    # theses are GUIDs edited to look right to the parsing and validation code.
+
+    _knowledge_base_id: str = "f028d9k3-7g9z-11d3-d300-2b8x98227q8w"
+    _endpoint_key: str = "1k997n7w-207z-36p3-j2u1-09tas20ci6011"
+    _host: str = "https://dummyqnahost.azurewebsites.net/qnamaker"
+
+    tests_endpoint = QnAMakerEndpoint(_knowledge_base_id, _endpoint_key, _host)
+
+    def test_qnamaker_construction(self):
+        # Arrange
+        endpoint = self.tests_endpoint
+
+        # Act
+        qna = QnAMaker(endpoint)
+        endpoint = qna._endpoint
+
+        # Assert
+        self.assertEqual(
+            "f028d9k3-7g9z-11d3-d300-2b8x98227q8w", endpoint.knowledge_base_id
+        )
+        self.assertEqual("1k997n7w-207z-36p3-j2u1-09tas20ci6011", endpoint.endpoint_key)
+        self.assertEqual(
+            "https://dummyqnahost.azurewebsites.net/qnamaker", endpoint.host
+        )
+
+    def test_endpoint_with_empty_kbid(self):
+        empty_kbid = ""
+
+        with self.assertRaises(TypeError):
+            QnAMakerEndpoint(empty_kbid, self._endpoint_key, self._host)
+
+    def test_endpoint_with_empty_endpoint_key(self):
+        empty_endpoint_key = ""
+
+        with self.assertRaises(TypeError):
+            QnAMakerEndpoint(self._knowledge_base_id, empty_endpoint_key, self._host)
+
+    def test_endpoint_with_emptyhost(self):
+        with self.assertRaises(TypeError):
+            QnAMakerEndpoint(self._knowledge_base_id, self._endpoint_key, "")
+
+    def test_qnamaker_with_none_endpoint(self):
+        with self.assertRaises(TypeError):
+            QnAMaker(None)
+
+    def test_set_default_options_with_no_options_arg(self):
+        qna_without_options = QnAMaker(self.tests_endpoint)
+        options = qna_without_options._generate_answer_helper.options
+
+        default_threshold = 0.3
+        default_top = 1
+        default_strict_filters = []
+
+        self.assertEqual(default_threshold, options.score_threshold)
+        self.assertEqual(default_top, options.top)
+        self.assertEqual(default_strict_filters, options.strict_filters)
+
+    def test_options_passed_to_ctor(self):
+        options = QnAMakerOptions(
+            score_threshold=0.8,
+            timeout=9000,
+            top=5,
+            strict_filters=[Metadata(**{"movie": "disney"})],
+        )
+
+        qna_with_options = QnAMaker(self.tests_endpoint, options)
+        actual_options = qna_with_options._generate_answer_helper.options
+
+        expected_threshold = 0.8
+        expected_timeout = 9000
+        expected_top = 5
+        expected_strict_filters = [Metadata(**{"movie": "disney"})]
+
+        self.assertEqual(expected_threshold, actual_options.score_threshold)
+        self.assertEqual(expected_timeout, actual_options.timeout)
+        self.assertEqual(expected_top, actual_options.top)
+        self.assertEqual(
+            expected_strict_filters[0].name, actual_options.strict_filters[0].name
+        )
+        self.assertEqual(
+            expected_strict_filters[0].value, actual_options.strict_filters[0].value
+        )
+
+    async def test_returns_answer(self):
+        # Arrange
+        question: str = "how do I clean the stove?"
+        response_path: str = "ReturnsAnswer.json"
+
+        # Act
+        result = await QnaApplicationTest._get_service_result(question, response_path)
+
+        first_answer = result[0]
+
+        # Assert
+        self.assertIsNotNone(result)
+        self.assertEqual(1, len(result))
+        self.assertEqual(
+            "BaseCamp: You can use a damp rag to clean around the Power Pack",
+            first_answer.answer,
+        )
+
+    async def test_active_learning_enabled_status(self):
+        # Arrange
+        question: str = "how do I clean the stove?"
+        response_path: str = "ReturnsAnswer.json"
+
+        # Act
+        result = await QnaApplicationTest._get_service_result_raw(
+            question, response_path
+        )
+
+        # Assert
+        self.assertIsNotNone(result)
+        self.assertEqual(1, len(result.answers))
+        self.assertFalse(result.active_learning_enabled)
+
+    async def test_returns_answer_with_strict_filters_with_or_operator(self):
+        # Arrange
+        question: str = "Where can you find"
+        response_path: str = "RetrunsAnswer_WithStrictFilter_Or_Operator.json"
+        response_json = QnaApplicationTest._get_json_for_file(response_path)
+
+        strict_filters = [
+            Metadata(name="species", value="human"),
+            Metadata(name="type", value="water"),
+        ]
+        options = QnAMakerOptions(
+            top=5,
+            strict_filters=strict_filters,
+            strict_filters_join_operator=JoinOperator.OR,
+        )
+        qna = QnAMaker(endpoint=QnaApplicationTest.tests_endpoint)
+        context = QnaApplicationTest._get_context(question, TestAdapter())
+
+        # Act
+        with patch(
+            "aiohttp.ClientSession.post",
+            return_value=aiounittest.futurized(response_json),
+        ) as mock_http_client:
+            result = await qna.get_answers_raw(context, options)
+
+            serialized_http_req_args = mock_http_client.call_args[1]["data"]
+            req_args = json.loads(serialized_http_req_args)
+
+            # Assert
+            self.assertIsNotNone(result)
+            self.assertEqual(3, len(result.answers))
+            self.assertEqual(
+                JoinOperator.OR, req_args["strictFiltersCompoundOperationType"]
+            )
+
+            req_args_strict_filters = req_args["strictFilters"]
+
+            first_filter = strict_filters[0]
+            self.assertEqual(first_filter.name, req_args_strict_filters[0]["name"])
+            self.assertEqual(first_filter.value, req_args_strict_filters[0]["value"])
+
+            second_filter = strict_filters[1]
+            self.assertEqual(second_filter.name, req_args_strict_filters[1]["name"])
+            self.assertEqual(second_filter.value, req_args_strict_filters[1]["value"])
+
+    async def test_returns_answer_with_strict_filters_with_and_operator(self):
+        # Arrange
+        question: str = "Where can you find"
+        response_path: str = "RetrunsAnswer_WithStrictFilter_And_Operator.json"
+        response_json = QnaApplicationTest._get_json_for_file(response_path)
+
+        strict_filters = [
+            Metadata(name="species", value="human"),
+            Metadata(name="type", value="water"),
+        ]
+        options = QnAMakerOptions(
+            top=5,
+            strict_filters=strict_filters,
+            strict_filters_join_operator=JoinOperator.AND,
+        )
+        qna = QnAMaker(endpoint=QnaApplicationTest.tests_endpoint)
+        context = QnaApplicationTest._get_context(question, TestAdapter())
+
+        # Act
+        with patch(
+            "aiohttp.ClientSession.post",
+            return_value=aiounittest.futurized(response_json),
+        ) as mock_http_client:
+            result = await qna.get_answers_raw(context, options)
+
+            serialized_http_req_args = mock_http_client.call_args[1]["data"]
+            req_args = json.loads(serialized_http_req_args)
+
+            # Assert
+            self.assertIsNotNone(result)
+            self.assertEqual(1, len(result.answers))
+            self.assertEqual(
+                JoinOperator.AND, req_args["strictFiltersCompoundOperationType"]
+            )
+
+            req_args_strict_filters = req_args["strictFilters"]
+
+            first_filter = strict_filters[0]
+            self.assertEqual(first_filter.name, req_args_strict_filters[0]["name"])
+            self.assertEqual(first_filter.value, req_args_strict_filters[0]["value"])
+
+            second_filter = strict_filters[1]
+            self.assertEqual(second_filter.name, req_args_strict_filters[1]["name"])
+            self.assertEqual(second_filter.value, req_args_strict_filters[1]["value"])
+
+    async def test_returns_answer_using_requests_module(self):
+        question: str = "how do I clean the stove?"
+        response_path: str = "ReturnsAnswer.json"
+        response_json = QnaApplicationTest._get_json_for_file(response_path)
+
+        qna = QnAMaker(endpoint=QnaApplicationTest.tests_endpoint, http_client=requests)
+        context = QnaApplicationTest._get_context(question, TestAdapter())
+
+        with patch("requests.post", return_value=response_json):
+            result = await qna.get_answers_raw(context)
+            answers = result.answers
+
+            self.assertIsNotNone(result)
+            self.assertEqual(1, len(answers))
+            self.assertEqual(
+                "BaseCamp: You can use a damp rag to clean around the Power Pack",
+                answers[0].answer,
+            )
+
+    async def test_returns_answer_using_options(self):
+        # Arrange
+        question: str = "up"
+        response_path: str = "AnswerWithOptions.json"
+        options = QnAMakerOptions(
+            score_threshold=0.8, top=5, strict_filters=[Metadata(**{"movie": "disney"})]
+        )
+
+        # Act
+        result = await QnaApplicationTest._get_service_result(
+            question, response_path, options=options
+        )
+
+        first_answer = result[0]
+        has_at_least_1_ans = True
+        first_metadata = first_answer.metadata[0]
+
+        # Assert
+        self.assertIsNotNone(result)
+        self.assertEqual(has_at_least_1_ans, len(result) >= 1)
+        self.assertTrue(first_answer.answer[0])
+        self.assertEqual("is a movie", first_answer.answer)
+        self.assertTrue(first_answer.score >= options.score_threshold)
+        self.assertEqual("movie", first_metadata.name)
+        self.assertEqual("disney", first_metadata.value)
+
+    async def test_trace_test(self):
+        activity = Activity(
+            type=ActivityTypes.message,
+            text="how do I clean the stove?",
+            conversation=ConversationAccount(),
+            recipient=ChannelAccount(),
+            from_property=ChannelAccount(),
+        )
+
+        response_json = QnaApplicationTest._get_json_for_file("ReturnsAnswer.json")
+        qna = QnAMaker(QnaApplicationTest.tests_endpoint)
+
+        context = TestContext(activity)
+
+        with patch(
+            "aiohttp.ClientSession.post",
+            return_value=aiounittest.futurized(response_json),
+        ):
+            result = await qna.get_answers(context)
+
+            qna_trace_activities = list(
+                filter(
+                    lambda act: act.type == "trace" and act.name == "QnAMaker",
+                    context.sent,
+                )
+            )
+            trace_activity = qna_trace_activities[0]
+
+            self.assertEqual("trace", trace_activity.type)
+            self.assertEqual("QnAMaker", trace_activity.name)
+            self.assertEqual("QnAMaker Trace", trace_activity.label)
+            self.assertEqual(
+                "https://www.qnamaker.ai/schemas/trace", trace_activity.value_type
+            )
+            self.assertEqual(True, hasattr(trace_activity, "value"))
+            self.assertEqual(True, hasattr(trace_activity.value, "message"))
+            self.assertEqual(True, hasattr(trace_activity.value, "query_results"))
+            self.assertEqual(True, hasattr(trace_activity.value, "score_threshold"))
+            self.assertEqual(True, hasattr(trace_activity.value, "top"))
+            self.assertEqual(True, hasattr(trace_activity.value, "strict_filters"))
+            self.assertEqual(
+                self._knowledge_base_id, trace_activity.value.knowledge_base_id
+            )
+
+            return result
+
+    async def test_returns_answer_with_timeout(self):
+        question: str = "how do I clean the stove?"
+        options = QnAMakerOptions(timeout=999999)
+        qna = QnAMaker(QnaApplicationTest.tests_endpoint, options)
+        context = QnaApplicationTest._get_context(question, TestAdapter())
+        response_json = QnaApplicationTest._get_json_for_file("ReturnsAnswer.json")
+
+        with patch(
+            "aiohttp.ClientSession.post",
+            return_value=aiounittest.futurized(response_json),
+        ):
+            result = await qna.get_answers(context, options)
+
+            self.assertIsNotNone(result)
+            self.assertEqual(
+                options.timeout, qna._generate_answer_helper.options.timeout
+            )
+
+    async def test_returns_answer_using_requests_module_with_no_timeout(self):
+        url = f"{QnaApplicationTest._host}/knowledgebases/{QnaApplicationTest._knowledge_base_id}/generateAnswer"
+        question = GenerateAnswerRequestBody(
+            question="how do I clean the stove?",
+            top=1,
+            score_threshold=0.3,
+            strict_filters=[],
+            context=None,
+            qna_id=None,
+            is_test=False,
+            ranker_type="Default",
+        )
+        response_path = "ReturnsAnswer.json"
+        response_json = QnaApplicationTest._get_json_for_file(response_path)
+
+        http_request_helper = HttpRequestUtils(requests)
+
+        with patch("requests.post", return_value=response_json):
+            result = await http_request_helper.execute_http_request(
+                url, question, QnaApplicationTest.tests_endpoint, timeout=None
+            )
+            answers = result["answers"]
+
+            self.assertIsNotNone(result)
+            self.assertEqual(1, len(answers))
+            self.assertEqual(
+                "BaseCamp: You can use a damp rag to clean around the Power Pack",
+                answers[0]["answer"],
+            )
+
+    async def test_telemetry_returns_answer(self):
+        # Arrange
+        question: str = "how do I clean the stove?"
+        response_json = QnaApplicationTest._get_json_for_file("ReturnsAnswer.json")
+        telemetry_client = unittest.mock.create_autospec(BotTelemetryClient)
+        log_personal_information = True
+        context = QnaApplicationTest._get_context(question, TestAdapter())
+        qna = QnAMaker(
+            QnaApplicationTest.tests_endpoint,
+            telemetry_client=telemetry_client,
+            log_personal_information=log_personal_information,
+        )
+
+        # Act
+        with patch(
+            "aiohttp.ClientSession.post",
+            return_value=aiounittest.futurized(response_json),
+        ):
+            results = await qna.get_answers(context)
+
+            telemetry_args = telemetry_client.track_event.call_args_list[0][1]
+            telemetry_properties = telemetry_args["properties"]
+            telemetry_metrics = telemetry_args["measurements"]
+            number_of_args = len(telemetry_args)
+            first_answer = telemetry_args["properties"][
+                QnATelemetryConstants.answer_property
+            ]
+            expected_answer = (
+                "BaseCamp: You can use a damp rag to clean around the Power Pack"
+            )
+
+            # Assert - Check Telemetry logged.
+            self.assertEqual(1, telemetry_client.track_event.call_count)
+            self.assertEqual(3, number_of_args)
+            self.assertEqual("QnaMessage", telemetry_args["name"])
+            self.assertTrue("answer" in telemetry_properties)
+            self.assertTrue("knowledgeBaseId" in telemetry_properties)
+            self.assertTrue("matchedQuestion" in telemetry_properties)
+            self.assertTrue("question" in telemetry_properties)
+            self.assertTrue("questionId" in telemetry_properties)
+            self.assertTrue("articleFound" in telemetry_properties)
+            self.assertEqual(expected_answer, first_answer)
+            self.assertTrue("score" in telemetry_metrics)
+            self.assertEqual(1, telemetry_metrics["score"])
+
+            # Assert - Validate we didn't break QnA functionality.
+            self.assertIsNotNone(results)
+            self.assertEqual(1, len(results))
+            self.assertEqual(expected_answer, results[0].answer)
+            self.assertEqual("Editorial", results[0].source)
+
+    async def test_telemetry_returns_answer_when_no_answer_found_in_kb(self):
+        # Arrange
+        question: str = "gibberish question"
+        response_json = QnaApplicationTest._get_json_for_file("NoAnswerFoundInKb.json")
+        telemetry_client = unittest.mock.create_autospec(BotTelemetryClient)
+        qna = QnAMaker(
+            QnaApplicationTest.tests_endpoint,
+            telemetry_client=telemetry_client,
+            log_personal_information=True,
+        )
+        context = QnaApplicationTest._get_context(question, TestAdapter())
+
+        # Act
+        with patch(
+            "aiohttp.ClientSession.post",
+            return_value=aiounittest.futurized(response_json),
+        ):
+            results = await qna.get_answers(context)
+
+            telemetry_args = telemetry_client.track_event.call_args_list[0][1]
+            telemetry_properties = telemetry_args["properties"]
+            number_of_args = len(telemetry_args)
+            first_answer = telemetry_args["properties"][
+                QnATelemetryConstants.answer_property
+            ]
+            expected_answer = "No Qna Answer matched"
+            expected_matched_question = "No Qna Question matched"
+
+            # Assert - Check Telemetry logged.
+            self.assertEqual(1, telemetry_client.track_event.call_count)
+            self.assertEqual(3, number_of_args)
+            self.assertEqual("QnaMessage", telemetry_args["name"])
+            self.assertTrue("answer" in telemetry_properties)
+            self.assertTrue("knowledgeBaseId" in telemetry_properties)
+            self.assertTrue("matchedQuestion" in telemetry_properties)
+            self.assertEqual(
+                expected_matched_question,
+                telemetry_properties[QnATelemetryConstants.matched_question_property],
+            )
+            self.assertTrue("question" in telemetry_properties)
+            self.assertTrue("questionId" in telemetry_properties)
+            self.assertTrue("articleFound" in telemetry_properties)
+            self.assertEqual(expected_answer, first_answer)
+
+            # Assert - Validate we didn't break QnA functionality.
+            self.assertIsNotNone(results)
+            self.assertEqual(0, len(results))
+
+    async def test_telemetry_pii(self):
+        # Arrange
+        question: str = "how do I clean the stove?"
+        response_json = QnaApplicationTest._get_json_for_file("ReturnsAnswer.json")
+        telemetry_client = unittest.mock.create_autospec(BotTelemetryClient)
+        log_personal_information = False
+        context = QnaApplicationTest._get_context(question, TestAdapter())
+        qna = QnAMaker(
+            QnaApplicationTest.tests_endpoint,
+            telemetry_client=telemetry_client,
+            log_personal_information=log_personal_information,
+        )
+
+        # Act
+        with patch(
+            "aiohttp.ClientSession.post",
+            return_value=aiounittest.futurized(response_json),
+        ):
+            results = await qna.get_answers(context)
+
+            telemetry_args = telemetry_client.track_event.call_args_list[0][1]
+            telemetry_properties = telemetry_args["properties"]
+            telemetry_metrics = telemetry_args["measurements"]
+            number_of_args = len(telemetry_args)
+            first_answer = telemetry_args["properties"][
+                QnATelemetryConstants.answer_property
+            ]
+            expected_answer = (
+                "BaseCamp: You can use a damp rag to clean around the Power Pack"
+            )
+
+            # Assert - Validate PII properties not logged.
+            self.assertEqual(1, telemetry_client.track_event.call_count)
+            self.assertEqual(3, number_of_args)
+            self.assertEqual("QnaMessage", telemetry_args["name"])
+            self.assertTrue("answer" in telemetry_properties)
+            self.assertTrue("knowledgeBaseId" in telemetry_properties)
+            self.assertTrue("matchedQuestion" in telemetry_properties)
+            self.assertTrue("question" not in telemetry_properties)
+            self.assertTrue("questionId" in telemetry_properties)
+            self.assertTrue("articleFound" in telemetry_properties)
+            self.assertEqual(expected_answer, first_answer)
+            self.assertTrue("score" in telemetry_metrics)
+            self.assertEqual(1, telemetry_metrics["score"])
+
+            # Assert - Validate we didn't break QnA functionality.
+            self.assertIsNotNone(results)
+            self.assertEqual(1, len(results))
+            self.assertEqual(expected_answer, results[0].answer)
+            self.assertEqual("Editorial", results[0].source)
+
+    async def test_telemetry_override(self):
+        # Arrange
+        question: str = "how do I clean the stove?"
+        response_json = QnaApplicationTest._get_json_for_file("ReturnsAnswer.json")
+        context = QnaApplicationTest._get_context(question, TestAdapter())
+        options = QnAMakerOptions(top=1)
+        telemetry_client = unittest.mock.create_autospec(BotTelemetryClient)
+        log_personal_information = False
+
+        # Act - Override the QnAMaker object to log custom stuff and honor params passed in.
+        telemetry_properties: Dict[str, str] = {"id": "MyId"}
+        qna = QnaApplicationTest.OverrideTelemetry(
+            QnaApplicationTest.tests_endpoint,
+            options,
+            None,
+            telemetry_client,
+            log_personal_information,
+        )
+        with patch(
+            "aiohttp.ClientSession.post",
+            return_value=aiounittest.futurized(response_json),
+        ):
+            results = await qna.get_answers(context, options, telemetry_properties)
+
+            telemetry_args = telemetry_client.track_event.call_args_list
+            first_call_args = telemetry_args[0][0]
+            first_call_properties = first_call_args[1]
+            second_call_args = telemetry_args[1][0]
+            second_call_properties = second_call_args[1]
+            expected_answer = (
+                "BaseCamp: You can use a damp rag to clean around the Power Pack"
+            )
+
+            # Assert
+            self.assertEqual(2, telemetry_client.track_event.call_count)
+            self.assertEqual(2, len(first_call_args))
+            self.assertEqual("QnaMessage", first_call_args[0])
+            self.assertEqual(2, len(first_call_properties))
+            self.assertTrue("my_important_property" in first_call_properties)
+            self.assertEqual(
+                "my_important_value", first_call_properties["my_important_property"]
+            )
+            self.assertTrue("id" in first_call_properties)
+            self.assertEqual("MyId", first_call_properties["id"])
+
+            self.assertEqual("my_second_event", second_call_args[0])
+            self.assertTrue("my_important_property2" in second_call_properties)
+            self.assertEqual(
+                "my_important_value2", second_call_properties["my_important_property2"]
+            )
+
+            # Validate we didn't break QnA functionality.
+            self.assertIsNotNone(results)
+            self.assertEqual(1, len(results))
+            self.assertEqual(expected_answer, results[0].answer)
+            self.assertEqual("Editorial", results[0].source)
+
+    async def test_telemetry_additional_props_metrics(self):
+        # Arrange
+        question: str = "how do I clean the stove?"
+        response_json = QnaApplicationTest._get_json_for_file("ReturnsAnswer.json")
+        context = QnaApplicationTest._get_context(question, TestAdapter())
+        options = QnAMakerOptions(top=1)
+        telemetry_client = unittest.mock.create_autospec(BotTelemetryClient)
+        log_personal_information = False
+
+        # Act
+        with patch(
+            "aiohttp.ClientSession.post",
+            return_value=aiounittest.futurized(response_json),
+        ):
+            qna = QnAMaker(
+                QnaApplicationTest.tests_endpoint,
+                options,
+                None,
+                telemetry_client,
+                log_personal_information,
+            )
+            telemetry_properties: Dict[str, str] = {
+                "my_important_property": "my_important_value"
+            }
+            telemetry_metrics: Dict[str, float] = {"my_important_metric": 3.14159}
+
+            results = await qna.get_answers(
+                context, None, telemetry_properties, telemetry_metrics
+            )
+
+            # Assert - Added properties were added.
+            telemetry_args = telemetry_client.track_event.call_args_list[0][1]
+            telemetry_properties = telemetry_args["properties"]
+            expected_answer = (
+                "BaseCamp: You can use a damp rag to clean around the Power Pack"
+            )
+
+            self.assertEqual(1, telemetry_client.track_event.call_count)
+            self.assertEqual(3, len(telemetry_args))
+            self.assertEqual("QnaMessage", telemetry_args["name"])
+            self.assertTrue("knowledgeBaseId" in telemetry_properties)
+            self.assertTrue("question" not in telemetry_properties)
+            self.assertTrue("matchedQuestion" in telemetry_properties)
+            self.assertTrue("questionId" in telemetry_properties)
+            self.assertTrue("answer" in telemetry_properties)
+            self.assertTrue(expected_answer, telemetry_properties["answer"])
+            self.assertTrue("my_important_property" in telemetry_properties)
+            self.assertEqual(
+                "my_important_value", telemetry_properties["my_important_property"]
+            )
+
+            tracked_metrics = telemetry_args["measurements"]
+
+            self.assertEqual(2, len(tracked_metrics))
+            self.assertTrue("score" in tracked_metrics)
+            self.assertTrue("my_important_metric" in tracked_metrics)
+            self.assertEqual(3.14159, tracked_metrics["my_important_metric"])
+
+            # Assert - Validate we didn't break QnA functionality.
+            self.assertIsNotNone(results)
+            self.assertEqual(1, len(results))
+            self.assertEqual(expected_answer, results[0].answer)
+            self.assertEqual("Editorial", results[0].source)
+
+    async def test_telemetry_additional_props_override(self):
+        question: str = "how do I clean the stove?"
+        response_json = QnaApplicationTest._get_json_for_file("ReturnsAnswer.json")
+        context = QnaApplicationTest._get_context(question, TestAdapter())
+        options = QnAMakerOptions(top=1)
+        telemetry_client = unittest.mock.create_autospec(BotTelemetryClient)
+        log_personal_information = False
+
+        # Act - Pass in properties during QnA invocation that override default properties
+        # NOTE: We are invoking this with PII turned OFF, and passing a PII property (originalQuestion).
+        qna = QnAMaker(
+            QnaApplicationTest.tests_endpoint,
+            options,
+            None,
+            telemetry_client,
+            log_personal_information,
+        )
+        telemetry_properties = {
+            "knowledge_base_id": "my_important_value",
+            "original_question": "my_important_value2",
+        }
+        telemetry_metrics = {"score": 3.14159}
+
+        with patch(
+            "aiohttp.ClientSession.post",
+            return_value=aiounittest.futurized(response_json),
+        ):
+            results = await qna.get_answers(
+                context, None, telemetry_properties, telemetry_metrics
+            )
+
+            # Assert - Added properties were added.
+            tracked_args = telemetry_client.track_event.call_args_list[0][1]
+            tracked_properties = tracked_args["properties"]
+            expected_answer = (
+                "BaseCamp: You can use a damp rag to clean around the Power Pack"
+            )
+            tracked_metrics = tracked_args["measurements"]
+
+            self.assertEqual(1, telemetry_client.track_event.call_count)
+            self.assertEqual(3, len(tracked_args))
+            self.assertEqual("QnaMessage", tracked_args["name"])
+            self.assertTrue("knowledge_base_id" in tracked_properties)
+            self.assertEqual(
+                "my_important_value", tracked_properties["knowledge_base_id"]
+            )
+            self.assertTrue("original_question" in tracked_properties)
+            self.assertTrue("matchedQuestion" in tracked_properties)
+            self.assertEqual(
+                "my_important_value2", tracked_properties["original_question"]
+            )
+            self.assertTrue("question" not in tracked_properties)
+            self.assertTrue("questionId" in tracked_properties)
+            self.assertTrue("answer" in tracked_properties)
+            self.assertEqual(expected_answer, tracked_properties["answer"])
+            self.assertTrue("my_important_property" not in tracked_properties)
+            self.assertEqual(1, len(tracked_metrics))
+            self.assertTrue("score" in tracked_metrics)
+            self.assertEqual(3.14159, tracked_metrics["score"])
+
+            # Assert - Validate we didn't break QnA functionality.
+            self.assertIsNotNone(results)
+            self.assertEqual(1, len(results))
+            self.assertEqual(expected_answer, results[0].answer)
+            self.assertEqual("Editorial", results[0].source)
+
+    async def test_telemetry_fill_props_override(self):
+        # Arrange
+        question: str = "how do I clean the stove?"
+        response_json = QnaApplicationTest._get_json_for_file("ReturnsAnswer.json")
+        context: TurnContext = QnaApplicationTest._get_context(question, TestAdapter())
+        options = QnAMakerOptions(top=1)
+        telemetry_client = unittest.mock.create_autospec(BotTelemetryClient)
+        log_personal_information = False
+
+        # Act - Pass in properties during QnA invocation that override default properties
+        #       In addition Override with derivation.  This presents an interesting question of order of setting
+        #       properties.
+        #       If I want to override "originalQuestion" property:
+        #           - Set in "Stock" schema
+        #           - Set in derived QnAMaker class
+        #           - Set in GetAnswersAsync
+        #       Logically, the GetAnswersAync should win.  But ultimately OnQnaResultsAsync decides since it is the last
+        #       code to touch the properties before logging (since it actually logs the event).
+        qna = QnaApplicationTest.OverrideFillTelemetry(
+            QnaApplicationTest.tests_endpoint,
+            options,
+            None,
+            telemetry_client,
+            log_personal_information,
+        )
+        telemetry_properties: Dict[str, str] = {
+            "knowledgeBaseId": "my_important_value",
+            "matchedQuestion": "my_important_value2",
+        }
+        telemetry_metrics: Dict[str, float] = {"score": 3.14159}
+
+        with patch(
+            "aiohttp.ClientSession.post",
+            return_value=aiounittest.futurized(response_json),
+        ):
+            results = await qna.get_answers(
+                context, None, telemetry_properties, telemetry_metrics
+            )
+
+            # Assert - Added properties were added.
+            first_call_args = telemetry_client.track_event.call_args_list[0][0]
+            first_properties = first_call_args[1]
+            expected_answer = (
+                "BaseCamp: You can use a damp rag to clean around the Power Pack"
+            )
+            first_metrics = first_call_args[2]
+
+            self.assertEqual(2, telemetry_client.track_event.call_count)
+            self.assertEqual(3, len(first_call_args))
+            self.assertEqual("QnaMessage", first_call_args[0])
+            self.assertEqual(6, len(first_properties))
+            self.assertTrue("knowledgeBaseId" in first_properties)
+            self.assertEqual("my_important_value", first_properties["knowledgeBaseId"])
+            self.assertTrue("matchedQuestion" in first_properties)
+            self.assertEqual("my_important_value2", first_properties["matchedQuestion"])
+            self.assertTrue("questionId" in first_properties)
+            self.assertTrue("answer" in first_properties)
+            self.assertEqual(expected_answer, first_properties["answer"])
+            self.assertTrue("articleFound" in first_properties)
+            self.assertTrue("my_important_property" in first_properties)
+            self.assertEqual(
+                "my_important_value", first_properties["my_important_property"]
+            )
+
+            self.assertEqual(1, len(first_metrics))
+            self.assertTrue("score" in first_metrics)
+            self.assertEqual(3.14159, first_metrics["score"])
+
+            # Assert - Validate we didn't break QnA functionality.
+            self.assertIsNotNone(results)
+            self.assertEqual(1, len(results))
+            self.assertEqual(expected_answer, results[0].answer)
+            self.assertEqual("Editorial", results[0].source)
+
+    async def test_call_train(self):
+        feedback_records = []
+
+        feedback1 = FeedbackRecord(
+            qna_id=1, user_id="test", user_question="How are you?"
+        )
+
+        feedback2 = FeedbackRecord(qna_id=2, user_id="test", user_question="What up??")
+
+        feedback_records.extend([feedback1, feedback2])
+
+        with patch.object(
+            QnAMaker, "call_train", return_value=None
+        ) as mocked_call_train:
+            qna = QnAMaker(QnaApplicationTest.tests_endpoint)
+            qna.call_train(feedback_records)
+
+            mocked_call_train.assert_called_once_with(feedback_records)
+
+    async def test_should_filter_low_score_variation(self):
+        options = QnAMakerOptions(top=5)
+        qna = QnAMaker(QnaApplicationTest.tests_endpoint, options)
+        question: str = "Q11"
+        context = QnaApplicationTest._get_context(question, TestAdapter())
+        response_json = QnaApplicationTest._get_json_for_file("TopNAnswer.json")
+
+        with patch(
+            "aiohttp.ClientSession.post",
+            return_value=aiounittest.futurized(response_json),
+        ):
+            results = await qna.get_answers(context)
+            self.assertEqual(4, len(results), "Should have received 4 answers.")
+
+            filtered_results = qna.get_low_score_variation(results)
+            self.assertEqual(
+                3,
+                len(filtered_results),
+                "Should have 3 filtered answers after low score variation.",
+            )
+
+    async def test_should_answer_with_is_test_true(self):
+        options = QnAMakerOptions(top=1, is_test=True)
+        qna = QnAMaker(QnaApplicationTest.tests_endpoint)
+        question: str = "Q11"
+        context = QnaApplicationTest._get_context(question, TestAdapter())
+        response_json = QnaApplicationTest._get_json_for_file(
+            "QnaMaker_IsTest_true.json"
+        )
+
+        with patch(
+            "aiohttp.ClientSession.post",
+            return_value=aiounittest.futurized(response_json),
+        ):
+            results = await qna.get_answers(context, options=options)
+            self.assertEqual(0, len(results), "Should have received zero answer.")
+
+    async def test_should_answer_with_ranker_type_question_only(self):
+        options = QnAMakerOptions(top=1, ranker_type="QuestionOnly")
+        qna = QnAMaker(QnaApplicationTest.tests_endpoint)
+        question: str = "Q11"
+        context = QnaApplicationTest._get_context(question, TestAdapter())
+        response_json = QnaApplicationTest._get_json_for_file(
+            "QnaMaker_RankerType_QuestionOnly.json"
+        )
+
+        with patch(
+            "aiohttp.ClientSession.post",
+            return_value=aiounittest.futurized(response_json),
+        ):
+            results = await qna.get_answers(context, options=options)
+            self.assertEqual(2, len(results), "Should have received two answers.")
+
+    async def test_should_answer_with_prompts(self):
+        options = QnAMakerOptions(top=2)
+        qna = QnAMaker(QnaApplicationTest.tests_endpoint, options)
+        question: str = "how do I clean the stove?"
+        turn_context = QnaApplicationTest._get_context(question, TestAdapter())
+        response_json = QnaApplicationTest._get_json_for_file("AnswerWithPrompts.json")
+
+        with patch(
+            "aiohttp.ClientSession.post",
+            return_value=aiounittest.futurized(response_json),
+        ):
+            results = await qna.get_answers(turn_context, options)
+            self.assertEqual(1, len(results), "Should have received 1 answers.")
+            self.assertEqual(
+                1, len(results[0].context.prompts), "Should have received 1 prompt."
+            )
+
+    async def test_should_answer_with_high_score_provided_context(self):
+        qna = QnAMaker(QnaApplicationTest.tests_endpoint)
+        question: str = "where can I buy?"
+        context = QnARequestContext(
+            previous_qna_id=5, previous_user_query="how do I clean the stove?"
+        )
+        options = QnAMakerOptions(top=2, qna_id=55, context=context)
+        turn_context = QnaApplicationTest._get_context(question, TestAdapter())
+        response_json = QnaApplicationTest._get_json_for_file(
+            "AnswerWithHighScoreProvidedContext.json"
+        )
+
+        with patch(
+            "aiohttp.ClientSession.post",
+            return_value=aiounittest.futurized(response_json),
+        ):
+            results = await qna.get_answers(turn_context, options)
+            self.assertEqual(1, len(results), "Should have received 1 answers.")
+            self.assertEqual(1, results[0].score, "Score should be high.")
+
+    async def test_should_answer_with_high_score_provided_qna_id(self):
+        qna = QnAMaker(QnaApplicationTest.tests_endpoint)
+        question: str = "where can I buy?"
+
+        options = QnAMakerOptions(top=2, qna_id=55)
+        turn_context = QnaApplicationTest._get_context(question, TestAdapter())
+        response_json = QnaApplicationTest._get_json_for_file(
+            "AnswerWithHighScoreProvidedContext.json"
+        )
+
+        with patch(
+            "aiohttp.ClientSession.post",
+            return_value=aiounittest.futurized(response_json),
+        ):
+            results = await qna.get_answers(turn_context, options)
+            self.assertEqual(1, len(results), "Should have received 1 answers.")
+            self.assertEqual(1, results[0].score, "Score should be high.")
+
+    async def test_should_answer_with_low_score_without_provided_context(self):
+        qna = QnAMaker(QnaApplicationTest.tests_endpoint)
+        question: str = "where can I buy?"
+        options = QnAMakerOptions(top=2, context=None)
+
+        turn_context = QnaApplicationTest._get_context(question, TestAdapter())
+        response_json = QnaApplicationTest._get_json_for_file(
+            "AnswerWithLowScoreProvidedWithoutContext.json"
+        )
+
+        with patch(
+            "aiohttp.ClientSession.post",
+            return_value=aiounittest.futurized(response_json),
+        ):
+            results = await qna.get_answers(turn_context, options)
+            self.assertEqual(
+                2, len(results), "Should have received more than one answers."
+            )
+            self.assertEqual(True, results[0].score < 1, "Score should be low.")
+
+    async def test_low_score_variation(self):
+        qna = QnAMaker(QnaApplicationTest.tests_endpoint)
+        options = QnAMakerOptions(top=5, context=None)
+
+        turn_context = QnaApplicationTest._get_context("Q11", TestAdapter())
+        response_json = QnaApplicationTest._get_json_for_file(
+            "QnaMaker_TopNAnswer.json"
+        )
+
+        # active learning enabled
+        with patch(
+            "aiohttp.ClientSession.post",
+            return_value=aiounittest.futurized(response_json),
+        ):
+            results = await qna.get_answers(turn_context, options)
+            self.assertIsNotNone(results)
+            self.assertEqual(4, len(results), "should get four results")
+
+            filtered_results = qna.get_low_score_variation(results)
+            self.assertIsNotNone(filtered_results)
+            self.assertEqual(3, len(filtered_results), "should get three results")
+
+        # active learning disabled
+        turn_context = QnaApplicationTest._get_context("Q11", TestAdapter())
+        response_json = QnaApplicationTest._get_json_for_file(
+            "QnaMaker_TopNAnswer_DisableActiveLearning.json"
+        )
+
+        with patch(
+            "aiohttp.ClientSession.post",
+            return_value=aiounittest.futurized(response_json),
+        ):
+            results = await qna.get_answers(turn_context, options)
+            self.assertIsNotNone(results)
+            self.assertEqual(4, len(results), "should get four results")
+
+            filtered_results = qna.get_low_score_variation(results)
+            self.assertIsNotNone(filtered_results)
+            self.assertEqual(3, len(filtered_results), "should get three results")
+
+    @classmethod
+    async def _get_service_result(
+        cls,
+        utterance: str,
+        response_file: str,
+        bot_adapter: BotAdapter = TestAdapter(),
+        options: QnAMakerOptions = None,
+    ) -> [dict]:
+        response_json = QnaApplicationTest._get_json_for_file(response_file)
+
+        qna = QnAMaker(QnaApplicationTest.tests_endpoint)
+        context = QnaApplicationTest._get_context(utterance, bot_adapter)
+
+        with patch(
+            "aiohttp.ClientSession.post",
+            return_value=aiounittest.futurized(response_json),
+        ):
+            result = await qna.get_answers(context, options)
+
+            return result
+
+    @classmethod
+    async def _get_service_result_raw(
+        cls,
+        utterance: str,
+        response_file: str,
+        bot_adapter: BotAdapter = TestAdapter(),
+        options: QnAMakerOptions = None,
+    ) -> [dict]:
+        response_json = QnaApplicationTest._get_json_for_file(response_file)
+
+        qna = QnAMaker(QnaApplicationTest.tests_endpoint)
+        context = QnaApplicationTest._get_context(utterance, bot_adapter)
+
+        with patch(
+            "aiohttp.ClientSession.post",
+            return_value=aiounittest.futurized(response_json),
+        ):
+            result = await qna.get_answers_raw(context, options)
+
+            return result
+
+    @classmethod
+    def _get_json_for_file(cls, response_file: str) -> object:
+        curr_dir = path.dirname(path.abspath(__file__))
+        response_path = path.join(curr_dir, "test_data", response_file)
+
+        with open(response_path, "r", encoding="utf-8-sig") as file:
+            response_str = file.read()
+        response_json = json.loads(response_str)
+
+        return response_json
+
+    @staticmethod
+    def _get_context(question: str, bot_adapter: BotAdapter) -> TurnContext:
+        test_adapter = bot_adapter or TestAdapter()
+        activity = Activity(
+            type=ActivityTypes.message,
+            text=question,
+            conversation=ConversationAccount(),
+            recipient=ChannelAccount(),
+            from_property=ChannelAccount(),
+        )
+
+        return TurnContext(test_adapter, activity)
+
+    class OverrideTelemetry(QnAMaker):
+        def __init__(  # pylint: disable=useless-super-delegation
+            self,
+            endpoint: QnAMakerEndpoint,
+            options: QnAMakerOptions,
+            http_client: ClientSession,
+            telemetry_client: BotTelemetryClient,
+            log_personal_information: bool,
+        ):
+            super().__init__(
+                endpoint,
+                options,
+                http_client,
+                telemetry_client,
+                log_personal_information,
+            )
+
+        async def on_qna_result(  # pylint: disable=unused-argument
+            self,
+            query_results: [QueryResult],
+            turn_context: TurnContext,
+            telemetry_properties: Dict[str, str] = None,
+            telemetry_metrics: Dict[str, float] = None,
+        ):
+            properties = telemetry_properties or {}
+
+            # get_answers overrides derived class
+            properties["my_important_property"] = "my_important_value"
+
+            # Log event
+            self.telemetry_client.track_event(
+                QnATelemetryConstants.qna_message_event, properties
+            )
+
+            # Create 2nd event.
+            second_event_properties = {"my_important_property2": "my_important_value2"}
+            self.telemetry_client.track_event(
+                "my_second_event", second_event_properties
+            )
+
+    class OverrideFillTelemetry(QnAMaker):
+        def __init__(  # pylint: disable=useless-super-delegation
+            self,
+            endpoint: QnAMakerEndpoint,
+            options: QnAMakerOptions,
+            http_client: ClientSession,
+            telemetry_client: BotTelemetryClient,
+            log_personal_information: bool,
+        ):
+            super().__init__(
+                endpoint,
+                options,
+                http_client,
+                telemetry_client,
+                log_personal_information,
+            )
+
+        async def on_qna_result(
+            self,
+            query_results: [QueryResult],
+            turn_context: TurnContext,
+            telemetry_properties: Dict[str, str] = None,
+            telemetry_metrics: Dict[str, float] = None,
+        ):
+            event_data = await self.fill_qna_event(
+                query_results, turn_context, telemetry_properties, telemetry_metrics
+            )
+
+            # Add my property.
+            event_data.properties.update(
+                {"my_important_property": "my_important_value"}
+            )
+
+            # Log QnaMessage event.
+            self.telemetry_client.track_event(
+                QnATelemetryConstants.qna_message_event,
+                event_data.properties,
+                event_data.metrics,
+            )
+
+            # Create second event.
+            second_event_properties: Dict[str, str] = {
+                "my_important_property2": "my_important_value2"
+            }
+
+            self.telemetry_client.track_event("MySecondEvent", second_event_properties)
diff --git a/libraries/botbuilder-ai/tests/requirements.txt b/libraries/botbuilder-ai/tests/requirements.txt
new file mode 100644
index 000000000..93fc8e8ff
--- /dev/null
+++ b/libraries/botbuilder-ai/tests/requirements.txt
@@ -0,0 +1 @@
+aioresponses==0.6.3
\ No newline at end of file
diff --git a/libraries/botbuilder-applicationinsights/README.rst b/libraries/botbuilder-applicationinsights/README.rst
index 43f6046da..6e5c9c0df 100644
--- a/libraries/botbuilder-applicationinsights/README.rst
+++ b/libraries/botbuilder-applicationinsights/README.rst
@@ -3,8 +3,8 @@
 BotBuilder-ApplicationInsights SDK for Python
 =============================================
 
-.. image:: https://fuselabs.visualstudio.com/SDK_v4/_apis/build/status/Python/SDK_v4-Python-CI?branchName=master
-   :target:  https://fuselabs.visualstudio.com/SDK_v4/_apis/build/status/Python/SDK_v4-Python-CI
+.. image:: https://dev.azure.com/FuseLabs/SDK_v4/_apis/build/status/Python/Python-CI-PR-yaml?branchName=master
+   :target:  https://dev.azure.com/FuseLabs/SDK_v4/_apis/build/status/Python/Python-CI-PR-yaml?branchName=master
    :align: right
    :alt: Azure DevOps status for master branch
 .. image:: https://badge.fury.io/py/botbuilder-applicationinsights.svg
diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/about.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/about.py
index bae8313bf..841b3ba9a 100644
--- a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/about.py
+++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/about.py
@@ -6,7 +6,7 @@
 
 __title__ = "botbuilder-applicationinsights"
 __version__ = (
-    os.environ["packageVersion"] if "packageVersion" in os.environ else "4.4.0b1"
+    os.environ["packageVersion"] if "packageVersion" in os.environ else "4.12.0"
 )
 __uri__ = "https://www.github.com/Microsoft/botbuilder-python"
 __author__ = "Microsoft"
diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/application_insights_telemetry_client.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/application_insights_telemetry_client.py
index ae660eb7b..39b1eac3a 100644
--- a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/application_insights_telemetry_client.py
+++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/application_insights_telemetry_client.py
@@ -38,13 +38,18 @@ def __init__(
         instrumentation_key: str,
         telemetry_client: TelemetryClient = None,
         telemetry_processor: Callable[[object, object], bool] = None,
+        client_queue_size: int = None,
     ):
         self._instrumentation_key = instrumentation_key
+
         self._client = (
             telemetry_client
             if telemetry_client is not None
             else TelemetryClient(self._instrumentation_key)
         )
+        if client_queue_size:
+            self._client.channel.queue.max_queue_length = client_queue_size
+
         # Telemetry Processor
         processor = (
             telemetry_processor
@@ -63,13 +68,18 @@ def track_pageview(
     ) -> None:
         """
         Send information about the page viewed in the application (a web page for instance).
+
         :param name: the name of the page that was viewed.
+        :type name: str
         :param url: the URL of the page that was viewed.
+        :type url: str
         :param duration: the duration of the page view in milliseconds. (defaults to: 0)
-        :param properties: the set of custom properties the client wants attached to this data item.
-         (defaults to: None)
-        :param measurements: the set of custom measurements the client wants to attach to this data item.
-         (defaults to: None)
+        :duration: int
+        :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)
+        :type properties: :class:`typing.Dict[str, object]`
+        :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to:
+         None)
+        :type measurements: :class:`typing.Dict[str, object]`
         """
         self._client.track_pageview(name, url, duration, properties, measurements)
 
@@ -83,13 +93,15 @@ def track_exception(
     ) -> None:
         """
         Send information about a single exception that occurred in the application.
+
         :param exception_type: the type of the exception that was thrown.
         :param value: the exception that the client wants to send.
         :param trace: the traceback information as returned by :func:`sys.exc_info`.
-        :param properties: the set of custom properties the client wants attached to this data item.
-         (defaults to: None)
-        :param measurements: the set of custom measurements the client wants to attach to this data item.
-         (defaults to: None)
+        :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)
+        :type properties: :class:`typing.Dict[str, object]`
+        :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to:
+         None)
+        :type measurements: :class:`typing.Dict[str, object]`
         """
         self._client.track_exception(
             exception_type, value, trace, properties, measurements
@@ -103,11 +115,14 @@ def track_event(
     ) -> None:
         """
         Send information about a single event that has occurred in the context of the application.
+
         :param name: the data to associate to this event.
-        :param properties: the set of custom properties the client wants attached to this data item.
-         (defaults to: None)
-        :param measurements: the set of custom measurements the client wants to attach to this data item.
-         (defaults to: None)
+        :type name: str
+        :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)
+        :type properties: :class:`typing.Dict[str, object]`
+        :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to:
+         None)
+        :type measurements: :class:`typing.Dict[str, object]`
         """
         self._client.track_event(name, properties=properties, measurements=measurements)
 
@@ -124,18 +139,25 @@ def track_metric(
     ) -> NotImplemented:
         """
         Send information about a single metric data point that was captured for the application.
+
         :param name: The name of the metric that was captured.
+        :type name: str
         :param value: The value of the metric that was captured.
+        :type value: float
         :param tel_type: The type of the metric. (defaults to: TelemetryDataPointType.aggregation`)
         :param count: the number of metrics that were aggregated into this data point. (defaults to: None)
-        :param min_val: the minimum of all metrics collected that were aggregated into this data point.
-         (defaults to: None)
-        :param max_val: the maximum of all metrics collected that were aggregated into this data point.
-         (defaults to: None)
+        :type count: int
+        :param min_val: the minimum of all metrics collected that were aggregated into this data point. (defaults to:
+         None)
+        :type min_val: float
+        :param max_val: the maximum of all metrics collected that were aggregated into this data point. (defaults to:
+         None)
+        :type max_val: float
         :param std_dev: the standard deviation of all metrics collected that were aggregated into this data point.
          (defaults to: None)
-        :param properties: the set of custom properties the client wants attached to this data item.
-         (defaults to: None)
+        :type std_dev: float
+        :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)
+        :type properties: :class:`typing.Dict[str, object]`
         """
         self._client.track_metric(
             name, value, tel_type, count, min_val, max_val, std_dev, properties
@@ -146,8 +168,11 @@ def track_trace(
     ):
         """
         Sends a single trace statement.
+
         :param name: the trace statement.
+        :type name: str
         :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)
+        :type properties: :class:`typing.Dict[str, object]`
         :param severity: the severity level of this trace, one of DEBUG, INFO, WARNING, ERROR, CRITICAL
         """
         self._client.track_trace(name, properties, severity)
@@ -167,19 +192,29 @@ def track_request(
     ):
         """
         Sends a single request that was captured for the application.
+
         :param name: The name for this request. All requests with the same name will be grouped together.
+        :type name: str
         :param url: The actual URL for this request (to show in individual request instances).
+        :type url: str
         :param success: True if the request ended in success, False otherwise.
+        :type success: bool
         :param start_time: the start time of the request. The value should look the same as the one returned by
-         :func:`datetime.isoformat()` (defaults to: None)
+         :func:`datetime.isoformat`. (defaults to: None)
+        :type start_time: str
         :param duration: the number of milliseconds that this request lasted. (defaults to: None)
+        :type duration: int
         :param response_code: the response code that this request returned. (defaults to: None)
+        :type response_code: str
         :param http_method: the HTTP method that triggered this request. (defaults to: None)
-        :param properties: the set of custom properties the client wants attached to this data item.
-         (defaults to: None)
-        :param measurements: the set of custom measurements the client wants to attach to this data item.
-         (defaults to: None)
+        :type http_method: str
+        :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)
+        :type properties: :class:`typing.Dict[str, object]`
+        :param measurements: the set of custom measurements the client wants to attach to this data item. (defaults to:
+         None)
+        :type measurements: :class:`typing.Dict[str, object]`
         :param request_id: the id for this request. If None, a new uuid will be generated. (defaults to: None)
+        :type request_id: str
         """
         self._client.track_request(
             name,
@@ -209,25 +244,35 @@ def track_dependency(
     ):
         """
         Sends a single dependency telemetry that was captured for the application.
+
         :param name: the name of the command initiated with this dependency call. Low cardinality value.
          Examples are stored procedure name and URL path template.
-        :param data: the command initiated by this dependency call.
-         Examples are SQL statement and HTTP URL with all query parameters.
+        :type name: str
+        :param data: the command initiated by this dependency call. Examples are SQL statement and HTTP URL with all
+         query parameters.
+        :type data: str
         :param type_name: the dependency type name. Low cardinality value for logical grouping of dependencies and
          interpretation of other fields like commandName and resultCode. Examples are SQL, Azure table, and HTTP.
           (default to: None)
-        :param target: the target site of a dependency call. Examples are server name, host address.
-         (default to: None)
-        :param duration: the number of milliseconds that this dependency call lasted.
-         (defaults to: None)
-        :param success: true if the dependency call ended in success, false otherwise.
-         (defaults to: None)
+        :type type_name: str
+        :param target: the target site of a dependency call. Examples are server name, host address. (default to: None)
+        :type target: str
+        :param duration: the number of milliseconds that this dependency call lasted. (defaults to: None)
+        :type duration: int
+        :param success: true if the dependency call ended in success, false otherwise. (defaults to: None)
+        :type success: bool
         :param result_code: the result code of a dependency call. Examples are SQL error code and HTTP status code.
          (defaults to: None)
-        :param properties: the set of custom properties the client wants attached to this data item. (defaults to: None)
+        :type result_code: str
+        :param properties: the set of custom properties the client wants attached to this data item.
+         (defaults to: None)
+        :type properties: :class:`typing.Dict[str, object]`
         :param measurements: the set of custom measurements the client wants to attach to this data item.
          (defaults to: None)
-        :param id: the id for this dependency call. If None, a new uuid will be generated. (defaults to: None)
+        :type measurements: :class:`typing.Dict[str, object]`
+        :param dependency_id: the id for this dependency call. If None, a new uuid will be generated.
+         (defaults to: None)
+        :type dependency_id: str
         """
         self._client.track_dependency(
             name,
diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/bot_telemetry_middleware.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/bot_telemetry_middleware.py
index 10b4b9b20..4508dcef1 100644
--- a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/bot_telemetry_middleware.py
+++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/bot_telemetry_middleware.py
@@ -10,10 +10,10 @@
 
 
 def retrieve_bot_body():
-    """ retrieve_bot_body
+    """
     Retrieve the POST body text from temporary cache.
-    The POST body corresponds with the thread id and should resides in
-    cache just for lifetime of request.
+
+    The POST body corresponds to the thread ID and must reside in the cache just for the lifetime of the request.
     """
 
     result = _REQUEST_BODIES.get(current_thread().ident, None)
@@ -22,15 +22,17 @@ def retrieve_bot_body():
 
 class BotTelemetryMiddleware:
     """
-    Save off the POST body to later populate bot-specific properties to
-    add to Application Insights.
+    Save off the POST body to later populate bot-specific properties to add to Application Insights.
 
     Example activating MIDDLEWARE in Django settings:
-    MIDDLEWARE = [
-        # Ideally add somewhere near top
-        'botbuilder.applicationinsights.django.BotTelemetryMiddleware',
-        ...
-        ]
+
+    .. code-block:: python
+
+        MIDDLEWARE = [
+            # Ideally add somewhere near top
+            'botbuilder.applicationinsights.django.BotTelemetryMiddleware',
+            ...
+            ]
     """
 
     def __init__(self, get_response):
diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/common.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/common.py
index a7f61588c..0479b3e22 100644
--- a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/common.py
+++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/common.py
@@ -11,7 +11,6 @@
     SynchronousQueue,
     TelemetryChannel,
 )
-from django.conf import settings
 
 from ..processor.telemetry_processor import TelemetryProcessor
 from .django_telemetry_processor import DjangoTelemetryProcessor
@@ -34,6 +33,8 @@
 
 
 def load_settings():
+    from django.conf import settings  # pylint: disable=import-outside-toplevel
+
     if hasattr(settings, "APPLICATION_INSIGHTS"):
         config = settings.APPLICATION_INSIGHTS
     elif hasattr(settings, "APPLICATIONINSIGHTS"):
diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/logging.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/logging.py
index dc36a362b..78e651aa7 100644
--- a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/logging.py
+++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/django/logging.py
@@ -8,7 +8,7 @@
 class LoggingHandler(logging.LoggingHandler):
     """This class is a LoggingHandler that uses the same settings as the Django middleware to configure
     the telemetry client.  This can be referenced from LOGGING in your Django settings.py file.  As an
-    example, this code would send all Django log messages--WARNING and up--to Application Insights:
+    example, this code would send all Django log messages, WARNING and up, to Application Insights:
 
     .. code:: python
 
diff --git a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/processor/telemetry_processor.py b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/processor/telemetry_processor.py
index 7a15acb16..f03588c82 100644
--- a/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/processor/telemetry_processor.py
+++ b/libraries/botbuilder-applicationinsights/botbuilder/applicationinsights/processor/telemetry_processor.py
@@ -1,7 +1,9 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License.
+import base64
 import json
 from abc import ABC, abstractmethod
+from _sha256 import sha256
 
 
 class TelemetryProcessor(ABC):
@@ -11,8 +13,9 @@ class TelemetryProcessor(ABC):
     def activity_json(self) -> json:
         """Retrieve the request body as json (Activity)."""
         body_text = self.get_request_body()
-        body = json.loads(body_text) if body_text is not None else None
-        return body
+        if body_text:
+            return body_text if isinstance(body_text, dict) else json.loads(body_text)
+        return None
 
     @abstractmethod
     def can_process(self) -> bool:
@@ -67,15 +70,34 @@ def __call__(self, data, context) -> bool:
         conversation = (
             post_data["conversation"] if "conversation" in post_data else None
         )
-        conversation_id = conversation["id"] if "id" in conversation else None
+
+        session_id = ""
+        if "id" in conversation:
+            conversation_id = conversation["id"]
+            session_id = base64.b64encode(
+                sha256(conversation_id.encode("utf-8")).digest()
+            ).decode()
+
+        # Set the user id on the Application Insights telemetry item.
         context.user.id = channel_id + user_id
-        context.session.id = conversation_id
 
-        # Additional bot-specific properties
+        # Set the session id on the Application Insights telemetry item.
+        # Hashed ID is used due to max session ID length for App Insights session Id
+        context.session.id = session_id
+
+        # Set the activity id:
+        # https://github.com/Microsoft/botframework-obi/blob/master/botframework-activity/botframework-activity.md#id
         if "id" in post_data:
             data.properties["activityId"] = post_data["id"]
+
+        # Set the channel id:
+        # https://github.com/Microsoft/botframework-obi/blob/master/botframework-activity/botframework-activity.md#channel-id
         if "channelId" in post_data:
             data.properties["channelId"] = post_data["channelId"]
+
+        # Set the activity type:
+        # https://github.com/Microsoft/botframework-obi/blob/master/botframework-activity/botframework-activity.md#type
         if "type" in post_data:
             data.properties["activityType"] = post_data["type"]
+
         return True
diff --git a/libraries/botbuilder-applicationinsights/requirements.txt b/libraries/botbuilder-applicationinsights/requirements.txt
index d4f4ea13b..6c59cce95 100644
--- a/libraries/botbuilder-applicationinsights/requirements.txt
+++ b/libraries/botbuilder-applicationinsights/requirements.txt
@@ -1,3 +1,3 @@
-msrest>=0.6.6
-botbuilder-core>=4.4.0b1
-aiounittest>=1.1.0
\ No newline at end of file
+msrest==0.6.10
+botbuilder-core==4.12.0
+aiounittest==1.3.0
\ No newline at end of file
diff --git a/libraries/botbuilder-applicationinsights/setup.py b/libraries/botbuilder-applicationinsights/setup.py
index b48bf73cf..0f2706e9f 100644
--- a/libraries/botbuilder-applicationinsights/setup.py
+++ b/libraries/botbuilder-applicationinsights/setup.py
@@ -5,16 +5,16 @@
 from setuptools import setup
 
 REQUIRES = [
-    "applicationinsights>=0.11.9",
-    "botbuilder-schema>=4.4.0b1",
-    "botframework-connector>=4.4.0b1",
-    "botbuilder-core>=4.4.0b1",
+    "applicationinsights==0.11.9",
+    "botbuilder-schema==4.12.0",
+    "botframework-connector==4.12.0",
+    "botbuilder-core==4.12.0",
 ]
 TESTS_REQUIRES = [
-    "aiounittest>=1.1.0",
-    "django>=2.2",  # For samples
-    "djangorestframework>=3.9.2",  # For samples
-    "flask>=1.0.2",  # For samples
+    "aiounittest==1.3.0",
+    "django==2.2.6",  # For samples
+    "djangorestframework==3.10.3",  # For samples
+    "flask==1.1.1",  # For samples
 ]
 
 root = os.path.abspath(os.path.dirname(__file__))
@@ -47,6 +47,7 @@
         "botbuilder.applicationinsights",
         "botbuilder.applicationinsights.django",
         "botbuilder.applicationinsights.flask",
+        "botbuilder.applicationinsights.processor",
     ],
     install_requires=REQUIRES + TESTS_REQUIRES,
     tests_require=TESTS_REQUIRES,
@@ -56,7 +57,7 @@
         "Intended Audience :: Developers",
         "License :: OSI Approved :: MIT License",
         "Operating System :: OS Independent",
-        "Development Status :: 3 - Alpha",
+        "Development Status :: 5 - Production/Stable",
         "Topic :: Scientific/Engineering :: Artificial Intelligence",
     ],
 )
diff --git a/libraries/botbuilder-applicationinsights/tests/test_telemetry_waterfall.py b/libraries/botbuilder-applicationinsights/tests/test_telemetry_waterfall.py
index c42adee2f..31f10527c 100644
--- a/libraries/botbuilder-applicationinsights/tests/test_telemetry_waterfall.py
+++ b/libraries/botbuilder-applicationinsights/tests/test_telemetry_waterfall.py
@@ -1,178 +1,209 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from unittest.mock import patch
-from typing import Dict
-import aiounittest
-from botbuilder.core.adapters import TestAdapter, TestFlow
-from botbuilder.schema import Activity
-from botbuilder.core import (
-    ConversationState,
-    MemoryStorage,
-    TurnContext,
-    NullTelemetryClient,
-)
-from botbuilder.dialogs import (
-    Dialog,
-    DialogSet,
-    WaterfallDialog,
-    DialogTurnResult,
-    DialogTurnStatus,
-)
-
-BEGIN_MESSAGE = Activity()
-BEGIN_MESSAGE.text = "begin"
-BEGIN_MESSAGE.type = "message"
-
-
-class TelemetryWaterfallTests(aiounittest.AsyncTestCase):
-    def test_none_telemetry_client(self):
-        # arrange
-        dialog = WaterfallDialog("myId")
-        # act
-        dialog.telemetry_client = None
-        # assert
-        self.assertEqual(type(dialog.telemetry_client), NullTelemetryClient)
-
-    @patch("botbuilder.applicationinsights.ApplicationInsightsTelemetryClient")
-    async def test_execute_sequence_waterfall_steps(  # pylint: disable=invalid-name
-        self, MockTelemetry
-    ):
-        # arrange
-
-        # Create new ConversationState with MemoryStorage and register the state as middleware.
-        convo_state = ConversationState(MemoryStorage())
-        telemetry = MockTelemetry()
-
-        # Create a DialogState property, DialogSet and register the WaterfallDialog.
-        dialog_state = convo_state.create_property("dialogState")
-        dialogs = DialogSet(dialog_state)
-
-        async def step1(step) -> DialogTurnResult:
-            await step.context.send_activity("bot responding.")
-            return Dialog.end_of_turn
-
-        async def step2(step) -> DialogTurnResult:
-            await step.context.send_activity("ending WaterfallDialog.")
-            return Dialog.end_of_turn
-
-        # act
-
-        my_dialog = WaterfallDialog("test", [step1, step2])
-        my_dialog.telemetry_client = telemetry
-        dialogs.add(my_dialog)
-
-        # Initialize TestAdapter
-        async def exec_test(turn_context: TurnContext) -> None:
-
-            dialog_context = await dialogs.create_context(turn_context)
-            results = await dialog_context.continue_dialog()
-            if results.status == DialogTurnStatus.Empty:
-                await dialog_context.begin_dialog("test")
-            else:
-                if results.status == DialogTurnStatus.Complete:
-                    await turn_context.send_activity(results.result)
-
-            await convo_state.save_changes(turn_context)
-
-        adapt = TestAdapter(exec_test)
-
-        test_flow = TestFlow(None, adapt)
-        tf2 = await test_flow.send(BEGIN_MESSAGE)
-        tf3 = await tf2.assert_reply("bot responding.")
-        tf4 = await tf3.send("continue")
-        await tf4.assert_reply("ending WaterfallDialog.")
-
-        # assert
-
-        telemetry_calls = [
-            ("WaterfallStart", {"DialogId": "test"}),
-            ("WaterfallStep", {"DialogId": "test", "StepName": "Step1of2"}),
-            ("WaterfallStep", {"DialogId": "test", "StepName": "Step2of2"}),
-        ]
-        self.assert_telemetry_calls(telemetry, telemetry_calls)
-
-    @patch("botbuilder.applicationinsights.ApplicationInsightsTelemetryClient")
-    async def test_ensure_end_dialog_called(
-        self, MockTelemetry
-    ):  # pylint: disable=invalid-name
-        # arrange
-
-        # Create new ConversationState with MemoryStorage and register the state as middleware.
-        convo_state = ConversationState(MemoryStorage())
-        telemetry = MockTelemetry()
-
-        # Create a DialogState property, DialogSet and register the WaterfallDialog.
-        dialog_state = convo_state.create_property("dialogState")
-        dialogs = DialogSet(dialog_state)
-
-        async def step1(step) -> DialogTurnResult:
-            await step.context.send_activity("step1 response")
-            return Dialog.end_of_turn
-
-        async def step2(step) -> DialogTurnResult:
-            await step.context.send_activity("step2 response")
-            return Dialog.end_of_turn
-
-        # act
-
-        my_dialog = WaterfallDialog("test", [step1, step2])
-        my_dialog.telemetry_client = telemetry
-        dialogs.add(my_dialog)
-
-        # Initialize TestAdapter
-        async def exec_test(turn_context: TurnContext) -> None:
-
-            dialog_context = await dialogs.create_context(turn_context)
-            await dialog_context.continue_dialog()
-            if not turn_context.responded:
-                await dialog_context.begin_dialog("test", None)
-            await convo_state.save_changes(turn_context)
-
-        adapt = TestAdapter(exec_test)
-
-        test_flow = TestFlow(None, adapt)
-        tf2 = await test_flow.send(BEGIN_MESSAGE)
-        tf3 = await tf2.assert_reply("step1 response")
-        tf4 = await tf3.send("continue")
-        tf5 = await tf4.assert_reply("step2 response")
-        await tf5.send(
-            "Should hit end of steps - this will restart the dialog and trigger COMPLETE event"
-        )
-        # assert
-        telemetry_calls = [
-            ("WaterfallStart", {"DialogId": "test"}),
-            ("WaterfallStep", {"DialogId": "test", "StepName": "Step1of2"}),
-            ("WaterfallStep", {"DialogId": "test", "StepName": "Step2of2"}),
-            ("WaterfallComplete", {"DialogId": "test"}),
-            ("WaterfallStart", {"DialogId": "test"}),
-            ("WaterfallStep", {"DialogId": "test", "StepName": "Step1of2"}),
-        ]
-        print(str(telemetry.track_event.call_args_list))
-        self.assert_telemetry_calls(telemetry, telemetry_calls)
-
-    def assert_telemetry_call(
-        self, telemetry_mock, index: int, event_name: str, props: Dict[str, str]
-    ) -> None:
-        # pylint: disable=unused-variable
-        args, kwargs = telemetry_mock.track_event.call_args_list[index]
-        self.assertEqual(args[0], event_name)
-
-        for key, val in props.items():
-            self.assertTrue(
-                key in args[1],
-                msg=f"Could not find value {key} in {args[1]} for index {index}",
-            )
-            self.assertTrue(isinstance(args[1], dict))
-            self.assertTrue(val == args[1][key])
-
-    def assert_telemetry_calls(self, telemetry_mock, calls) -> None:
-        index = 0
-        for event_name, props in calls:
-            self.assert_telemetry_call(telemetry_mock, index, event_name, props)
-            index += 1
-        if index != len(telemetry_mock.track_event.call_args_list):
-            self.assertTrue(  # pylint: disable=redundant-unittest-assert
-                False,
-                f"Found {len(telemetry_mock.track_event.call_args_list)} calls, testing for {index + 1}",
-            )
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from unittest.mock import create_autospec, MagicMock
+from typing import Dict
+import aiounittest
+from botbuilder.core.adapters import TestAdapter, TestFlow
+from botbuilder.schema import Activity
+from botbuilder.core import (
+    ConversationState,
+    MemoryStorage,
+    TurnContext,
+    NullTelemetryClient,
+)
+from botbuilder.dialogs import (
+    Dialog,
+    DialogInstance,
+    DialogReason,
+    DialogSet,
+    WaterfallDialog,
+    DialogTurnResult,
+    DialogTurnStatus,
+)
+
+BEGIN_MESSAGE = Activity()
+BEGIN_MESSAGE.text = "begin"
+BEGIN_MESSAGE.type = "message"
+
+MOCK_TELEMETRY = "botbuilder.applicationinsights.ApplicationInsightsTelemetryClient"
+
+
+class TelemetryWaterfallTests(aiounittest.AsyncTestCase):
+    def test_none_telemetry_client(self):
+        # arrange
+        dialog = WaterfallDialog("myId")
+        # act
+        dialog.telemetry_client = None
+        # assert
+        self.assertEqual(type(dialog.telemetry_client), NullTelemetryClient)
+
+    async def test_execute_sequence_waterfall_steps(self):
+        # arrange
+
+        # Create new ConversationState with MemoryStorage and register the state as middleware.
+        convo_state = ConversationState(MemoryStorage())
+        telemetry = MagicMock(name=MOCK_TELEMETRY)
+
+        # Create a DialogState property, DialogSet and register the WaterfallDialog.
+        dialog_state = convo_state.create_property("dialogState")
+        dialogs = DialogSet(dialog_state)
+
+        async def step1(step) -> DialogTurnResult:
+            await step.context.send_activity("bot responding.")
+            return Dialog.end_of_turn
+
+        async def step2(step) -> DialogTurnResult:
+            await step.context.send_activity("ending WaterfallDialog.")
+            return Dialog.end_of_turn
+
+        # act
+
+        my_dialog = WaterfallDialog("test", [step1, step2])
+        my_dialog.telemetry_client = telemetry
+        dialogs.add(my_dialog)
+
+        # Initialize TestAdapter
+        async def exec_test(turn_context: TurnContext) -> None:
+
+            dialog_context = await dialogs.create_context(turn_context)
+            results = await dialog_context.continue_dialog()
+            if results.status == DialogTurnStatus.Empty:
+                await dialog_context.begin_dialog("test")
+            else:
+                if results.status == DialogTurnStatus.Complete:
+                    await turn_context.send_activity(results.result)
+
+            await convo_state.save_changes(turn_context)
+
+        adapt = TestAdapter(exec_test)
+
+        test_flow = TestFlow(None, adapt)
+        tf2 = await test_flow.send(BEGIN_MESSAGE)
+        tf3 = await tf2.assert_reply("bot responding.")
+        tf4 = await tf3.send("continue")
+        await tf4.assert_reply("ending WaterfallDialog.")
+
+        # assert
+        telemetry_calls = [
+            ("WaterfallStart", {"DialogId": "test"}),
+            ("WaterfallStep", {"DialogId": "test", "StepName": step1.__qualname__}),
+            ("WaterfallStep", {"DialogId": "test", "StepName": step2.__qualname__}),
+        ]
+        self.assert_telemetry_calls(telemetry, telemetry_calls)
+
+    async def test_ensure_end_dialog_called(self):
+        # arrange
+
+        # Create new ConversationState with MemoryStorage and register the state as middleware.
+        convo_state = ConversationState(MemoryStorage())
+        telemetry = MagicMock(name=MOCK_TELEMETRY)
+
+        # Create a DialogState property, DialogSet and register the WaterfallDialog.
+        dialog_state = convo_state.create_property("dialogState")
+        dialogs = DialogSet(dialog_state)
+
+        async def step1(step) -> DialogTurnResult:
+            await step.context.send_activity("step1 response")
+            return Dialog.end_of_turn
+
+        async def step2(step) -> DialogTurnResult:
+            await step.context.send_activity("step2 response")
+            return Dialog.end_of_turn
+
+        # act
+
+        my_dialog = WaterfallDialog("test", [step1, step2])
+        my_dialog.telemetry_client = telemetry
+        dialogs.add(my_dialog)
+
+        # Initialize TestAdapter
+        async def exec_test(turn_context: TurnContext) -> None:
+
+            dialog_context = await dialogs.create_context(turn_context)
+            await dialog_context.continue_dialog()
+            if not turn_context.responded:
+                await dialog_context.begin_dialog("test", None)
+            await convo_state.save_changes(turn_context)
+
+        adapt = TestAdapter(exec_test)
+
+        test_flow = TestFlow(None, adapt)
+        tf2 = await test_flow.send(BEGIN_MESSAGE)
+        tf3 = await tf2.assert_reply("step1 response")
+        tf4 = await tf3.send("continue")
+        tf5 = await tf4.assert_reply("step2 response")
+        await tf5.send(
+            "Should hit end of steps - this will restart the dialog and trigger COMPLETE event"
+        )
+        # assert
+        telemetry_calls = [
+            ("WaterfallStart", {"DialogId": "test"}),
+            ("WaterfallStep", {"DialogId": "test", "StepName": step1.__qualname__}),
+            ("WaterfallStep", {"DialogId": "test", "StepName": step2.__qualname__}),
+            ("WaterfallComplete", {"DialogId": "test"}),
+            ("WaterfallStart", {"DialogId": "test"}),
+            ("WaterfallStep", {"DialogId": "test", "StepName": step1.__qualname__}),
+        ]
+        self.assert_telemetry_calls(telemetry, telemetry_calls)
+
+    async def test_cancelling_waterfall_telemetry(self):
+        # Arrange
+        dialog_id = "waterfall"
+        index = 0
+        guid = "(guid)"
+
+        async def my_waterfall_step(step) -> DialogTurnResult:
+            await step.context.send_activity("step1 response")
+            return Dialog.end_of_turn
+
+        dialog = WaterfallDialog(dialog_id, [my_waterfall_step])
+
+        telemetry_client = create_autospec(NullTelemetryClient)
+        dialog.telemetry_client = telemetry_client
+
+        dialog_instance = DialogInstance()
+        dialog_instance.id = dialog_id
+        dialog_instance.state = {"instanceId": guid, "stepIndex": index}
+
+        # Act
+        await dialog.end_dialog(
+            TurnContext(TestAdapter(), Activity()),
+            dialog_instance,
+            DialogReason.CancelCalled,
+        )
+
+        # Assert
+        telemetry_props = telemetry_client.track_event.call_args_list[0][0][1]
+
+        self.assertEqual(3, len(telemetry_props))
+        self.assertEqual(dialog_id, telemetry_props["DialogId"])
+        self.assertEqual(my_waterfall_step.__qualname__, telemetry_props["StepName"])
+        self.assertEqual(guid, telemetry_props["InstanceId"])
+        telemetry_client.track_event.assert_called_once()
+
+    def assert_telemetry_call(
+        self, telemetry_mock, index: int, event_name: str, props: Dict[str, str]
+    ) -> None:
+        # pylint: disable=unused-variable
+        args, kwargs = telemetry_mock.track_event.call_args_list[index]
+        self.assertEqual(args[0], event_name)
+
+        for key, val in props.items():
+            self.assertTrue(
+                key in args[1],
+                msg=f"Could not find value {key} in {args[1]} for index {index}",
+            )
+            self.assertTrue(isinstance(args[1], dict))
+            self.assertTrue(val == args[1][key])
+
+    def assert_telemetry_calls(self, telemetry_mock, calls) -> None:
+        index = 0
+        for event_name, props in calls:
+            self.assert_telemetry_call(telemetry_mock, index, event_name, props)
+            index += 1
+        if index != len(telemetry_mock.track_event.call_args_list):
+            self.assertTrue(  # pylint: disable=redundant-unittest-assert
+                False,
+                f"Found {len(telemetry_mock.track_event.call_args_list)} calls, testing for {index + 1}",
+            )
diff --git a/libraries/botbuilder-azure/README.rst b/libraries/botbuilder-azure/README.rst
index 04af9dacc..ab937de70 100644
--- a/libraries/botbuilder-azure/README.rst
+++ b/libraries/botbuilder-azure/README.rst
@@ -3,12 +3,12 @@
 BotBuilder-Azure SDK for Python
 ===============================
 
-.. image:: https://fuselabs.visualstudio.com/SDK_v4/_apis/build/status/Python/SDK_v4-Python-CI?branchName=master
-   :target:  https://fuselabs.visualstudio.com/SDK_v4/_apis/build/status/Python/SDK_v4-Python-CI
+.. image:: https://dev.azure.com/FuseLabs/SDK_v4/_apis/build/status/Python/Python-CI-PR-yaml?branchName=master
+   :target:  https://dev.azure.com/FuseLabs/SDK_v4/_apis/build/status/Python/Python-CI-PR-yaml?branchName=master
    :align: right
    :alt: Azure DevOps status for master branch
-.. image:: https://badge.fury.io/py/botbuilder-dialogs.svg
-   :target: https://badge.fury.io/py/botbuilder-dialogs
+.. image:: https://badge.fury.io/py/botbuilder-azure.svg
+   :target: https://badge.fury.io/py/botbuilder-azure
    :alt: Latest PyPI package version
 
 Azure extensions for Microsoft BotBuilder.
diff --git a/libraries/botbuilder-azure/botbuilder/azure/__init__.py b/libraries/botbuilder-azure/botbuilder/azure/__init__.py
index 54dea209d..9980f8aa4 100644
--- a/libraries/botbuilder-azure/botbuilder/azure/__init__.py
+++ b/libraries/botbuilder-azure/botbuilder/azure/__init__.py
@@ -7,6 +7,10 @@
 
 from .about import __version__
 from .cosmosdb_storage import CosmosDbStorage, CosmosDbConfig, CosmosDbKeyEscape
+from .cosmosdb_partitioned_storage import (
+    CosmosDbPartitionedStorage,
+    CosmosDbPartitionedConfig,
+)
 from .blob_storage import BlobStorage, BlobStorageSettings
 
 __all__ = [
@@ -15,5 +19,7 @@
     "CosmosDbStorage",
     "CosmosDbConfig",
     "CosmosDbKeyEscape",
+    "CosmosDbPartitionedStorage",
+    "CosmosDbPartitionedConfig",
     "__version__",
 ]
diff --git a/libraries/botbuilder-azure/botbuilder/azure/about.py b/libraries/botbuilder-azure/botbuilder/azure/about.py
index 94b912127..9052a0c03 100644
--- a/libraries/botbuilder-azure/botbuilder/azure/about.py
+++ b/libraries/botbuilder-azure/botbuilder/azure/about.py
@@ -1,14 +1,14 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import os
-
-__title__ = "botbuilder-azure"
-__version__ = (
-    os.environ["packageVersion"] if "packageVersion" in os.environ else "4.4.0b1"
-)
-__uri__ = "https://www.github.com/Microsoft/botbuilder-python"
-__author__ = "Microsoft"
-__description__ = "Microsoft Bot Framework Bot Builder"
-__summary__ = "Microsoft Bot Framework Bot Builder SDK for Python."
-__license__ = "MIT"
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+
+__title__ = "botbuilder-azure"
+__version__ = (
+    os.environ["packageVersion"] if "packageVersion" in os.environ else "4.12.0"
+)
+__uri__ = "https://www.github.com/Microsoft/botbuilder-python"
+__author__ = "Microsoft"
+__description__ = "Microsoft Bot Framework Bot Builder"
+__summary__ = "Microsoft Bot Framework Bot Builder SDK for Python."
+__license__ = "MIT"
diff --git a/libraries/botbuilder-azure/botbuilder/azure/blob_storage.py b/libraries/botbuilder-azure/botbuilder/azure/blob_storage.py
index ae3ad1766..b69217680 100644
--- a/libraries/botbuilder-azure/botbuilder/azure/blob_storage.py
+++ b/libraries/botbuilder-azure/botbuilder/azure/blob_storage.py
@@ -3,7 +3,6 @@
 
 from jsonpickle import encode
 from jsonpickle.unpickler import Unpickler
-
 from azure.storage.blob import BlockBlobService, Blob, PublicAccess
 from botbuilder.core import Storage
 
@@ -42,7 +41,7 @@ def __init__(self, settings: BlobStorageSettings):
 
     async def read(self, keys: List[str]) -> Dict[str, object]:
         if not keys:
-            raise Exception("Please provide at least one key to read from storage.")
+            raise Exception("Keys are required when reading")
 
         self.client.create_container(self.settings.container_name)
         self.client.set_container_acl(
@@ -63,24 +62,35 @@ async def read(self, keys: List[str]) -> Dict[str, object]:
         return items
 
     async def write(self, changes: Dict[str, object]):
+        if changes is None:
+            raise Exception("Changes are required when writing")
+        if not changes:
+            return
+
         self.client.create_container(self.settings.container_name)
         self.client.set_container_acl(
             self.settings.container_name, public_access=PublicAccess.Container
         )
 
-        for name, item in changes.items():
-            e_tag = (
-                None if not hasattr(item, "e_tag") or item.e_tag == "*" else item.e_tag
-            )
-            if e_tag:
-                item.e_tag = e_tag.replace('"', '\\"')
+        for (name, item) in changes.items():
+            e_tag = None
+            if isinstance(item, dict):
+                e_tag = item.get("e_tag", None)
+            elif hasattr(item, "e_tag"):
+                e_tag = item.e_tag
+            e_tag = None if e_tag == "*" else e_tag
+            if e_tag == "":
+                raise Exception("blob_storage.write(): etag missing")
             item_str = self._store_item_to_str(item)
-            self.client.create_blob_from_text(
-                container_name=self.settings.container_name,
-                blob_name=name,
-                text=item_str,
-                if_match=e_tag,
-            )
+            try:
+                self.client.create_blob_from_text(
+                    container_name=self.settings.container_name,
+                    blob_name=name,
+                    text=item_str,
+                    if_match=e_tag,
+                )
+            except Exception as error:
+                raise error
 
     async def delete(self, keys: List[str]):
         if keys is None:
@@ -102,7 +112,6 @@ async def delete(self, keys: List[str]):
     def _blob_to_store_item(self, blob: Blob) -> object:
         item = json.loads(blob.content)
         item["e_tag"] = blob.properties.etag
-        item["id"] = blob.name
         result = Unpickler().restore(item)
         return result
 
diff --git a/libraries/botbuilder-azure/botbuilder/azure/cosmosdb_partitioned_storage.py b/libraries/botbuilder-azure/botbuilder/azure/cosmosdb_partitioned_storage.py
new file mode 100644
index 000000000..db5ae1685
--- /dev/null
+++ b/libraries/botbuilder-azure/botbuilder/azure/cosmosdb_partitioned_storage.py
@@ -0,0 +1,330 @@
+"""Implements a CosmosDB based storage provider using partitioning for a bot.
+"""
+
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+from typing import Dict, List
+from threading import Lock
+import json
+
+from azure.cosmos import documents, http_constants
+from jsonpickle.pickler import Pickler
+from jsonpickle.unpickler import Unpickler
+import azure.cosmos.cosmos_client as cosmos_client  # pylint: disable=no-name-in-module,import-error
+import azure.cosmos.errors as cosmos_errors  # pylint: disable=no-name-in-module,import-error
+from botbuilder.core.storage import Storage
+from botbuilder.azure import CosmosDbKeyEscape
+
+
+class CosmosDbPartitionedConfig:
+    """The class for partitioned CosmosDB configuration for the Azure Bot Framework."""
+
+    def __init__(
+        self,
+        cosmos_db_endpoint: str = None,
+        auth_key: str = None,
+        database_id: str = None,
+        container_id: str = None,
+        cosmos_client_options: dict = None,
+        container_throughput: int = 400,
+        key_suffix: str = "",
+        compatibility_mode: bool = False,
+        **kwargs,
+    ):
+        """Create the Config object.
+
+        :param cosmos_db_endpoint: The CosmosDB endpoint.
+        :param auth_key: The authentication key for Cosmos DB.
+        :param database_id: The database identifier for Cosmos DB instance.
+        :param container_id: The container identifier.
+        :param cosmos_client_options: The options for the CosmosClient. Currently only supports connection_policy and
+            consistency_level
+        :param container_throughput: The throughput set when creating the Container. Defaults to 400.
+        :param key_suffix: The suffix to be added to every key. The keySuffix must contain only valid ComosDb
+            key characters. (e.g. not: '\\', '?', '/', '#', '*')
+        :param compatibility_mode: True if keys should be truncated in order to support previous CosmosDb
+            max key length of 255.
+        :return CosmosDbPartitionedConfig:
+        """
+        self.__config_file = kwargs.get("filename")
+        if self.__config_file:
+            kwargs = json.load(open(self.__config_file))
+        self.cosmos_db_endpoint = cosmos_db_endpoint or kwargs.get("cosmos_db_endpoint")
+        self.auth_key = auth_key or kwargs.get("auth_key")
+        self.database_id = database_id or kwargs.get("database_id")
+        self.container_id = container_id or kwargs.get("container_id")
+        self.cosmos_client_options = cosmos_client_options or kwargs.get(
+            "cosmos_client_options", {}
+        )
+        self.container_throughput = container_throughput or kwargs.get(
+            "container_throughput"
+        )
+        self.key_suffix = key_suffix or kwargs.get("key_suffix")
+        self.compatibility_mode = compatibility_mode or kwargs.get("compatibility_mode")
+
+
+class CosmosDbPartitionedStorage(Storage):
+    """A CosmosDB based storage provider using partitioning for a bot."""
+
+    def __init__(self, config: CosmosDbPartitionedConfig):
+        """Create the storage object.
+
+        :param config:
+        """
+        super(CosmosDbPartitionedStorage, self).__init__()
+        self.config = config
+        self.client = None
+        self.database = None
+        self.container = None
+        self.compatability_mode_partition_key = False
+        # Lock used for synchronizing container creation
+        self.__lock = Lock()
+        if config.key_suffix is None:
+            config.key_suffix = ""
+        if not config.key_suffix.__eq__(""):
+            if config.compatibility_mode:
+                raise Exception(
+                    "compatibilityMode cannot be true while using a keySuffix."
+                )
+            suffix_escaped = CosmosDbKeyEscape.sanitize_key(config.key_suffix)
+            if not suffix_escaped.__eq__(config.key_suffix):
+                raise Exception(
+                    f"Cannot use invalid Row Key characters: {config.key_suffix} in keySuffix."
+                )
+
+    async def read(self, keys: List[str]) -> Dict[str, object]:
+        """Read storeitems from storage.
+
+        :param keys:
+        :return dict:
+        """
+        if not keys:
+            raise Exception("Keys are required when reading")
+
+        await self.initialize()
+
+        store_items = {}
+
+        for key in keys:
+            try:
+                escaped_key = CosmosDbKeyEscape.sanitize_key(
+                    key, self.config.key_suffix, self.config.compatibility_mode
+                )
+
+                read_item_response = self.client.ReadItem(
+                    self.__item_link(escaped_key), self.__get_partition_key(escaped_key)
+                )
+                document_store_item = read_item_response
+                if document_store_item:
+                    store_items[document_store_item["realId"]] = self.__create_si(
+                        document_store_item
+                    )
+            # When an item is not found a CosmosException is thrown, but we want to
+            # return an empty collection so in this instance we catch and do not rethrow.
+            # Throw for any other exception.
+            except cosmos_errors.HTTPFailure as err:
+                if (
+                    err.status_code
+                    == cosmos_errors.http_constants.StatusCodes.NOT_FOUND
+                ):
+                    continue
+                raise err
+            except Exception as err:
+                raise err
+        return store_items
+
+    async def write(self, changes: Dict[str, object]):
+        """Save storeitems to storage.
+
+        :param changes:
+        :return:
+        """
+        if changes is None:
+            raise Exception("Changes are required when writing")
+        if not changes:
+            return
+
+        await self.initialize()
+
+        for (key, change) in changes.items():
+            e_tag = None
+            if isinstance(change, dict):
+                e_tag = change.get("e_tag", None)
+            elif hasattr(change, "e_tag"):
+                e_tag = change.e_tag
+            doc = {
+                "id": CosmosDbKeyEscape.sanitize_key(
+                    key, self.config.key_suffix, self.config.compatibility_mode
+                ),
+                "realId": key,
+                "document": self.__create_dict(change),
+            }
+            if e_tag == "":
+                raise Exception("cosmosdb_storage.write(): etag missing")
+
+            access_condition = {
+                "accessCondition": {"type": "IfMatch", "condition": e_tag}
+            }
+            options = (
+                access_condition if e_tag != "*" and e_tag and e_tag != "" else None
+            )
+            try:
+                self.client.UpsertItem(
+                    database_or_Container_link=self.__container_link,
+                    document=doc,
+                    options=options,
+                )
+            except cosmos_errors.HTTPFailure as err:
+                raise err
+            except Exception as err:
+                raise err
+
+    async def delete(self, keys: List[str]):
+        """Remove storeitems from storage.
+
+        :param keys:
+        :return:
+        """
+        await self.initialize()
+
+        for key in keys:
+            escaped_key = CosmosDbKeyEscape.sanitize_key(
+                key, self.config.key_suffix, self.config.compatibility_mode
+            )
+            try:
+                self.client.DeleteItem(
+                    document_link=self.__item_link(escaped_key),
+                    options=self.__get_partition_key(escaped_key),
+                )
+            except cosmos_errors.HTTPFailure as err:
+                if (
+                    err.status_code
+                    == cosmos_errors.http_constants.StatusCodes.NOT_FOUND
+                ):
+                    continue
+                raise err
+            except Exception as err:
+                raise err
+
+    async def initialize(self):
+        if not self.container:
+            if not self.client:
+                self.client = cosmos_client.CosmosClient(
+                    self.config.cosmos_db_endpoint,
+                    {"masterKey": self.config.auth_key},
+                    self.config.cosmos_client_options.get("connection_policy", None),
+                    self.config.cosmos_client_options.get("consistency_level", None),
+                )
+
+            if not self.database:
+                with self.__lock:
+                    try:
+                        if not self.database:
+                            self.database = self.client.CreateDatabase(
+                                {"id": self.config.database_id}
+                            )
+                    except cosmos_errors.HTTPFailure:
+                        self.database = self.client.ReadDatabase(
+                            "dbs/" + self.config.database_id
+                        )
+
+            self.__get_or_create_container()
+
+    def __get_or_create_container(self):
+        with self.__lock:
+            container_def = {
+                "id": self.config.container_id,
+                "partitionKey": {
+                    "paths": ["/id"],
+                    "kind": documents.PartitionKind.Hash,
+                },
+            }
+            try:
+                if not self.container:
+                    self.container = self.client.CreateContainer(
+                        "dbs/" + self.database["id"],
+                        container_def,
+                        {"offerThroughput": self.config.container_throughput},
+                    )
+            except cosmos_errors.HTTPFailure as err:
+                if err.status_code == http_constants.StatusCodes.CONFLICT:
+                    self.container = self.client.ReadContainer(
+                        "dbs/" + self.database["id"] + "/colls/" + container_def["id"]
+                    )
+                    if "partitionKey" not in self.container:
+                        self.compatability_mode_partition_key = True
+                    else:
+                        paths = self.container["partitionKey"]["paths"]
+                        if "/partitionKey" in paths:
+                            self.compatability_mode_partition_key = True
+                        elif "/id" not in paths:
+                            raise Exception(
+                                f"Custom Partition Key Paths are not supported. {self.config.container_id} "
+                                "has a custom Partition Key Path of {paths[0]}."
+                            )
+
+                else:
+                    raise err
+
+    def __get_partition_key(self, key: str) -> str:
+        return None if self.compatability_mode_partition_key else {"partitionKey": key}
+
+    @staticmethod
+    def __create_si(result) -> object:
+        """Create an object from a result out of CosmosDB.
+
+        :param result:
+        :return object:
+        """
+        # get the document item from the result and turn into a dict
+        doc = result.get("document")
+        # read the e_tag from Cosmos
+        if result.get("_etag"):
+            doc["e_tag"] = result["_etag"]
+
+        result_obj = Unpickler().restore(doc)
+
+        # create and return the object
+        return result_obj
+
+    @staticmethod
+    def __create_dict(store_item: object) -> Dict:
+        """Return the dict of an object.
+
+        This eliminates non_magic attributes and the e_tag.
+
+        :param store_item:
+        :return dict:
+        """
+        # read the content
+        json_dict = Pickler().flatten(store_item)
+        if "e_tag" in json_dict:
+            del json_dict["e_tag"]
+
+        # loop through attributes and write and return a dict
+        return json_dict
+
+    def __item_link(self, identifier) -> str:
+        """Return the item link of a item in the container.
+
+        :param identifier:
+        :return str:
+        """
+        return self.__container_link + "/docs/" + identifier
+
+    @property
+    def __container_link(self) -> str:
+        """Return the container link in the database.
+
+        :param:
+        :return str:
+        """
+        return self.__database_link + "/colls/" + self.config.container_id
+
+    @property
+    def __database_link(self) -> str:
+        """Return the database link.
+
+        :return str:
+        """
+        return "dbs/" + self.config.database_id
diff --git a/libraries/botbuilder-azure/botbuilder/azure/cosmosdb_storage.py b/libraries/botbuilder-azure/botbuilder/azure/cosmosdb_storage.py
index c8a25a017..9a1c89d2e 100644
--- a/libraries/botbuilder-azure/botbuilder/azure/cosmosdb_storage.py
+++ b/libraries/botbuilder-azure/botbuilder/azure/cosmosdb_storage.py
@@ -1,7 +1,4 @@
-"""CosmosDB Middleware for Python Bot Framework.
-
-This is middleware to store items in CosmosDB.
-Part of the Azure Bot Framework in Python.
+"""Implements a CosmosDB based storage provider.
 """
 
 # Copyright (c) Microsoft Corporation. All rights reserved.
@@ -58,12 +55,18 @@ def __init__(
 
 class CosmosDbKeyEscape:
     @staticmethod
-    def sanitize_key(key) -> str:
+    def sanitize_key(
+        key: str, key_suffix: str = "", compatibility_mode: bool = True
+    ) -> str:
         """Return the sanitized key.
 
         Replace characters that are not allowed in keys in Cosmos.
 
-        :param key:
+        :param key: The provided key to be escaped.
+        :param key_suffix: The string to add a the end of all RowKeys.
+        :param compatibility_mode: True if keys should be truncated in order to support previous CosmosDb
+            max key length of 255.  This behavior can be overridden by setting
+            cosmosdb_partitioned_config.compatibility_mode to False.
         :return str:
         """
         # forbidden characters
@@ -72,12 +75,18 @@ def sanitize_key(key) -> str:
         # Unicode code point of the character and return the new string
         key = "".join(map(lambda x: "*" + str(ord(x)) if x in bad_chars else x, key))
 
-        return CosmosDbKeyEscape.truncate_key(key)
+        if key_suffix is None:
+            key_suffix = ""
+
+        return CosmosDbKeyEscape.truncate_key(f"{key}{key_suffix}", compatibility_mode)
 
     @staticmethod
-    def truncate_key(key: str) -> str:
+    def truncate_key(key: str, compatibility_mode: bool = True) -> str:
         max_key_len = 255
 
+        if not compatibility_mode:
+            return key
+
         if len(key) > max_key_len:
             aux_hash = sha256(key.encode("utf-8"))
             aux_hex = aux_hash.hexdigest()
@@ -88,7 +97,7 @@ def truncate_key(key: str) -> str:
 
 
 class CosmosDbStorage(Storage):
-    """The class for CosmosDB middleware for the Azure Bot Framework."""
+    """A CosmosDB based storage provider for a bot."""
 
     def __init__(
         self, config: CosmosDbConfig, client: cosmos_client.CosmosClient = None
@@ -160,6 +169,10 @@ async def write(self, changes: Dict[str, object]):
         :param changes:
         :return:
         """
+        if changes is None:
+            raise Exception("Changes are required when writing")
+        if not changes:
+            return
         try:
             # check if the database and container exists and if not create
             if not self.__container_exists:
@@ -167,13 +180,19 @@ async def write(self, changes: Dict[str, object]):
                 # iterate over the changes
             for (key, change) in changes.items():
                 # store the e_tag
-                e_tag = change.e_tag
+                e_tag = None
+                if isinstance(change, dict):
+                    e_tag = change.get("e_tag", None)
+                elif hasattr(change, "e_tag"):
+                    e_tag = change.e_tag
                 # create the new document
                 doc = {
                     "id": CosmosDbKeyEscape.sanitize_key(key),
                     "realId": key,
                     "document": self.__create_dict(change),
                 }
+                if e_tag == "":
+                    raise Exception("cosmosdb_storage.write(): etag missing")
                 # the e_tag will be * for new docs so do an insert
                 if e_tag == "*" or not e_tag:
                     self.client.UpsertItem(
@@ -191,9 +210,6 @@ async def write(self, changes: Dict[str, object]):
                         new_document=doc,
                         options={"accessCondition": access_condition},
                     )
-                # error when there is no e_tag
-                else:
-                    raise Exception("cosmosdb_storage.write(): etag missing")
         except Exception as error:
             raise error
 
diff --git a/libraries/botbuilder-azure/setup.py b/libraries/botbuilder-azure/setup.py
index dd3a5e12a..48333f8d3 100644
--- a/libraries/botbuilder-azure/setup.py
+++ b/libraries/botbuilder-azure/setup.py
@@ -5,13 +5,13 @@
 from setuptools import setup
 
 REQUIRES = [
-    "azure-cosmos>=3.0.0",
-    "azure-storage-blob>=2.1.0",
-    "botbuilder-schema>=4.4.0b1",
-    "botframework-connector>=4.4.0b1",
-    "jsonpickle>=1.2",
+    "azure-cosmos==3.2.0",
+    "azure-storage-blob==2.1.0",
+    "botbuilder-schema==4.12.0",
+    "botframework-connector==4.12.0",
+    "jsonpickle==1.2",
 ]
-TEST_REQUIRES = ["aiounittest>=1.1.0"]
+TEST_REQUIRES = ["aiounittest==1.3.0"]
 
 root = os.path.abspath(os.path.dirname(__file__))
 
@@ -41,7 +41,7 @@
         "Intended Audience :: Developers",
         "License :: OSI Approved :: MIT License",
         "Operating System :: OS Independent",
-        "Development Status :: 3 - Alpha",
+        "Development Status :: 5 - Production/Stable",
         "Topic :: Scientific/Engineering :: Artificial Intelligence",
     ],
 )
diff --git a/libraries/botbuilder-azure/tests/test_blob_storage.py b/libraries/botbuilder-azure/tests/test_blob_storage.py
index b29d2ad9f..31f54a231 100644
--- a/libraries/botbuilder-azure/tests/test_blob_storage.py
+++ b/libraries/botbuilder-azure/tests/test_blob_storage.py
@@ -4,16 +4,29 @@
 import pytest
 from botbuilder.core import StoreItem
 from botbuilder.azure import BlobStorage, BlobStorageSettings
+from botbuilder.testing import StorageBaseTests
 
 # local blob emulator instance blob
+
 BLOB_STORAGE_SETTINGS = BlobStorageSettings(
-    account_name="", account_key="", container_name="test"
+    account_name="",
+    account_key="",
+    container_name="test",
+    # Default Azure Storage Emulator Connection String
+    connection_string="AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq"
+    + "2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;DefaultEndpointsProtocol=http;BlobEndpoint="
+    + "http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;"
+    + "TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;",
 )
 EMULATOR_RUNNING = False
 
 
+def get_storage():
+    return BlobStorage(BLOB_STORAGE_SETTINGS)
+
+
 async def reset():
-    storage = BlobStorage(BLOB_STORAGE_SETTINGS)
+    storage = get_storage()
     try:
         await storage.client.delete_container(
             container_name=BLOB_STORAGE_SETTINGS.container_name
@@ -29,7 +42,7 @@ def __init__(self, counter=1, e_tag="*"):
         self.e_tag = e_tag
 
 
-class TestBlobStorage:
+class TestBlobStorageConstructor:
     @pytest.mark.asyncio
     async def test_blob_storage_init_should_error_without_cosmos_db_config(self):
         try:
@@ -37,17 +50,104 @@ async def test_blob_storage_init_should_error_without_cosmos_db_config(self):
         except Exception as error:
             assert error
 
+
+class TestBlobStorageBaseTests:
     @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
     @pytest.mark.asyncio
-    async def test_blob_storage_read_should_return_data_with_valid_key(self):
-        storage = BlobStorage(BLOB_STORAGE_SETTINGS)
-        await storage.write({"user": SimpleStoreItem()})
+    async def test_return_empty_object_when_reading_unknown_key(self):
+        await reset()
 
-        data = await storage.read(["user"])
-        assert "user" in data
-        assert data["user"].counter == 1
-        assert len(data.keys()) == 1
+        test_ran = await StorageBaseTests.return_empty_object_when_reading_unknown_key(
+            get_storage()
+        )
+
+        assert test_ran
+
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_handle_null_keys_when_reading(self):
+        await reset()
+
+        test_ran = await StorageBaseTests.handle_null_keys_when_reading(get_storage())
+
+        assert test_ran
+
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_handle_null_keys_when_writing(self):
+        await reset()
+
+        test_ran = await StorageBaseTests.handle_null_keys_when_writing(get_storage())
+
+        assert test_ran
+
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_does_not_raise_when_writing_no_items(self):
+        await reset()
+
+        test_ran = await StorageBaseTests.does_not_raise_when_writing_no_items(
+            get_storage()
+        )
+
+        assert test_ran
+
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_create_object(self):
+        await reset()
+
+        test_ran = await StorageBaseTests.create_object(get_storage())
+
+        assert test_ran
+
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_handle_crazy_keys(self):
+        await reset()
+
+        test_ran = await StorageBaseTests.handle_crazy_keys(get_storage())
+
+        assert test_ran
+
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_update_object(self):
+        await reset()
 
+        test_ran = await StorageBaseTests.update_object(get_storage())
+
+        assert test_ran
+
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_delete_object(self):
+        await reset()
+
+        test_ran = await StorageBaseTests.delete_object(get_storage())
+
+        assert test_ran
+
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_perform_batch_operations(self):
+        await reset()
+
+        test_ran = await StorageBaseTests.perform_batch_operations(get_storage())
+
+        assert test_ran
+
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_proceeds_through_waterfall(self):
+        await reset()
+
+        test_ran = await StorageBaseTests.proceeds_through_waterfall(get_storage())
+
+        assert test_ran
+
+
+class TestBlobStorage:
     @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
     @pytest.mark.asyncio
     async def test_blob_storage_read_update_should_return_new_etag(self):
@@ -60,29 +160,10 @@ async def test_blob_storage_read_update_should_return_new_etag(self):
         assert data_updated["test"].counter == 2
         assert data_updated["test"].e_tag != data_result["test"].e_tag
 
-    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
-    @pytest.mark.asyncio
-    async def test_blob_storage_read_no_key_should_throw(self):
-        try:
-            storage = BlobStorage(BLOB_STORAGE_SETTINGS)
-            await storage.read([])
-        except Exception as error:
-            assert error
-
-    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
-    @pytest.mark.asyncio
-    async def test_blob_storage_write_should_add_new_value(self):
-        storage = BlobStorage(BLOB_STORAGE_SETTINGS)
-        await storage.write({"user": SimpleStoreItem(counter=1)})
-
-        data = await storage.read(["user"])
-        assert "user" in data
-        assert data["user"].counter == 1
-
     @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
     @pytest.mark.asyncio
     async def test_blob_storage_write_should_overwrite_when_new_e_tag_is_an_asterisk(
-        self
+        self,
     ):
         storage = BlobStorage(BLOB_STORAGE_SETTINGS)
         await storage.write({"user": SimpleStoreItem()})
@@ -91,32 +172,6 @@ async def test_blob_storage_write_should_overwrite_when_new_e_tag_is_an_asterisk
         data = await storage.read(["user"])
         assert data["user"].counter == 10
 
-    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
-    @pytest.mark.asyncio
-    async def test_blob_storage_write_batch_operation(self):
-        storage = BlobStorage(BLOB_STORAGE_SETTINGS)
-        await storage.write(
-            {
-                "batch1": SimpleStoreItem(counter=1),
-                "batch2": SimpleStoreItem(counter=1),
-                "batch3": SimpleStoreItem(counter=1),
-            }
-        )
-        data = await storage.read(["batch1", "batch2", "batch3"])
-        assert len(data.keys()) == 3
-        assert data["batch1"]
-        assert data["batch2"]
-        assert data["batch3"]
-        assert data["batch1"].counter == 1
-        assert data["batch2"].counter == 1
-        assert data["batch3"].counter == 1
-        assert data["batch1"].e_tag
-        assert data["batch2"].e_tag
-        assert data["batch3"].e_tag
-        await storage.delete(["batch1", "batch2", "batch3"])
-        data = await storage.read(["batch1", "batch2", "batch3"])
-        assert not data.keys()
-
     @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
     @pytest.mark.asyncio
     async def test_blob_storage_delete_should_delete_according_cached_data(self):
@@ -135,7 +190,7 @@ async def test_blob_storage_delete_should_delete_according_cached_data(self):
     @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
     @pytest.mark.asyncio
     async def test_blob_storage_delete_should_delete_multiple_values_when_given_multiple_valid_keys(
-        self
+        self,
     ):
         storage = BlobStorage(BLOB_STORAGE_SETTINGS)
         await storage.write({"test": SimpleStoreItem(), "test2": SimpleStoreItem(2)})
@@ -147,7 +202,7 @@ async def test_blob_storage_delete_should_delete_multiple_values_when_given_mult
     @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
     @pytest.mark.asyncio
     async def test_blob_storage_delete_should_delete_values_when_given_multiple_valid_keys_and_ignore_other_data(
-        self
+        self,
     ):
         storage = BlobStorage(BLOB_STORAGE_SETTINGS)
         await storage.write(
@@ -165,7 +220,7 @@ async def test_blob_storage_delete_should_delete_values_when_given_multiple_vali
     @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
     @pytest.mark.asyncio
     async def test_blob_storage_delete_invalid_key_should_do_nothing_and_not_affect_cached_data(
-        self
+        self,
     ):
         storage = BlobStorage(BLOB_STORAGE_SETTINGS)
         await storage.write({"test": SimpleStoreItem()})
@@ -179,7 +234,7 @@ async def test_blob_storage_delete_invalid_key_should_do_nothing_and_not_affect_
     @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
     @pytest.mark.asyncio
     async def test_blob_storage_delete_invalid_keys_should_do_nothing_and_not_affect_cached_data(
-        self
+        self,
     ):
         storage = BlobStorage(BLOB_STORAGE_SETTINGS)
         await storage.write({"test": SimpleStoreItem()})
diff --git a/libraries/botbuilder-azure/tests/test_cosmos_partitioned_storage.py b/libraries/botbuilder-azure/tests/test_cosmos_partitioned_storage.py
new file mode 100644
index 000000000..cb6dd0822
--- /dev/null
+++ b/libraries/botbuilder-azure/tests/test_cosmos_partitioned_storage.py
@@ -0,0 +1,202 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import azure.cosmos.errors as cosmos_errors
+from azure.cosmos import documents
+import pytest
+from botbuilder.azure import CosmosDbPartitionedStorage, CosmosDbPartitionedConfig
+from botbuilder.testing import StorageBaseTests
+
+EMULATOR_RUNNING = False
+
+
+def get_settings() -> CosmosDbPartitionedConfig:
+    return CosmosDbPartitionedConfig(
+        cosmos_db_endpoint="https://localhost:8081",
+        auth_key="C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
+        database_id="test-db",
+        container_id="bot-storage",
+    )
+
+
+def get_storage():
+    return CosmosDbPartitionedStorage(get_settings())
+
+
+async def reset():
+    storage = CosmosDbPartitionedStorage(get_settings())
+    await storage.initialize()
+    try:
+        storage.client.DeleteDatabase(database_link="dbs/" + get_settings().database_id)
+    except cosmos_errors.HTTPFailure:
+        pass
+
+
+class TestCosmosDbPartitionedStorageConstructor:
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_raises_error_when_instantiated_with_no_arguments(self):
+        try:
+            # noinspection PyArgumentList
+            # pylint: disable=no-value-for-parameter
+            CosmosDbPartitionedStorage()
+        except Exception as error:
+            assert error
+
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_raises_error_when_no_endpoint_provided(self):
+        no_endpoint = get_settings()
+        no_endpoint.cosmos_db_endpoint = None
+        try:
+            CosmosDbPartitionedStorage(no_endpoint)
+        except Exception as error:
+            assert error
+
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_raises_error_when_no_auth_key_provided(self):
+        no_auth_key = get_settings()
+        no_auth_key.auth_key = None
+        try:
+            CosmosDbPartitionedStorage(no_auth_key)
+        except Exception as error:
+            assert error
+
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_raises_error_when_no_database_id_provided(self):
+        no_database_id = get_settings()
+        no_database_id.database_id = None
+        try:
+            CosmosDbPartitionedStorage(no_database_id)
+        except Exception as error:
+            assert error
+
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_raises_error_when_no_container_id_provided(self):
+        no_container_id = get_settings()
+        no_container_id.container_id = None
+        try:
+            CosmosDbPartitionedStorage(no_container_id)
+        except Exception as error:
+            assert error
+
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_passes_cosmos_client_options(self):
+        settings_with_options = get_settings()
+
+        connection_policy = documents.ConnectionPolicy()
+        connection_policy.DisableSSLVerification = True
+
+        settings_with_options.cosmos_client_options = {
+            "connection_policy": connection_policy,
+            "consistency_level": documents.ConsistencyLevel.Eventual,
+        }
+
+        client = CosmosDbPartitionedStorage(settings_with_options)
+        await client.initialize()
+
+        assert client.client.connection_policy.DisableSSLVerification is True
+        assert (
+            client.client.default_headers["x-ms-consistency-level"]
+            == documents.ConsistencyLevel.Eventual
+        )
+
+
+class TestCosmosDbPartitionedStorageBaseStorageTests:
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_return_empty_object_when_reading_unknown_key(self):
+        await reset()
+
+        test_ran = await StorageBaseTests.return_empty_object_when_reading_unknown_key(
+            get_storage()
+        )
+
+        assert test_ran
+
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_handle_null_keys_when_reading(self):
+        await reset()
+
+        test_ran = await StorageBaseTests.handle_null_keys_when_reading(get_storage())
+
+        assert test_ran
+
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_handle_null_keys_when_writing(self):
+        await reset()
+
+        test_ran = await StorageBaseTests.handle_null_keys_when_writing(get_storage())
+
+        assert test_ran
+
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_does_not_raise_when_writing_no_items(self):
+        await reset()
+
+        test_ran = await StorageBaseTests.does_not_raise_when_writing_no_items(
+            get_storage()
+        )
+
+        assert test_ran
+
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_create_object(self):
+        await reset()
+
+        test_ran = await StorageBaseTests.create_object(get_storage())
+
+        assert test_ran
+
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_handle_crazy_keys(self):
+        await reset()
+
+        test_ran = await StorageBaseTests.handle_crazy_keys(get_storage())
+
+        assert test_ran
+
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_update_object(self):
+        await reset()
+
+        test_ran = await StorageBaseTests.update_object(get_storage())
+
+        assert test_ran
+
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_delete_object(self):
+        await reset()
+
+        test_ran = await StorageBaseTests.delete_object(get_storage())
+
+        assert test_ran
+
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_perform_batch_operations(self):
+        await reset()
+
+        test_ran = await StorageBaseTests.perform_batch_operations(get_storage())
+
+        assert test_ran
+
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_proceeds_through_waterfall(self):
+        await reset()
+
+        test_ran = await StorageBaseTests.proceeds_through_waterfall(get_storage())
+
+        assert test_ran
diff --git a/libraries/botbuilder-azure/tests/test_cosmos_storage.py b/libraries/botbuilder-azure/tests/test_cosmos_storage.py
index 7e686f20b..c66660857 100644
--- a/libraries/botbuilder-azure/tests/test_cosmos_storage.py
+++ b/libraries/botbuilder-azure/tests/test_cosmos_storage.py
@@ -7,6 +7,7 @@
 import pytest
 from botbuilder.core import StoreItem
 from botbuilder.azure import CosmosDbStorage, CosmosDbConfig
+from botbuilder.testing import StorageBaseTests
 
 # local cosmosdb emulator instance cosmos_db_config
 COSMOS_DB_CONFIG = CosmosDbConfig(
@@ -18,6 +19,10 @@
 EMULATOR_RUNNING = False
 
 
+def get_storage():
+    return CosmosDbStorage(COSMOS_DB_CONFIG)
+
+
 async def reset():
     storage = CosmosDbStorage(COSMOS_DB_CONFIG)
     try:
@@ -27,7 +32,7 @@ async def reset():
 
 
 def get_mock_client(identifier: str = "1"):
-    # pylint: disable=invalid-name
+    # pylint: disable=attribute-defined-outside-init, invalid-name
     mock = MockClient()
 
     mock.QueryDatabases = Mock(return_value=[])
@@ -50,7 +55,7 @@ def __init__(self, counter=1, e_tag="*"):
         self.e_tag = e_tag
 
 
-class TestCosmosDbStorage:
+class TestCosmosDbStorageConstructor:
     @pytest.mark.asyncio
     async def test_cosmos_storage_init_should_error_without_cosmos_db_config(self):
         try:
@@ -59,7 +64,7 @@ async def test_cosmos_storage_init_should_error_without_cosmos_db_config(self):
             assert error
 
     @pytest.mark.asyncio
-    async def test_creation_request_options_era_being_called(self):
+    async def test_creation_request_options_are_being_called(self):
         # pylint: disable=protected-access
         test_config = CosmosDbConfig(
             endpoint="https://localhost:8081",
@@ -86,6 +91,104 @@ async def test_creation_request_options_era_being_called(self):
             "dbs/" + test_id, {"id": test_id}, test_config.container_creation_options
         )
 
+
+class TestCosmosDbStorageBaseStorageTests:
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_return_empty_object_when_reading_unknown_key(self):
+        await reset()
+
+        test_ran = await StorageBaseTests.return_empty_object_when_reading_unknown_key(
+            get_storage()
+        )
+
+        assert test_ran
+
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_handle_null_keys_when_reading(self):
+        await reset()
+
+        test_ran = await StorageBaseTests.handle_null_keys_when_reading(get_storage())
+
+        assert test_ran
+
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_handle_null_keys_when_writing(self):
+        await reset()
+
+        test_ran = await StorageBaseTests.handle_null_keys_when_writing(get_storage())
+
+        assert test_ran
+
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_does_not_raise_when_writing_no_items(self):
+        await reset()
+
+        test_ran = await StorageBaseTests.does_not_raise_when_writing_no_items(
+            get_storage()
+        )
+
+        assert test_ran
+
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_create_object(self):
+        await reset()
+
+        test_ran = await StorageBaseTests.create_object(get_storage())
+
+        assert test_ran
+
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_handle_crazy_keys(self):
+        await reset()
+
+        test_ran = await StorageBaseTests.handle_crazy_keys(get_storage())
+
+        assert test_ran
+
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_update_object(self):
+        await reset()
+
+        test_ran = await StorageBaseTests.update_object(get_storage())
+
+        assert test_ran
+
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_delete_object(self):
+        await reset()
+
+        test_ran = await StorageBaseTests.delete_object(get_storage())
+
+        assert test_ran
+
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_perform_batch_operations(self):
+        await reset()
+
+        test_ran = await StorageBaseTests.perform_batch_operations(get_storage())
+
+        assert test_ran
+
+    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
+    @pytest.mark.asyncio
+    async def test_proceeds_through_waterfall(self):
+        await reset()
+
+        test_ran = await StorageBaseTests.proceeds_through_waterfall(get_storage())
+
+        assert test_ran
+
+
+class TestCosmosDbStorage:
     @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
     @pytest.mark.asyncio
     async def test_cosmos_storage_init_should_work_with_just_endpoint_and_key(self):
@@ -100,18 +203,6 @@ async def test_cosmos_storage_init_should_work_with_just_endpoint_and_key(self):
         assert data["user"].counter == 1
         assert len(data.keys()) == 1
 
-    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
-    @pytest.mark.asyncio
-    async def test_cosmos_storage_read_should_return_data_with_valid_key(self):
-        await reset()
-        storage = CosmosDbStorage(COSMOS_DB_CONFIG)
-        await storage.write({"user": SimpleStoreItem()})
-
-        data = await storage.read(["user"])
-        assert "user" in data
-        assert data["user"].counter == 1
-        assert len(data.keys()) == 1
-
     @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
     @pytest.mark.asyncio
     async def test_cosmos_storage_read_update_should_return_new_etag(self):
@@ -135,31 +226,10 @@ async def test_cosmos_storage_read_with_invalid_key_should_return_empty_dict(sel
         assert isinstance(data, dict)
         assert not data.keys()
 
-    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
-    @pytest.mark.asyncio
-    async def test_cosmos_storage_read_no_key_should_throw(self):
-        try:
-            await reset()
-            storage = CosmosDbStorage(COSMOS_DB_CONFIG)
-            await storage.read([])
-        except Exception as error:
-            assert error
-
-    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
-    @pytest.mark.asyncio
-    async def test_cosmos_storage_write_should_add_new_value(self):
-        await reset()
-        storage = CosmosDbStorage(COSMOS_DB_CONFIG)
-        await storage.write({"user": SimpleStoreItem(counter=1)})
-
-        data = await storage.read(["user"])
-        assert "user" in data
-        assert data["user"].counter == 1
-
     @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
     @pytest.mark.asyncio
     async def test_cosmos_storage_write_should_overwrite_when_new_e_tag_is_an_asterisk(
-        self
+        self,
     ):
         await reset()
         storage = CosmosDbStorage(COSMOS_DB_CONFIG)
@@ -169,66 +239,10 @@ async def test_cosmos_storage_write_should_overwrite_when_new_e_tag_is_an_asteri
         data = await storage.read(["user"])
         assert data["user"].counter == 10
 
-    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
-    @pytest.mark.asyncio
-    async def test_cosmos_storage_write_batch_operation(self):
-        await reset()
-        storage = CosmosDbStorage(COSMOS_DB_CONFIG)
-        await storage.write(
-            {
-                "batch1": SimpleStoreItem(counter=1),
-                "batch2": SimpleStoreItem(counter=1),
-                "batch3": SimpleStoreItem(counter=1),
-            }
-        )
-        data = await storage.read(["batch1", "batch2", "batch3"])
-        assert len(data.keys()) == 3
-        assert data["batch1"]
-        assert data["batch2"]
-        assert data["batch3"]
-        assert data["batch1"].counter == 1
-        assert data["batch2"].counter == 1
-        assert data["batch3"].counter == 1
-        assert data["batch1"].e_tag
-        assert data["batch2"].e_tag
-        assert data["batch3"].e_tag
-        await storage.delete(["batch1", "batch2", "batch3"])
-        data = await storage.read(["batch1", "batch2", "batch3"])
-        assert not data.keys()
-
-    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
-    @pytest.mark.asyncio
-    async def test_cosmos_storage_write_crazy_keys_work(self):
-        await reset()
-        storage = CosmosDbStorage(COSMOS_DB_CONFIG)
-        crazy_key = '!@#$%^&*()_+??><":QASD~`'
-        await storage.write({crazy_key: SimpleStoreItem(counter=1)})
-        data = await storage.read([crazy_key])
-        assert len(data.keys()) == 1
-        assert data[crazy_key]
-        assert data[crazy_key].counter == 1
-        assert data[crazy_key].e_tag
-
-    @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
-    @pytest.mark.asyncio
-    async def test_cosmos_storage_delete_should_delete_according_cached_data(self):
-        await reset()
-        storage = CosmosDbStorage(COSMOS_DB_CONFIG)
-        await storage.write({"test": SimpleStoreItem()})
-        try:
-            await storage.delete(["test"])
-        except Exception as error:
-            raise error
-        else:
-            data = await storage.read(["test"])
-
-            assert isinstance(data, dict)
-            assert not data.keys()
-
     @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
     @pytest.mark.asyncio
     async def test_cosmos_storage_delete_should_delete_multiple_values_when_given_multiple_valid_keys(
-        self
+        self,
     ):
         await reset()
         storage = CosmosDbStorage(COSMOS_DB_CONFIG)
@@ -241,7 +255,7 @@ async def test_cosmos_storage_delete_should_delete_multiple_values_when_given_mu
     @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
     @pytest.mark.asyncio
     async def test_cosmos_storage_delete_should_delete_values_when_given_multiple_valid_keys_and_ignore_other_data(
-        self
+        self,
     ):
         await reset()
         storage = CosmosDbStorage(COSMOS_DB_CONFIG)
@@ -260,7 +274,7 @@ async def test_cosmos_storage_delete_should_delete_values_when_given_multiple_va
     @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
     @pytest.mark.asyncio
     async def test_cosmos_storage_delete_invalid_key_should_do_nothing_and_not_affect_cached_data(
-        self
+        self,
     ):
         await reset()
         storage = CosmosDbStorage(COSMOS_DB_CONFIG)
@@ -275,7 +289,7 @@ async def test_cosmos_storage_delete_invalid_key_should_do_nothing_and_not_affec
     @pytest.mark.skipif(not EMULATOR_RUNNING, reason="Needs the emulator to run.")
     @pytest.mark.asyncio
     async def test_cosmos_storage_delete_invalid_keys_should_do_nothing_and_not_affect_cached_data(
-        self
+        self,
     ):
         await reset()
         storage = CosmosDbStorage(COSMOS_DB_CONFIG)
diff --git a/libraries/botbuilder-azure/tests/test_key_validation.py b/libraries/botbuilder-azure/tests/test_key_validation.py
index e9c1694ef..5cb8e9025 100644
--- a/libraries/botbuilder-azure/tests/test_key_validation.py
+++ b/libraries/botbuilder-azure/tests/test_key_validation.py
@@ -65,7 +65,7 @@ def test_should_not_truncate_short_key(self):
         assert len(fixed2) == 16, "short key was truncated improperly"
 
     def test_should_create_sufficiently_different_truncated_keys_of_similar_origin(
-        self
+        self,
     ):
         # create 2 very similar extra long key where the difference will definitely be trimmed off by truncate function
         long_key = "x" * 300 + "1"
diff --git a/libraries/botbuilder-core/README.rst b/libraries/botbuilder-core/README.rst
index 2fc7b5fe5..7cfe8e3f0 100644
--- a/libraries/botbuilder-core/README.rst
+++ b/libraries/botbuilder-core/README.rst
@@ -3,8 +3,8 @@
 BotBuilder-Core SDK for Python
 ==============================
 
-.. image:: https://fuselabs.visualstudio.com/SDK_v4/_apis/build/status/Python/SDK_v4-Python-CI?branchName=master
-   :target:  https://fuselabs.visualstudio.com/SDK_v4/_apis/build/status/Python/SDK_v4-Python-CI
+.. image:: https://dev.azure.com/FuseLabs/SDK_v4/_apis/build/status/Python/Python-CI-PR-yaml?branchName=master
+   :target:  https://dev.azure.com/FuseLabs/SDK_v4/_apis/build/status/Python/Python-CI-PR-yaml?branchName=master
    :align: right
    :alt: Azure DevOps status for master branch
 .. image:: https://badge.fury.io/py/botbuilder-core.svg
diff --git a/libraries/botbuilder-core/botbuilder/core/__init__.py b/libraries/botbuilder-core/botbuilder/core/__init__.py
index d6977c927..a596a2325 100644
--- a/libraries/botbuilder-core/botbuilder/core/__init__.py
+++ b/libraries/botbuilder-core/botbuilder/core/__init__.py
@@ -9,6 +9,7 @@
 from .about import __version__
 from .activity_handler import ActivityHandler
 from .auto_save_state_middleware import AutoSaveStateMiddleware
+from .bot import Bot
 from .bot_assert import BotAssert
 from .bot_adapter import BotAdapter
 from .bot_framework_adapter import BotFrameworkAdapter, BotFrameworkAdapterSettings
@@ -16,7 +17,11 @@
 from .bot_state_set import BotStateSet
 from .bot_telemetry_client import BotTelemetryClient, Severity
 from .card_factory import CardFactory
+from .channel_service_handler import BotActionNotImplementedError, ChannelServiceHandler
+from .component_registration import ComponentRegistration
 from .conversation_state import ConversationState
+from .oauth.extended_user_token_provider import ExtendedUserTokenProvider
+from .oauth.user_token_provider import UserTokenProvider
 from .intent_score import IntentScore
 from .invoke_response import InvokeResponse
 from .memory_storage import MemoryStorage
@@ -35,13 +40,19 @@
 from .telemetry_logger_constants import TelemetryLoggerConstants
 from .telemetry_logger_middleware import TelemetryLoggerMiddleware
 from .turn_context import TurnContext
+from .transcript_logger import TranscriptLogger, TranscriptLoggerMiddleware
 from .user_state import UserState
-from .user_token_provider import UserTokenProvider
+from .register_class_middleware import RegisterClassMiddleware
+from .adapter_extensions import AdapterExtensions
+from .healthcheck import HealthCheck
 
 __all__ = [
     "ActivityHandler",
+    "AdapterExtensions",
     "AnonymousReceiveMiddleware",
     "AutoSaveStateMiddleware",
+    "Bot",
+    "BotActionNotImplementedError",
     "BotAdapter",
     "BotAssert",
     "BotFrameworkAdapter",
@@ -51,8 +62,12 @@
     "BotTelemetryClient",
     "calculate_change_hash",
     "CardFactory",
+    "ChannelServiceHandler",
+    "ComponentRegistration",
     "ConversationState",
     "conversation_reference_extension",
+    "ExtendedUserTokenProvider",
+    "HealthCheck",
     "IntentScore",
     "InvokeResponse",
     "MemoryStorage",
@@ -62,6 +77,7 @@
     "MiddlewareSet",
     "NullTelemetryClient",
     "PrivateConversationState",
+    "RegisterClassMiddleware",
     "Recognizer",
     "RecognizerResult",
     "Severity",
@@ -74,6 +90,8 @@
     "TelemetryLoggerConstants",
     "TelemetryLoggerMiddleware",
     "TopIntent",
+    "TranscriptLogger",
+    "TranscriptLoggerMiddleware",
     "TurnContext",
     "UserState",
     "UserTokenProvider",
diff --git a/libraries/botbuilder-core/botbuilder/core/about.py b/libraries/botbuilder-core/botbuilder/core/about.py
index 6ec169c3a..d8fbaf9f3 100644
--- a/libraries/botbuilder-core/botbuilder/core/about.py
+++ b/libraries/botbuilder-core/botbuilder/core/about.py
@@ -1,14 +1,14 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import os
-
-__title__ = "botbuilder-core"
-__version__ = (
-    os.environ["packageVersion"] if "packageVersion" in os.environ else "4.4.0b1"
-)
-__uri__ = "https://www.github.com/Microsoft/botbuilder-python"
-__author__ = "Microsoft"
-__description__ = "Microsoft Bot Framework Bot Builder"
-__summary__ = "Microsoft Bot Framework Bot Builder SDK for Python."
-__license__ = "MIT"
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+
+__title__ = "botbuilder-core"
+__version__ = (
+    os.environ["packageVersion"] if "packageVersion" in os.environ else "4.12.0"
+)
+__uri__ = "https://www.github.com/Microsoft/botbuilder-python"
+__author__ = "Microsoft"
+__description__ = "Microsoft Bot Framework Bot Builder"
+__summary__ = "Microsoft Bot Framework Bot Builder SDK for Python."
+__license__ = "MIT"
diff --git a/libraries/botbuilder-core/botbuilder/core/activity_handler.py b/libraries/botbuilder-core/botbuilder/core/activity_handler.py
index adc51ba5c..28c924e0f 100644
--- a/libraries/botbuilder-core/botbuilder/core/activity_handler.py
+++ b/libraries/botbuilder-core/botbuilder/core/activity_handler.py
@@ -1,13 +1,56 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License.
-from typing import List
+from http import HTTPStatus
+from typing import List, Union
 
-from botbuilder.schema import ActivityTypes, ChannelAccount, MessageReaction
+from botbuilder.schema import (
+    Activity,
+    ActivityTypes,
+    ChannelAccount,
+    MessageReaction,
+    SignInConstants,
+    HealthCheckResponse,
+)
+
+from .bot import Bot
+from .bot_adapter import BotAdapter
+from .healthcheck import HealthCheck
+from .serializer_helper import serializer_helper
+from .bot_framework_adapter import BotFrameworkAdapter
+from .invoke_response import InvokeResponse
 from .turn_context import TurnContext
 
 
-class ActivityHandler:
-    async def on_turn(self, turn_context: TurnContext):
+class ActivityHandler(Bot):
+    """
+    Handles activities and should be subclassed.
+
+    .. remarks::
+        Derive from this class to handle particular activity types.
+        Yon can add pre and post processing of activities by calling the base class
+        in the derived class.
+    """
+
+    async def on_turn(
+        self, turn_context: TurnContext
+    ):  # pylint: disable=arguments-differ
+        """
+        Called by the adapter (for example, :class:`BotFrameworkAdapter`) at runtime
+        in order to process an inbound :class:`botbuilder.schema.Activity`.
+
+        :param turn_context: The context object for this turn
+        :type turn_context: :class:`botbuilder.core.TurnContext`
+
+        :returns: A task that represents the work queued to execute
+
+        .. remarks::
+            It calls other methods in this class based on the type of the activity to
+            process, which allows a derived class to provide type-specific logic in a controlled way.
+            In a derived class, override this method to add logic that applies to all activity types.
+            Also
+            - Add logic to apply before the type-specific logic and before calling :meth:`on_turn()`.
+            - Add logic to apply after the type-specific logic after calling :meth:`on_turn()`.
+        """
         if turn_context is None:
             raise TypeError("ActivityHandler.on_turn(): turn_context cannot be None.")
 
@@ -32,15 +75,60 @@ async def on_turn(self, turn_context: TurnContext):
             await self.on_message_reaction_activity(turn_context)
         elif turn_context.activity.type == ActivityTypes.event:
             await self.on_event_activity(turn_context)
+        elif turn_context.activity.type == ActivityTypes.invoke:
+            invoke_response = await self.on_invoke_activity(turn_context)
+
+            # If OnInvokeActivityAsync has already sent an InvokeResponse, do not send another one.
+            if invoke_response and not turn_context.turn_state.get(
+                BotFrameworkAdapter._INVOKE_RESPONSE_KEY  # pylint: disable=protected-access
+            ):
+                await turn_context.send_activity(
+                    Activity(value=invoke_response, type=ActivityTypes.invoke_response)
+                )
+        elif turn_context.activity.type == ActivityTypes.end_of_conversation:
+            await self.on_end_of_conversation_activity(turn_context)
+        elif turn_context.activity.type == ActivityTypes.typing:
+            await self.on_typing_activity(turn_context)
+        elif turn_context.activity.type == ActivityTypes.installation_update:
+            await self.on_installation_update(turn_context)
         else:
             await self.on_unrecognized_activity_type(turn_context)
 
     async def on_message_activity(  # pylint: disable=unused-argument
         self, turn_context: TurnContext
     ):
+        """
+        Override this method in a derived class to provide logic specific to activities,
+        such as the conversational logic.
+
+        :param turn_context: The context object for this turn
+        :type turn_context: :class:`botbuilder.core.TurnContext`
+
+        :returns: A task that represents the work queued to execute
+        """
         return
 
     async def on_conversation_update_activity(self, turn_context: TurnContext):
+        """
+        Invoked when a conversation update activity is received from the channel when the base behavior of
+        :meth:`on_turn()` is used.
+
+        :param turn_context: The context object for this turn
+        :type turn_context: :class:`botbuilder.core.TurnContext`
+
+        :returns: A task that represents the work queued to execute
+
+        .. remarks::
+            When the :meth:`on_turn()` method receives a conversation update activity, it calls this
+            method.
+            Also
+            - If the conversation update activity indicates that members other than the bot joined the conversation,
+            it calls the  :meth:`on_members_added_activity()` method.
+            - If the conversation update activity indicates that members other than the bot left the conversation,
+            it calls the  :meth:`on_members_removed_activity()`  method.
+            - In a derived class, override this method to add logic that applies to all conversation update activities.
+            Add logic to apply before the member added or removed logic before the call to this base class method.
+        """
         if (
             turn_context.activity.members_added is not None
             and turn_context.activity.members_added
@@ -58,16 +146,80 @@ async def on_conversation_update_activity(self, turn_context: TurnContext):
         return
 
     async def on_members_added_activity(
-        self, members_added: ChannelAccount, turn_context: TurnContext
+        self, members_added: List[ChannelAccount], turn_context: TurnContext
     ):  # pylint: disable=unused-argument
+        """
+        Override this method in a derived class to provide logic for when members other than the bot join
+        the conversation. You can add your bot's welcome logic.
+
+        :param members_added: A list of all the members added to the conversation, as described by the
+        conversation update activity
+        :type members_added: :class:`typing.List`
+        :param turn_context: The context object for this turn
+        :type turn_context: :class:`botbuilder.core.TurnContext`
+
+        :returns: A task that represents the work queued to execute
+
+        .. remarks::
+            When the :meth:`on_conversation_update_activity()` method receives a conversation
+            update activity that indicates
+            one or more users other than the bot are joining the conversation, it calls this method.
+        """
         return
 
     async def on_members_removed_activity(
-        self, members_removed: ChannelAccount, turn_context: TurnContext
+        self, members_removed: List[ChannelAccount], turn_context: TurnContext
     ):  # pylint: disable=unused-argument
+        """
+        Override this method in a derived class to provide logic for when members other than the bot leave
+        the conversation.  You can add your bot's good-bye logic.
+
+        :param members_added: A list of all the members removed from the conversation, as described by the
+        conversation update activity
+        :type members_added: :class:`typing.List`
+        :param turn_context: The context object for this turn
+        :type turn_context: :class:`botbuilder.core.TurnContext`
+
+        :returns: A task that represents the work queued to execute
+
+        .. remarks::
+            When the :meth:`on_conversation_update_activity()` method receives a conversation
+            update activity that indicates one or more users other than the bot are leaving the conversation,
+            it calls this method.
+        """
+
         return
 
     async def on_message_reaction_activity(self, turn_context: TurnContext):
+        """
+        Invoked when an event activity is received from the connector when the base behavior of
+        :meth:`on_turn()` is used.
+
+        :param turn_context: The context object for this turn
+        :type turn_context: :class:`botbuilder.core.TurnContext`
+
+        :returns: A task that represents the work queued to execute
+
+        .. remarks::
+            Message reactions correspond to the user adding a 'like' or 'sad' etc. (often an emoji) to a previously
+            sent activity.
+
+            Message reactions are only supported by a few channels. The activity that the message reaction corresponds
+            to is indicated in the reply to Id property. The value of this property is the activity id of a previously
+            sent activity given back to the bot as the response from a send call.
+            When the :meth:`on_turn()` method receives a message reaction activity, it calls this
+            method.
+
+            - If the message reaction indicates that reactions were added to a message, it calls
+            :meth:`on_reaction_added()`.
+            - If the message reaction indicates that reactions were removed from a message, it calls
+            :meth:`on_reaction_removed()`.
+
+            In a derived class, override this method to add logic that applies to all message reaction activities.
+            Add logic to apply before the reactions added or removed logic before the call to the this base class
+            method.
+            Add logic to apply after the reactions added or removed logic after the call to the this base class method.
+        """
         if turn_context.activity.reactions_added is not None:
             await self.on_reactions_added(
                 turn_context.activity.reactions_added, turn_context
@@ -81,15 +233,74 @@ async def on_message_reaction_activity(self, turn_context: TurnContext):
     async def on_reactions_added(  # pylint: disable=unused-argument
         self, message_reactions: List[MessageReaction], turn_context: TurnContext
     ):
+        """
+        Override this method in a derived class to provide logic for when reactions to a previous activity
+        are added to the conversation.
+
+        :param message_reactions: The list of reactions added
+        :type message_reactions: :class:`typing.List`
+        :param turn_context: The context object for this turn
+        :type turn_context: :class:`botbuilder.core.TurnContext`
+
+        :returns: A task that represents the work queued to execute
+
+        .. remarks::
+            Message reactions correspond to the user adding a 'like' or 'sad' etc. (often an emoji)
+            to a previously sent message on the conversation.
+            Message reactions are supported by only a few channels.
+            The activity that the message is in reaction to is identified by the activity's reply to ID property.
+            The value of this property is the activity ID of a previously sent activity. When the bot sends an activity,
+            the channel assigns an ID to it, which is available in the resource response Id of the result.
+        """
         return
 
     async def on_reactions_removed(  # pylint: disable=unused-argument
         self, message_reactions: List[MessageReaction], turn_context: TurnContext
     ):
+        """
+        Override this method in a derived class to provide logic for when reactions to a previous activity
+        are removed from the conversation.
+
+        :param message_reactions: The list of reactions removed
+        :type message_reactions: :class:`typing.List`
+        :param turn_context: The context object for this turn
+        :type turn_context: :class:`botbuilder.core.TurnContext`
+
+        :returns: A task that represents the work queued to execute
+
+        .. remarks::
+            Message reactions correspond to the user adding a 'like' or 'sad' etc. (often an emoji)
+            to a previously sent message on the conversation. Message reactions are supported by only a few channels.
+            The activity that the message is in reaction to is identified by the activity's reply to Id property.
+            The value of this property is the activity ID of a previously sent activity. When the bot sends an activity,
+            the channel assigns an ID to it, which is available in the resource response Id of the result.
+        """
         return
 
     async def on_event_activity(self, turn_context: TurnContext):
-        if turn_context.activity.name == "tokens/response":
+        """
+        Invoked when an event activity is received from the connector when the base behavior of
+        :meth:`on_turn()` is used.
+
+        :param turn_context: The context object for this turn
+        :type turn_context: :class:`botbuilder.core.TurnContext`
+
+        :returns: A task that represents the work queued to execute
+
+        .. remarks::
+            When the :meth:`on_turn()` method receives an event activity, it calls this method.
+            If the activity name is `tokens/response`, it calls :meth:`on_token_response_event()`;
+            otherwise, it calls :meth:`on_event()`.
+
+            In a derived class, override this method to add logic that applies to all event activities.
+            Add logic to apply before the specific event-handling logic before the call to this base class method.
+            Add logic to apply after the specific event-handling logic after the call to this base class method.
+
+            Event activities communicate programmatic information from a client or channel to a bot.
+            The meaning of an event activity is defined by the event activity name property, which is meaningful within
+            the scope of a channel.
+        """
+        if turn_context.activity.name == SignInConstants.token_response_event_name:
             return await self.on_token_response_event(turn_context)
 
         return await self.on_event(turn_context)
@@ -97,14 +308,201 @@ async def on_event_activity(self, turn_context: TurnContext):
     async def on_token_response_event(  # pylint: disable=unused-argument
         self, turn_context: TurnContext
     ):
+        """
+        Invoked when a `tokens/response` event is received when the base behavior of
+        :meth:`on_event_activity()` is used.
+        If using an `oauth_prompt`, override this method to forward this activity to the current dialog.
+
+        :param turn_context: The context object for this turn
+        :type turn_context: :class:`botbuilder.core.TurnContext`
+
+        :returns: A task that represents the work queued to execute
+
+        .. remarks::
+            When the :meth:`on_event()` method receives an event with an activity name of
+            `tokens/response`, it calls this method. If your bot uses an `oauth_prompt`, forward the incoming
+            activity to the current dialog.
+        """
         return
 
     async def on_event(  # pylint: disable=unused-argument
         self, turn_context: TurnContext
     ):
+        """
+        Invoked when an event other than `tokens/response` is received when the base behavior of
+        :meth:`on_event_activity()` is used.
+
+
+        :param turn_context: The context object for this turn
+        :type turn_context: :class:`botbuilder.core.TurnContext`
+
+        :returns: A task that represents the work queued to execute
+
+        .. remarks::
+            When the :meth:`on_event_activity()` is used method receives an event with an
+            activity name other than `tokens/response`, it calls this method.
+            This method could optionally be overridden if the bot is meant to handle miscellaneous events.
+        """
+        return
+
+    async def on_end_of_conversation_activity(  # pylint: disable=unused-argument
+        self, turn_context: TurnContext
+    ):
+        """
+        Invoked when a conversation end activity is received from the channel.
+
+        :param turn_context: The context object for this turn
+        :type turn_context: :class:`botbuilder.core.TurnContext`
+        :returns: A task that represents the work queued to execute
+        """
+        return
+
+    async def on_typing_activity(  # pylint: disable=unused-argument
+        self, turn_context: TurnContext
+    ):
+        """
+        Override this in a derived class to provide logic specific to
+        ActivityTypes.typing activities, such as the conversational logic.
+
+        :param turn_context: The context object for this turn
+        :type turn_context: :class:`botbuilder.core.TurnContext`
+        :returns: A task that represents the work queued to execute
+        """
+        return
+
+    async def on_installation_update(  # pylint: disable=unused-argument
+        self, turn_context: TurnContext
+    ):
+        """
+        Override this in a derived class to provide logic specific to
+        ActivityTypes.InstallationUpdate activities.
+
+        :param turn_context: The context object for this turn
+        :type turn_context: :class:`botbuilder.core.TurnContext`
+        :returns: A task that represents the work queued to execute
+        """
+        if turn_context.activity.action in ("add", "add-upgrade"):
+            return await self.on_installation_update_add(turn_context)
+        if turn_context.activity.action in ("remove", "remove-upgrade"):
+            return await self.on_installation_update_remove(turn_context)
+        return
+
+    async def on_installation_update_add(  # pylint: disable=unused-argument
+        self, turn_context: TurnContext
+    ):
+        """
+        Override this in a derived class to provide logic specific to
+        ActivityTypes.InstallationUpdate activities with 'action' set to 'add'.
+
+        :param turn_context: The context object for this turn
+        :type turn_context: :class:`botbuilder.core.TurnContext`
+        :returns: A task that represents the work queued to execute
+        """
+        return
+
+    async def on_installation_update_remove(  # pylint: disable=unused-argument
+        self, turn_context: TurnContext
+    ):
+        """
+        Override this in a derived class to provide logic specific to
+        ActivityTypes.InstallationUpdate activities with 'action' set to 'remove'.
+
+        :param turn_context: The context object for this turn
+        :type turn_context: :class:`botbuilder.core.TurnContext`
+        :returns: A task that represents the work queued to execute
+        """
         return
 
     async def on_unrecognized_activity_type(  # pylint: disable=unused-argument
         self, turn_context: TurnContext
     ):
+        """
+        Invoked when an activity other than a message, conversation update, or event is received when the base
+        behavior of :meth:`on_turn()` is used.
+        If overridden, this method could potentially respond to any of the other activity types.
+
+        :param turn_context: The context object for this turn
+        :type turn_context: :class:`botbuilder.core.TurnContext`
+
+        :returns: A task that represents the work queued to execute
+
+        .. remarks::
+            When the :meth:`on_turn()` method receives an activity that is not a message,
+            conversation update, message reaction, or event activity, it calls this method.
+        """
         return
+
+    async def on_invoke_activity(  # pylint: disable=unused-argument
+        self, turn_context: TurnContext
+    ) -> Union[InvokeResponse, None]:
+        """
+        Registers an activity event handler for the _invoke_ event, emitted for every incoming event activity.
+
+        :param turn_context: The context object for this turn
+        :type turn_context: :class:`botbuilder.core.TurnContext`
+
+        :returns: A task that represents the work queued to execute
+        """
+        try:
+            if (
+                turn_context.activity.name
+                == SignInConstants.verify_state_operation_name
+                or turn_context.activity.name
+                == SignInConstants.token_exchange_operation_name
+            ):
+                await self.on_sign_in_invoke(turn_context)
+                return self._create_invoke_response()
+
+            if turn_context.activity.name == "healthcheck":
+                return self._create_invoke_response(
+                    await self.on_healthcheck(turn_context)
+                )
+
+            raise _InvokeResponseException(HTTPStatus.NOT_IMPLEMENTED)
+        except _InvokeResponseException as invoke_exception:
+            return invoke_exception.create_invoke_response()
+
+    async def on_sign_in_invoke(  # pylint: disable=unused-argument
+        self, turn_context: TurnContext
+    ):
+        """
+        Invoked when a signin/verifyState or signin/tokenExchange event is received when the base behavior of
+        on_invoke_activity(TurnContext{InvokeActivity}) is used.
+        If using an OAuthPrompt, override this method to forward this Activity"/ to the current dialog.
+        By default, this method does nothing.
+
+        :param turn_context: The context object for this turn
+        :type turn_context: :class:`botbuilder.core.TurnContext`
+
+        :returns: A task that represents the work queued to execute
+        """
+        raise _InvokeResponseException(HTTPStatus.NOT_IMPLEMENTED)
+
+    async def on_healthcheck(self, turn_context: TurnContext) -> HealthCheckResponse:
+        """
+        Invoked when the bot is sent a health check from the hosting infrastructure or, in the case of
+        Skills the parent bot. By default, this method acknowledges the health state of the bot.
+
+        When the on_invoke_activity method receives an Invoke with a Activity.name of `healthCheck`, it
+        calls this method.
+
+        :param turn_context: A context object for this turn.
+        :return: The HealthCheckResponse object
+        """
+        return HealthCheck.create_healthcheck_response(
+            turn_context.turn_state.get(BotAdapter.BOT_CONNECTOR_CLIENT_KEY)
+        )
+
+    @staticmethod
+    def _create_invoke_response(body: object = None) -> InvokeResponse:
+        return InvokeResponse(status=int(HTTPStatus.OK), body=serializer_helper(body))
+
+
+class _InvokeResponseException(Exception):
+    def __init__(self, status_code: HTTPStatus, body: object = None):
+        super(_InvokeResponseException, self).__init__()
+        self._status_code = status_code
+        self._body = body
+
+    def create_invoke_response(self) -> InvokeResponse:
+        return InvokeResponse(status=int(self._status_code), body=self._body)
diff --git a/libraries/botbuilder-core/botbuilder/core/adapter_extensions.py b/libraries/botbuilder-core/botbuilder/core/adapter_extensions.py
new file mode 100644
index 000000000..db13d74b5
--- /dev/null
+++ b/libraries/botbuilder-core/botbuilder/core/adapter_extensions.py
@@ -0,0 +1,99 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+from warnings import warn
+
+from botbuilder.core import (
+    BotAdapter,
+    BotState,
+    Storage,
+    RegisterClassMiddleware,
+    UserState,
+    ConversationState,
+    AutoSaveStateMiddleware,
+)
+
+
+class AdapterExtensions:
+    @staticmethod
+    def use_storage(adapter: BotAdapter, storage: Storage) -> BotAdapter:
+        """
+        Registers a storage layer with the adapter. The storage object will be available via the turn context's
+        `turn_state` property.
+
+        :param adapter: The BotAdapter on which to register the storage object.
+        :param storage: The Storage object to register.
+        :return: The BotAdapter
+        """
+        return adapter.use(RegisterClassMiddleware(storage))
+
+    @staticmethod
+    def use_bot_state(
+        bot_adapter: BotAdapter, *bot_states: BotState, auto: bool = True
+    ) -> BotAdapter:
+        """
+        Registers bot state object into the TurnContext. The botstate will be available via the turn context.
+
+        :param bot_adapter: The BotAdapter on which to register the state objects.
+        :param bot_states: One or more BotState objects to register.
+        :return: The updated adapter.
+        """
+        if not bot_states:
+            raise TypeError("At least one BotAdapter is required")
+
+        for bot_state in bot_states:
+            bot_adapter.use(
+                RegisterClassMiddleware(
+                    bot_state, AdapterExtensions.fullname(bot_state)
+                )
+            )
+
+        if auto:
+            bot_adapter.use(AutoSaveStateMiddleware(bot_states))
+
+        return bot_adapter
+
+    @staticmethod
+    def fullname(obj):
+        module = obj.__class__.__module__
+        if module is None or module == str.__class__.__module__:
+            return obj.__class__.__name__  # Avoid reporting __builtin__
+        return module + "." + obj.__class__.__name__
+
+    @staticmethod
+    def use_state(
+        adapter: BotAdapter,
+        user_state: UserState,
+        conversation_state: ConversationState,
+        auto: bool = True,
+    ) -> BotAdapter:
+        """
+        [DEPRECATED] Registers user and conversation state objects with the adapter. These objects will be available via
+        the turn context's `turn_state` property.
+
+        :param adapter: The BotAdapter on which to register the state objects.
+        :param user_state: The UserState object to register.
+        :param conversation_state: The ConversationState object to register.
+        :param auto: True to automatically persist state each turn.
+        :return: The BotAdapter
+        """
+        warn(
+            "This method is deprecated in 4.9. You should use the method .use_bot_state() instead.",
+            DeprecationWarning,
+        )
+
+        if not adapter:
+            raise TypeError("BotAdapter is required")
+
+        if not user_state:
+            raise TypeError("UserState is required")
+
+        if not conversation_state:
+            raise TypeError("ConversationState is required")
+
+        adapter.use(RegisterClassMiddleware(user_state))
+        adapter.use(RegisterClassMiddleware(conversation_state))
+
+        if auto:
+            adapter.use(AutoSaveStateMiddleware([user_state, conversation_state]))
+
+        return adapter
diff --git a/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py b/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py
index 41bcb3e50..6488a2726 100644
--- a/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py
+++ b/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py
@@ -1,453 +1,710 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-# TODO: enable this in the future
-# With python 3.7 the line below will allow to do Postponed Evaluation of Annotations. See PEP 563
-# from __future__ import annotations
-
-import asyncio
-import inspect
-from datetime import datetime
-from typing import Awaitable, Coroutine, Dict, List, Callable, Union
-from copy import copy
-from threading import Lock
-from botbuilder.schema import (
-    ActivityTypes,
-    Activity,
-    ConversationAccount,
-    ConversationReference,
-    ChannelAccount,
-    ResourceResponse,
-    TokenResponse,
-)
-from ..bot_adapter import BotAdapter
-from ..turn_context import TurnContext
-from ..user_token_provider import UserTokenProvider
-
-
-class UserToken:
-    def __init__(
-        self,
-        connection_name: str = None,
-        user_id: str = None,
-        channel_id: str = None,
-        token: str = None,
-    ):
-        self.connection_name = connection_name
-        self.user_id = user_id
-        self.channel_id = channel_id
-        self.token = token
-
-    def equals_key(self, rhs: "UserToken"):
-        return (
-            rhs is not None
-            and self.connection_name == rhs.connection_name
-            and self.user_id == rhs.user_id
-            and self.channel_id == rhs.channel_id
-        )
-
-
-class TokenMagicCode:
-    def __init__(self, key: UserToken = None, magic_code: str = None):
-        self.key = key
-        self.magic_code = magic_code
-
-
-class TestAdapter(BotAdapter, UserTokenProvider):
-    def __init__(
-        self,
-        logic: Coroutine = None,
-        template_or_conversation: Union[Activity, ConversationReference] = None,
-        send_trace_activities: bool = False,
-    ):  # pylint: disable=unused-argument
-        """
-        Creates a new TestAdapter instance.
-        :param logic:
-        :param conversation: A reference to the conversation to begin the adapter state with.
-        """
-        super(TestAdapter, self).__init__()
-        self.logic = logic
-        self._next_id: int = 0
-        self._user_tokens: List[UserToken] = []
-        self._magic_codes: List[TokenMagicCode] = []
-        self._conversation_lock = Lock()
-        self.activity_buffer: List[Activity] = []
-        self.updated_activities: List[Activity] = []
-        self.deleted_activities: List[ConversationReference] = []
-        self.send_trace_activities = send_trace_activities
-
-        self.template = (
-            template_or_conversation
-            if isinstance(template_or_conversation, Activity)
-            else Activity(
-                channel_id="test",
-                service_url="https://test.com",
-                from_property=ChannelAccount(id="User1", name="user"),
-                recipient=ChannelAccount(id="bot", name="Bot"),
-                conversation=ConversationAccount(id="Convo1"),
-            )
-        )
-
-        if isinstance(template_or_conversation, ConversationReference):
-            self.template.channel_id = template_or_conversation.channel_id
-
-    async def process_activity(
-        self, activity: Activity, logic: Callable[[TurnContext], Awaitable]
-    ):
-        self._conversation_lock.acquire()
-        try:
-            # ready for next reply
-            if activity.type is None:
-                activity.type = ActivityTypes.message
-
-            activity.channel_id = self.template.channel_id
-            activity.from_property = self.template.from_property
-            activity.recipient = self.template.recipient
-            activity.conversation = self.template.conversation
-            activity.service_url = self.template.service_url
-
-            activity.id = str((self._next_id))
-            self._next_id += 1
-        finally:
-            self._conversation_lock.release()
-
-        activity.timestamp = activity.timestamp or datetime.utcnow()
-        await self.run_pipeline(TurnContext(self, activity), logic)
-
-    async def send_activities(self, context, activities: List[Activity]):
-        """
-        INTERNAL: called by the logic under test to send a set of activities. These will be buffered
-        to the current `TestFlow` instance for comparison against the expected results.
-        :param context:
-        :param activities:
-        :return:
-        """
-
-        def id_mapper(activity):
-            self.activity_buffer.append(activity)
-            self._next_id += 1
-            return ResourceResponse(id=str(self._next_id))
-
-        return [
-            id_mapper(activity)
-            for activity in activities
-            if self.send_trace_activities or activity.type != "trace"
-        ]
-
-    async def delete_activity(self, context, reference: ConversationReference):
-        """
-        INTERNAL: called by the logic under test to delete an existing activity. These are simply
-        pushed onto a [deletedActivities](#deletedactivities) array for inspection after the turn
-        completes.
-        :param reference:
-        :return:
-        """
-        self.deleted_activities.append(reference)
-
-    async def update_activity(self, context, activity: Activity):
-        """
-        INTERNAL: called by the logic under test to replace an existing activity. These are simply
-        pushed onto an [updatedActivities](#updatedactivities) array for inspection after the turn
-        completes.
-        :param activity:
-        :return:
-        """
-        self.updated_activities.append(activity)
-
-    async def continue_conversation(
-        self, bot_id: str, reference: ConversationReference, callback: Callable
-    ):
-        """
-        The `TestAdapter` just calls parent implementation.
-        :param bot_id
-        :param reference:
-        :param callback:
-        :return:
-        """
-        await super().continue_conversation(bot_id, reference, callback)
-
-    async def receive_activity(self, activity):
-        """
-        INTERNAL: called by a `TestFlow` instance to simulate a user sending a message to the bot.
-        This will cause the adapters middleware pipe to be run and it's logic to be called.
-        :param activity:
-        :return:
-        """
-        if isinstance(activity, str):
-            activity = Activity(type="message", text=activity)
-        # Initialize request.
-        request = copy(self.template)
-
-        for key, value in vars(activity).items():
-            if value is not None and key != "additional_properties":
-                setattr(request, key, value)
-
-        request.type = request.type or ActivityTypes.message
-        if not request.id:
-            self._next_id += 1
-            request.id = str(self._next_id)
-
-        # Create context object and run middleware.
-        context = TurnContext(self, request)
-        return await self.run_pipeline(context, self.logic)
-
-    def get_next_activity(self) -> Activity:
-        return self.activity_buffer.pop(0)
-
-    async def send(self, user_says) -> object:
-        """
-        Sends something to the bot. This returns a new `TestFlow` instance which can be used to add
-        additional steps for inspecting the bots reply and then sending additional activities.
-        :param user_says:
-        :return: A new instance of the TestFlow object
-        """
-        return TestFlow(await self.receive_activity(user_says), self)
-
-    async def test(
-        self, user_says, expected, description=None, timeout=None
-    ) -> "TestFlow":
-        """
-        Send something to the bot and expects the bot to return with a given reply. This is simply a
-        wrapper around calls to `send()` and `assertReply()`. This is such a common pattern that a
-        helper is provided.
-        :param user_says:
-        :param expected:
-        :param description:
-        :param timeout:
-        :return:
-        """
-        test_flow = await self.send(user_says)
-        test_flow = await test_flow.assert_reply(expected, description, timeout)
-        return test_flow
-
-    async def tests(self, *args):
-        """
-        Support multiple test cases without having to manually call `test()` repeatedly. This is a
-        convenience layer around the `test()`. Valid args are either lists or tuples of parameters
-        :param args:
-        :return:
-        """
-        for arg in args:
-            description = None
-            timeout = None
-            if len(arg) >= 3:
-                description = arg[2]
-                if len(arg) == 4:
-                    timeout = arg[3]
-            await self.test(arg[0], arg[1], description, timeout)
-
-    def add_user_token(
-        self,
-        connection_name: str,
-        channel_id: str,
-        user_id: str,
-        token: str,
-        magic_code: str = None,
-    ):
-        key = UserToken()
-        key.channel_id = channel_id
-        key.connection_name = connection_name
-        key.user_id = user_id
-        key.token = token
-
-        if not magic_code:
-            self._user_tokens.append(key)
-        else:
-            code = TokenMagicCode()
-            code.key = key
-            code.magic_code = magic_code
-            self._magic_codes.append(code)
-
-    async def get_user_token(
-        self, context: TurnContext, connection_name: str, magic_code: str = None
-    ) -> TokenResponse:
-        key = UserToken()
-        key.channel_id = context.activity.channel_id
-        key.connection_name = connection_name
-        key.user_id = context.activity.from_property.id
-
-        if magic_code:
-            magic_code_record = list(
-                filter(lambda x: key.equals_key(x.key), self._magic_codes)
-            )
-            if magic_code_record and magic_code_record[0].magic_code == magic_code:
-                # Move the token to long term dictionary.
-                self.add_user_token(
-                    connection_name,
-                    key.channel_id,
-                    key.user_id,
-                    magic_code_record[0].key.token,
-                )
-
-                # Remove from the magic code list.
-                idx = self._magic_codes.index(magic_code_record[0])
-                self._magic_codes = [self._magic_codes.pop(idx)]
-
-        match = [token for token in self._user_tokens if key.equals_key(token)]
-
-        if match:
-            return TokenResponse(
-                connection_name=match[0].connection_name,
-                token=match[0].token,
-                expiration=None,
-            )
-        # Not found.
-        return None
-
-    async def sign_out_user(
-        self, context: TurnContext, connection_name: str, user_id: str = None
-    ):
-        channel_id = context.activity.channel_id
-        user_id = context.activity.from_property.id
-
-        new_records = []
-        for token in self._user_tokens:
-            if (
-                token.channel_id != channel_id
-                or token.user_id != user_id
-                or (connection_name and connection_name != token.connection_name)
-            ):
-                new_records.append(token)
-        self._user_tokens = new_records
-
-    async def get_oauth_sign_in_link(
-        self, context: TurnContext, connection_name: str
-    ) -> str:
-        return (
-            f"https://fake.com/oauthsignin"
-            f"/{connection_name}/{context.activity.channel_id}/{context.activity.from_property.id}"
-        )
-
-    async def get_aad_tokens(
-        self, context: TurnContext, connection_name: str, resource_urls: List[str]
-    ) -> Dict[str, TokenResponse]:
-        return None
-
-
-class TestFlow:
-    def __init__(self, previous: Callable, adapter: TestAdapter):
-        """
-        INTERNAL: creates a new TestFlow instance.
-        :param previous:
-        :param adapter:
-        """
-        self.previous = previous
-        self.adapter = adapter
-
-    async def test(
-        self, user_says, expected, description=None, timeout=None
-    ) -> "TestFlow":
-        """
-        Send something to the bot and expects the bot to return with a given reply. This is simply a
-        wrapper around calls to `send()` and `assertReply()`. This is such a common pattern that a
-        helper is provided.
-        :param user_says:
-        :param expected:
-        :param description:
-        :param timeout:
-        :return:
-        """
-        test_flow = await self.send(user_says)
-        return await test_flow.assert_reply(
-            expected, description or f'test("{user_says}", "{expected}")', timeout
-        )
-
-    async def send(self, user_says) -> "TestFlow":
-        """
-        Sends something to the bot.
-        :param user_says:
-        :return:
-        """
-
-        async def new_previous():
-            nonlocal self, user_says
-            if callable(self.previous):
-                await self.previous()
-            await self.adapter.receive_activity(user_says)
-
-        return TestFlow(await new_previous(), self.adapter)
-
-    async def assert_reply(
-        self,
-        expected: Union[str, Activity, Callable[[Activity, str], None]],
-        description=None,
-        timeout=None,  # pylint: disable=unused-argument
-        is_substring=False,
-    ) -> "TestFlow":
-        """
-        Generates an assertion if the bots response doesn't match the expected text/activity.
-        :param expected:
-        :param description:
-        :param timeout:
-        :param is_substring:
-        :return:
-        """
-        # TODO: refactor method so expected can take a Callable[[Activity], None]
-        def default_inspector(reply, description=None):
-            if isinstance(expected, Activity):
-                validate_activity(reply, expected)
-            else:
-                assert reply.type == "message", description + f" type == {reply.type}"
-                if is_substring:
-                    assert expected in reply.text.strip(), (
-                        description + f" text == {reply.text}"
-                    )
-                else:
-                    assert reply.text.strip() == expected.strip(), (
-                        description + f" text == {reply.text}"
-                    )
-
-        if description is None:
-            description = ""
-
-        inspector = expected if callable(expected) else default_inspector
-
-        async def test_flow_previous():
-            nonlocal timeout
-            if not timeout:
-                timeout = 3000
-            start = datetime.now()
-            adapter = self.adapter
-
-            async def wait_for_activity():
-                nonlocal expected, timeout
-                current = datetime.now()
-                if (current - start).total_seconds() * 1000 > timeout:
-                    if isinstance(expected, Activity):
-                        expecting = expected.text
-                    elif callable(expected):
-                        expecting = inspect.getsourcefile(expected)
-                    else:
-                        expecting = str(expected)
-                    raise RuntimeError(
-                        f"TestAdapter.assert_reply({expecting}): {description} Timed out after "
-                        f"{current - start}ms."
-                    )
-                if adapter.activity_buffer:
-                    reply = adapter.activity_buffer.pop(0)
-                    try:
-                        await inspector(reply, description)
-                    except Exception:
-                        inspector(reply, description)
-
-                else:
-                    await asyncio.sleep(0.05)
-                    await wait_for_activity()
-
-            await wait_for_activity()
-
-        return TestFlow(await test_flow_previous(), self.adapter)
-
-
-def validate_activity(activity, expected) -> None:
-    """
-    Helper method that compares activities
-    :param activity:
-    :param expected:
-    :return:
-    """
-    iterable_expected = vars(expected).items()
-
-    for attr, value in iterable_expected:
-        if value is not None and attr != "additional_properties":
-            assert value == getattr(activity, attr)
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+# TODO: enable this in the future
+# With python 3.7 the line below will allow to do Postponed Evaluation of Annotations. See PEP 563
+# from __future__ import annotations
+
+import asyncio
+import inspect
+import uuid
+from datetime import datetime
+from uuid import uuid4
+from typing import Awaitable, Coroutine, Dict, List, Callable, Union
+from copy import copy
+from threading import Lock
+from botframework.connector.auth import AppCredentials, ClaimsIdentity
+from botframework.connector.token_api.models import (
+    SignInUrlResponse,
+    TokenExchangeResource,
+    TokenExchangeRequest,
+)
+from botbuilder.schema import (
+    ActivityTypes,
+    Activity,
+    ConversationAccount,
+    ConversationReference,
+    ChannelAccount,
+    ResourceResponse,
+    TokenResponse,
+)
+from ..bot_adapter import BotAdapter
+from ..turn_context import TurnContext
+from ..oauth.extended_user_token_provider import ExtendedUserTokenProvider
+
+
+class UserToken:
+    def __init__(
+        self,
+        connection_name: str = None,
+        user_id: str = None,
+        channel_id: str = None,
+        token: str = None,
+    ):
+        self.connection_name = connection_name
+        self.user_id = user_id
+        self.channel_id = channel_id
+        self.token = token
+
+    def equals_key(self, rhs: "UserToken"):
+        return (
+            rhs is not None
+            and self.connection_name == rhs.connection_name
+            and self.user_id == rhs.user_id
+            and self.channel_id == rhs.channel_id
+        )
+
+
+class ExchangeableToken(UserToken):
+    def __init__(
+        self,
+        connection_name: str = None,
+        user_id: str = None,
+        channel_id: str = None,
+        token: str = None,
+        exchangeable_item: str = None,
+    ):
+        super(ExchangeableToken, self).__init__(
+            connection_name=connection_name,
+            user_id=user_id,
+            channel_id=channel_id,
+            token=token,
+        )
+
+        self.exchangeable_item = exchangeable_item
+
+    def equals_key(self, rhs: "ExchangeableToken") -> bool:
+        return (
+            rhs is not None
+            and self.exchangeable_item == rhs.exchangeable_item
+            and super().equals_key(rhs)
+        )
+
+    def to_key(self) -> str:
+        return self.exchangeable_item
+
+
+class TokenMagicCode:
+    def __init__(self, key: UserToken = None, magic_code: str = None):
+        self.key = key
+        self.magic_code = magic_code
+
+
+class TestAdapter(BotAdapter, ExtendedUserTokenProvider):
+    __test__ = False
+    __EXCEPTION_EXPECTED = "ExceptionExpected"
+
+    def __init__(
+        self,
+        logic: Coroutine = None,
+        template_or_conversation: Union[Activity, ConversationReference] = None,
+        send_trace_activities: bool = False,
+    ):
+        """
+        Creates a new TestAdapter instance.
+        :param logic:
+        :param conversation: A reference to the conversation to begin the adapter state with.
+        """
+        super(TestAdapter, self).__init__()
+        self.logic = logic
+        self._next_id: int = 0
+        self._user_tokens: List[UserToken] = []
+        self._magic_codes: List[TokenMagicCode] = []
+        self._conversation_lock = Lock()
+        self.exchangeable_tokens: Dict[str, ExchangeableToken] = {}
+        self.activity_buffer: List[Activity] = []
+        self.updated_activities: List[Activity] = []
+        self.deleted_activities: List[ConversationReference] = []
+        self.send_trace_activities = send_trace_activities
+
+        self.template = (
+            template_or_conversation
+            if isinstance(template_or_conversation, Activity)
+            else Activity(
+                channel_id="test",
+                service_url="https://test.com",
+                from_property=ChannelAccount(id="User1", name="user"),
+                recipient=ChannelAccount(id="bot", name="Bot"),
+                conversation=ConversationAccount(id="Convo1"),
+            )
+        )
+
+        if isinstance(template_or_conversation, ConversationReference):
+            self.template.channel_id = template_or_conversation.channel_id
+
+    async def process_activity(
+        self, activity: Activity, logic: Callable[[TurnContext], Awaitable]
+    ):
+        self._conversation_lock.acquire()
+        try:
+            # ready for next reply
+            if activity.type is None:
+                activity.type = ActivityTypes.message
+
+            activity.channel_id = self.template.channel_id
+            activity.from_property = self.template.from_property
+            activity.recipient = self.template.recipient
+            activity.conversation = self.template.conversation
+            activity.service_url = self.template.service_url
+
+            activity.id = str((self._next_id))
+            self._next_id += 1
+        finally:
+            self._conversation_lock.release()
+
+        activity.timestamp = activity.timestamp or datetime.utcnow()
+        await self.run_pipeline(self.create_turn_context(activity), logic)
+
+    async def send_activities(
+        self, context, activities: List[Activity]
+    ) -> List[ResourceResponse]:
+        """
+        INTERNAL: called by the logic under test to send a set of activities. These will be buffered
+        to the current `TestFlow` instance for comparison against the expected results.
+        :param context:
+        :param activities:
+        :return:
+        """
+
+        def id_mapper(activity):
+            self.activity_buffer.append(activity)
+            self._next_id += 1
+            return ResourceResponse(id=str(self._next_id))
+
+        return [
+            id_mapper(activity)
+            for activity in activities
+            if self.send_trace_activities or activity.type != "trace"
+        ]
+
+    async def delete_activity(self, context, reference: ConversationReference):
+        """
+        INTERNAL: called by the logic under test to delete an existing activity. These are simply
+        pushed onto a [deletedActivities](#deletedactivities) array for inspection after the turn
+        completes.
+        :param reference:
+        :return:
+        """
+        self.deleted_activities.append(reference)
+
+    async def update_activity(self, context, activity: Activity):
+        """
+        INTERNAL: called by the logic under test to replace an existing activity. These are simply
+        pushed onto an [updatedActivities](#updatedactivities) array for inspection after the turn
+        completes.
+        :param activity:
+        :return:
+        """
+        self.updated_activities.append(activity)
+
+    async def continue_conversation(
+        self,
+        reference: ConversationReference,
+        callback: Callable,
+        bot_id: str = None,
+        claims_identity: ClaimsIdentity = None,  # pylint: disable=unused-argument
+        audience: str = None,
+    ):
+        """
+        The `TestAdapter` just calls parent implementation.
+        :param reference:
+        :param callback:
+        :param bot_id:
+        :param claims_identity:
+        :return:
+        """
+        await super().continue_conversation(
+            reference, callback, bot_id, claims_identity, audience
+        )
+
+    async def create_conversation(
+        self, channel_id: str, callback: Callable  # pylint: disable=unused-argument
+    ):
+        self.activity_buffer.clear()
+        update = Activity(
+            type=ActivityTypes.conversation_update,
+            members_added=[],
+            members_removed=[],
+            channel_id=channel_id,
+            conversation=ConversationAccount(id=str(uuid.uuid4())),
+        )
+        context = self.create_turn_context(update)
+        return await callback(context)
+
+    async def receive_activity(self, activity):
+        """
+        INTERNAL: called by a `TestFlow` instance to simulate a user sending a message to the bot.
+        This will cause the adapters middleware pipe to be run and it's logic to be called.
+        :param activity:
+        :return:
+        """
+        if isinstance(activity, str):
+            activity = Activity(type="message", text=activity)
+        # Initialize request.
+        request = copy(self.template)
+
+        for key, value in vars(activity).items():
+            if value is not None and key != "additional_properties":
+                setattr(request, key, value)
+
+        request.type = request.type or ActivityTypes.message
+        if not request.id:
+            self._next_id += 1
+            request.id = str(self._next_id)
+
+        # Create context object and run middleware.
+        context = self.create_turn_context(request)
+        return await self.run_pipeline(context, self.logic)
+
+    def get_next_activity(self) -> Activity:
+        if len(self.activity_buffer) > 0:
+            return self.activity_buffer.pop(0)
+        return None
+
+    async def send(self, user_says) -> object:
+        """
+        Sends something to the bot. This returns a new `TestFlow` instance which can be used to add
+        additional steps for inspecting the bots reply and then sending additional activities.
+        :param user_says:
+        :return: A new instance of the TestFlow object
+        """
+        return TestFlow(await self.receive_activity(user_says), self)
+
+    async def test(
+        self, user_says, expected, description=None, timeout=None
+    ) -> "TestFlow":
+        """
+        Send something to the bot and expects the bot to return with a given reply. This is simply a
+        wrapper around calls to `send()` and `assertReply()`. This is such a common pattern that a
+        helper is provided.
+        :param user_says:
+        :param expected:
+        :param description:
+        :param timeout:
+        :return:
+        """
+        test_flow = await self.send(user_says)
+        test_flow = await test_flow.assert_reply(expected, description, timeout)
+        return test_flow
+
+    async def tests(self, *args):
+        """
+        Support multiple test cases without having to manually call `test()` repeatedly. This is a
+        convenience layer around the `test()`. Valid args are either lists or tuples of parameters
+        :param args:
+        :return:
+        """
+        for arg in args:
+            description = None
+            timeout = None
+            if len(arg) >= 3:
+                description = arg[2]
+                if len(arg) == 4:
+                    timeout = arg[3]
+            await self.test(arg[0], arg[1], description, timeout)
+
+    @staticmethod
+    def create_conversation_reference(
+        name: str, user: str = "User1", bot: str = "Bot"
+    ) -> ConversationReference:
+        return ConversationReference(
+            channel_id="test",
+            service_url="https://test.com",
+            conversation=ConversationAccount(
+                is_group=False, conversation_type=name, id=name,
+            ),
+            user=ChannelAccount(id=user.lower(), name=user.lower(),),
+            bot=ChannelAccount(id=bot.lower(), name=bot.lower(),),
+        )
+
+    def add_user_token(
+        self,
+        connection_name: str,
+        channel_id: str,
+        user_id: str,
+        token: str,
+        magic_code: str = None,
+    ):
+        key = UserToken()
+        key.channel_id = channel_id
+        key.connection_name = connection_name
+        key.user_id = user_id
+        key.token = token
+
+        if not magic_code:
+            self._user_tokens.append(key)
+        else:
+            code = TokenMagicCode()
+            code.key = key
+            code.magic_code = magic_code
+            self._magic_codes.append(code)
+
+    async def get_user_token(
+        self,
+        context: TurnContext,
+        connection_name: str,
+        magic_code: str = None,
+        oauth_app_credentials: AppCredentials = None,  # pylint: disable=unused-argument
+    ) -> TokenResponse:
+        key = UserToken()
+        key.channel_id = context.activity.channel_id
+        key.connection_name = connection_name
+        key.user_id = context.activity.from_property.id
+
+        if magic_code:
+            magic_code_record = list(
+                filter(lambda x: key.equals_key(x.key), self._magic_codes)
+            )
+            if magic_code_record and magic_code_record[0].magic_code == magic_code:
+                # Move the token to long term dictionary.
+                self.add_user_token(
+                    connection_name,
+                    key.channel_id,
+                    key.user_id,
+                    magic_code_record[0].key.token,
+                )
+
+                # Remove from the magic code list.
+                idx = self._magic_codes.index(magic_code_record[0])
+                self._magic_codes = [self._magic_codes.pop(idx)]
+
+        match = [token for token in self._user_tokens if key.equals_key(token)]
+
+        if match:
+            return TokenResponse(
+                connection_name=match[0].connection_name,
+                token=match[0].token,
+                expiration=None,
+            )
+        # Not found.
+        return None
+
+    async def sign_out_user(
+        self,
+        context: TurnContext,
+        connection_name: str = None,
+        user_id: str = None,
+        oauth_app_credentials: AppCredentials = None,  # pylint: disable=unused-argument
+    ):
+        channel_id = context.activity.channel_id
+        user_id = context.activity.from_property.id
+
+        new_records = []
+        for token in self._user_tokens:
+            if (
+                token.channel_id != channel_id
+                or token.user_id != user_id
+                or (connection_name and connection_name != token.connection_name)
+            ):
+                new_records.append(token)
+        self._user_tokens = new_records
+
+    async def get_oauth_sign_in_link(
+        self,
+        context: TurnContext,
+        connection_name: str,
+        final_redirect: str = None,  # pylint: disable=unused-argument
+        oauth_app_credentials: AppCredentials = None,  # pylint: disable=unused-argument
+    ) -> str:
+        return (
+            f"https://fake.com/oauthsignin"
+            f"/{connection_name}/{context.activity.channel_id}/{context.activity.from_property.id}"
+        )
+
+    async def get_token_status(
+        self,
+        context: TurnContext,
+        connection_name: str = None,
+        user_id: str = None,
+        include_filter: str = None,
+        oauth_app_credentials: AppCredentials = None,
+    ) -> Dict[str, TokenResponse]:
+        return None
+
+    async def get_aad_tokens(
+        self,
+        context: TurnContext,
+        connection_name: str,
+        resource_urls: List[str],
+        user_id: str = None,  # pylint: disable=unused-argument
+        oauth_app_credentials: AppCredentials = None,  # pylint: disable=unused-argument
+    ) -> Dict[str, TokenResponse]:
+        return None
+
+    def add_exchangeable_token(
+        self,
+        connection_name: str,
+        channel_id: str,
+        user_id: str,
+        exchangeable_item: str,
+        token: str,
+    ):
+        key = ExchangeableToken(
+            connection_name=connection_name,
+            channel_id=channel_id,
+            user_id=user_id,
+            exchangeable_item=exchangeable_item,
+            token=token,
+        )
+        self.exchangeable_tokens[key.to_key()] = key
+
+    def throw_on_exchange_request(
+        self,
+        connection_name: str,
+        channel_id: str,
+        user_id: str,
+        exchangeable_item: str,
+    ):
+        key = ExchangeableToken(
+            connection_name=connection_name,
+            channel_id=channel_id,
+            user_id=user_id,
+            exchangeable_item=exchangeable_item,
+            token=TestAdapter.__EXCEPTION_EXPECTED,
+        )
+
+        self.exchangeable_tokens[key.to_key()] = key
+
+    async def get_sign_in_resource_from_user(
+        self,
+        turn_context: TurnContext,
+        connection_name: str,
+        user_id: str,
+        final_redirect: str = None,
+    ) -> SignInUrlResponse:
+        return await self.get_sign_in_resource_from_user_and_credentials(
+            turn_context, None, connection_name, user_id, final_redirect
+        )
+
+    async def get_sign_in_resource_from_user_and_credentials(
+        self,
+        turn_context: TurnContext,
+        oauth_app_credentials: AppCredentials,
+        connection_name: str,
+        user_id: str,
+        final_redirect: str = None,
+    ) -> SignInUrlResponse:
+        return SignInUrlResponse(
+            sign_in_link=f"https://fake.com/oauthsignin/{connection_name}/{turn_context.activity.channel_id}/{user_id}",
+            token_exchange_resource=TokenExchangeResource(
+                id=str(uuid4()),
+                provider_id=None,
+                uri=f"api://{connection_name}/resource",
+            ),
+        )
+
+    async def exchange_token(
+        self,
+        turn_context: TurnContext,
+        connection_name: str,
+        user_id: str,
+        exchange_request: TokenExchangeRequest,
+    ) -> TokenResponse:
+        return await self.exchange_token_from_credentials(
+            turn_context, None, connection_name, user_id, exchange_request
+        )
+
+    async def exchange_token_from_credentials(
+        self,
+        turn_context: TurnContext,
+        oauth_app_credentials: AppCredentials,
+        connection_name: str,
+        user_id: str,
+        exchange_request: TokenExchangeRequest,
+    ) -> TokenResponse:
+        exchangeable_value = exchange_request.token or exchange_request.uri
+
+        key = ExchangeableToken(
+            channel_id=turn_context.activity.channel_id,
+            connection_name=connection_name,
+            exchangeable_item=exchangeable_value,
+            user_id=user_id,
+        )
+
+        token_exchange_response = self.exchangeable_tokens.get(key.to_key())
+        if token_exchange_response:
+            if token_exchange_response.token == TestAdapter.__EXCEPTION_EXPECTED:
+                raise Exception("Exception occurred during exchanging tokens")
+
+            return TokenResponse(
+                channel_id=key.channel_id,
+                connection_name=key.connection_name,
+                token=token_exchange_response.token,
+                expiration=None,
+            )
+
+        return None
+
+    def create_turn_context(self, activity: Activity) -> TurnContext:
+        return TurnContext(self, activity)
+
+
+class TestFlow:
+    __test__ = False
+
+    def __init__(self, previous: Callable, adapter: TestAdapter):
+        """
+        INTERNAL: creates a TestFlow instance.
+        :param previous:
+        :param adapter:
+        """
+        self.previous = previous
+        self.adapter = adapter
+
+    async def test(
+        self, user_says, expected, description=None, timeout=None
+    ) -> "TestFlow":
+        """
+        Send something to the bot and expects the bot to return with a given reply. This is simply a
+        wrapper around calls to `send()` and `assertReply()`. This is such a common pattern that a
+        helper is provided.
+        :param user_says:
+        :param expected:
+        :param description:
+        :param timeout:
+        :return:
+        """
+        test_flow = await self.send(user_says)
+        return await test_flow.assert_reply(
+            expected, description or f'test("{user_says}", "{expected}")', timeout
+        )
+
+    async def send(self, user_says) -> "TestFlow":
+        """
+        Sends something to the bot.
+        :param user_says:
+        :return:
+        """
+
+        async def new_previous():
+            nonlocal self, user_says
+            if callable(self.previous):
+                await self.previous()
+            await self.adapter.receive_activity(user_says)
+
+        return TestFlow(await new_previous(), self.adapter)
+
+    async def assert_reply(
+        self,
+        expected: Union[str, Activity, Callable[[Activity, str], None]],
+        description=None,
+        timeout=None,  # pylint: disable=unused-argument
+        is_substring=False,
+    ) -> "TestFlow":
+        """
+        Generates an assertion if the bots response doesn't match the expected text/activity.
+        :param expected:
+        :param description:
+        :param timeout:
+        :param is_substring:
+        :return:
+        """
+
+        # TODO: refactor method so expected can take a Callable[[Activity], None]
+        def default_inspector(reply, description=None):
+            if isinstance(expected, Activity):
+                validate_activity(reply, expected)
+            else:
+                assert reply.type == "message", description + f" type == {reply.type}"
+                if is_substring:
+                    assert expected in reply.text.strip(), (
+                        description + f" text == {reply.text}"
+                    )
+                else:
+                    assert reply.text.strip() == expected.strip(), (
+                        description + f" text == {reply.text}"
+                    )
+
+        if description is None:
+            description = ""
+
+        inspector = expected if callable(expected) else default_inspector
+
+        async def test_flow_previous():
+            nonlocal timeout
+            if not timeout:
+                timeout = 3000
+            start = datetime.now()
+            adapter = self.adapter
+
+            async def wait_for_activity():
+                nonlocal expected, timeout
+                current = datetime.now()
+                if (current - start).total_seconds() * 1000 > timeout:
+                    if isinstance(expected, Activity):
+                        expecting = expected.text
+                    elif callable(expected):
+                        expecting = inspect.getsourcefile(expected)
+                    else:
+                        expecting = str(expected)
+                    raise RuntimeError(
+                        f"TestAdapter.assert_reply({expecting}): {description} Timed out after "
+                        f"{current - start}ms."
+                    )
+                if adapter.activity_buffer:
+                    reply = adapter.activity_buffer.pop(0)
+                    try:
+                        await inspector(reply, description)
+                    except Exception:
+                        inspector(reply, description)
+
+                else:
+                    await asyncio.sleep(0.05)
+                    await wait_for_activity()
+
+            await wait_for_activity()
+
+        return TestFlow(await test_flow_previous(), self.adapter)
+
+    async def assert_no_reply(
+        self, description=None, timeout=None,  # pylint: disable=unused-argument
+    ) -> "TestFlow":
+        """
+        Generates an assertion if the bot responds when no response is expected.
+        :param description:
+        :param timeout:
+        """
+        if description is None:
+            description = ""
+
+        async def test_flow_previous():
+            nonlocal timeout
+            if not timeout:
+                timeout = 3000
+            start = datetime.now()
+            adapter = self.adapter
+
+            async def wait_for_activity():
+                nonlocal timeout
+                current = datetime.now()
+
+                if (current - start).total_seconds() * 1000 > timeout:
+                    # operation timed out and recieved no reply
+                    return
+
+                if adapter.activity_buffer:
+                    reply = adapter.activity_buffer.pop(0)
+                    raise RuntimeError(
+                        f"TestAdapter.assert_no_reply(): '{reply.text}' is responded when waiting for no reply."
+                    )
+
+                await asyncio.sleep(0.05)
+                await wait_for_activity()
+
+            await wait_for_activity()
+
+        return TestFlow(await test_flow_previous(), self.adapter)
+
+
+def validate_activity(activity, expected) -> None:
+    """
+    Helper method that compares activities
+    :param activity:
+    :param expected:
+    :return:
+    """
+    iterable_expected = vars(expected).items()
+
+    for attr, value in iterable_expected:
+        if value is not None and attr != "additional_properties":
+            assert value == getattr(activity, attr)
diff --git a/libraries/botbuilder-core/botbuilder/core/auto_save_state_middleware.py b/libraries/botbuilder-core/botbuilder/core/auto_save_state_middleware.py
index 561adab27..c137d59b9 100644
--- a/libraries/botbuilder-core/botbuilder/core/auto_save_state_middleware.py
+++ b/libraries/botbuilder-core/botbuilder/core/auto_save_state_middleware.py
@@ -1,3 +1,6 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
 from typing import Awaitable, Callable, List, Union
 
 from .bot_state import BotState
diff --git a/libraries/botbuilder-core/botbuilder/core/bot.py b/libraries/botbuilder-core/botbuilder/core/bot.py
new file mode 100644
index 000000000..afbaa3293
--- /dev/null
+++ b/libraries/botbuilder-core/botbuilder/core/bot.py
@@ -0,0 +1,21 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from abc import ABC, abstractmethod
+
+from .turn_context import TurnContext
+
+
+class Bot(ABC):
+    """
+    Represents a bot that can operate on incoming activities.
+    """
+
+    @abstractmethod
+    async def on_turn(self, context: TurnContext):
+        """
+        When implemented in a bot, handles an incoming activity.
+        :param context: The context object for this turn.
+        :return:
+        """
+        raise NotImplementedError()
diff --git a/libraries/botbuilder-core/botbuilder/core/bot_adapter.py b/libraries/botbuilder-core/botbuilder/core/bot_adapter.py
index 7452625c4..5c7d26396 100644
--- a/libraries/botbuilder-core/botbuilder/core/bot_adapter.py
+++ b/libraries/botbuilder-core/botbuilder/core/bot_adapter.py
@@ -1,105 +1,138 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from abc import ABC, abstractmethod
-from typing import List, Callable, Awaitable
-from botbuilder.schema import Activity, ConversationReference
-
-from . import conversation_reference_extension
-from .bot_assert import BotAssert
-from .turn_context import TurnContext
-from .middleware_set import MiddlewareSet
-
-
-class BotAdapter(ABC):
-    def __init__(
-        self, on_turn_error: Callable[[TurnContext, Exception], Awaitable] = None
-    ):
-        self._middleware = MiddlewareSet()
-        self.on_turn_error = on_turn_error
-
-    @abstractmethod
-    async def send_activities(self, context: TurnContext, activities: List[Activity]):
-        """
-        Sends a set of activities to the user. An array of responses from the server will be returned.
-        :param context:
-        :param activities:
-        :return:
-        """
-        raise NotImplementedError()
-
-    @abstractmethod
-    async def update_activity(self, context: TurnContext, activity: Activity):
-        """
-        Replaces an existing activity.
-        :param context:
-        :param activity:
-        :return:
-        """
-        raise NotImplementedError()
-
-    @abstractmethod
-    async def delete_activity(
-        self, context: TurnContext, reference: ConversationReference
-    ):
-        """
-        Deletes an existing activity.
-        :param context:
-        :param reference:
-        :return:
-        """
-        raise NotImplementedError()
-
-    def use(self, middleware):
-        """
-        Registers a middleware handler with the adapter.
-        :param middleware:
-        :return:
-        """
-        self._middleware.use(middleware)
-        return self
-
-    async def continue_conversation(
-        self, bot_id: str, reference: ConversationReference, callback: Callable
-    ):  # pylint: disable=unused-argument
-        """
-        Sends a proactive message to a conversation. Call this method to proactively send a message to a conversation.
-        Most _channels require a user to initiate a conversation with a bot before the bot can send activities
-        to the user.
-        :param bot_id: The application ID of the bot. This parameter is ignored in
-        single tenant the Adpters (Console, Test, etc) but is critical to the BotFrameworkAdapter
-        which is multi-tenant aware. 
-        :param reference: A reference to the conversation to continue.
-        :param callback: The method to call for the resulting bot turn.
-        """
-        context = TurnContext(
-            self, conversation_reference_extension.get_continuation_activity(reference)
-        )
-        return await self.run_pipeline(context, callback)
-
-    async def run_pipeline(
-        self, context: TurnContext, callback: Callable[[TurnContext], Awaitable] = None
-    ):
-        """
-        Called by the parent class to run the adapters middleware set and calls the passed in `callback()` handler at
-        the end of the chain.
-        :param context:
-        :param callback:
-        :return:
-        """
-        BotAssert.context_not_none(context)
-
-        if context.activity is not None:
-            try:
-                return await self._middleware.receive_activity_with_status(
-                    context, callback
-                )
-            except Exception as error:
-                if self.on_turn_error is not None:
-                    await self.on_turn_error(context, error)
-                else:
-                    raise error
-        else:
-            # callback to caller on proactive case
-            if callback is not None:
-                await callback(context)
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from abc import ABC, abstractmethod
+from typing import List, Callable, Awaitable
+from botbuilder.schema import Activity, ConversationReference, ResourceResponse
+from botframework.connector.auth import ClaimsIdentity
+
+from . import conversation_reference_extension
+from .bot_assert import BotAssert
+from .turn_context import TurnContext
+from .middleware_set import MiddlewareSet
+
+
+class BotAdapter(ABC):
+    BOT_IDENTITY_KEY = "BotIdentity"
+    BOT_OAUTH_SCOPE_KEY = "botbuilder.core.BotAdapter.OAuthScope"
+    BOT_CONNECTOR_CLIENT_KEY = "ConnectorClient"
+    BOT_CALLBACK_HANDLER_KEY = "BotCallbackHandler"
+
+    def __init__(
+        self, on_turn_error: Callable[[TurnContext, Exception], Awaitable] = None
+    ):
+        self._middleware = MiddlewareSet()
+        self.on_turn_error = on_turn_error
+
+    @abstractmethod
+    async def send_activities(
+        self, context: TurnContext, activities: List[Activity]
+    ) -> List[ResourceResponse]:
+        """
+        Sends a set of activities to the user. An array of responses from the server will be returned.
+
+        :param context: The context object for the turn.
+        :type context: :class:`TurnContext`
+        :param activities: The activities to send.
+        :type activities: :class:`typing.List[Activity]`
+        :return:
+        """
+        raise NotImplementedError()
+
+    @abstractmethod
+    async def update_activity(self, context: TurnContext, activity: Activity):
+        """
+        Replaces an existing activity.
+
+        :param context: The context object for the turn.
+        :type context: :class:`TurnContext`
+        :param activity: New replacement activity.
+        :type activity: :class:`botbuilder.schema.Activity`
+        :return:
+        """
+        raise NotImplementedError()
+
+    @abstractmethod
+    async def delete_activity(
+        self, context: TurnContext, reference: ConversationReference
+    ):
+        """
+        Deletes an existing activity.
+
+        :param context: The context object for the turn.
+        :type context: :class:`TurnContext`
+        :param reference: Conversation reference for the activity to delete.
+        :type reference: :class:`botbuilder.schema.ConversationReference`
+        :return:
+        """
+        raise NotImplementedError()
+
+    def use(self, middleware):
+        """
+        Registers a middleware handler with the adapter.
+
+        :param middleware: The middleware to register.
+        :return:
+        """
+        self._middleware.use(middleware)
+        return self
+
+    async def continue_conversation(
+        self,
+        reference: ConversationReference,
+        callback: Callable,
+        bot_id: str = None,  # pylint: disable=unused-argument
+        claims_identity: ClaimsIdentity = None,  # pylint: disable=unused-argument
+        audience: str = None,  # pylint: disable=unused-argument
+    ):
+        """
+        Sends a proactive message to a conversation. Call this method to proactively send a message to a conversation.
+        Most channels require a user to initiate a conversation with a bot before the bot can send activities
+        to the user.
+
+        :param bot_id: The application ID of the bot. This parameter is ignored in
+        single tenant the Adapters (Console, Test, etc) but is critical to the BotFrameworkAdapter
+        which is multi-tenant aware.
+        :param reference: A reference to the conversation to continue.
+        :type reference: :class:`botbuilder.schema.ConversationReference`
+        :param callback: The method to call for the resulting bot turn.
+        :type callback: :class:`typing.Callable`
+        :param claims_identity: A :class:`botframework.connector.auth.ClaimsIdentity` for the conversation.
+        :type claims_identity: :class:`botframework.connector.auth.ClaimsIdentity`
+        :param audience:A value signifying the recipient of the proactive message.
+        :type audience: str
+        """
+        context = TurnContext(
+            self, conversation_reference_extension.get_continuation_activity(reference)
+        )
+        return await self.run_pipeline(context, callback)
+
+    async def run_pipeline(
+        self, context: TurnContext, callback: Callable[[TurnContext], Awaitable] = None
+    ):
+        """
+        Called by the parent class to run the adapters middleware set and calls the passed in `callback()` handler at
+        the end of the chain.
+
+        :param context: The context object for the turn.
+        :type context: :class:`TurnContext`
+        :param callback: A callback method to run at the end of the pipeline.
+        :type callback: :class:`typing.Callable[[TurnContext], Awaitable]`
+        :return:
+        """
+        BotAssert.context_not_none(context)
+
+        if context.activity is not None:
+            try:
+                return await self._middleware.receive_activity_with_status(
+                    context, callback
+                )
+            except Exception as error:
+                if self.on_turn_error is not None:
+                    await self.on_turn_error(context, error)
+                else:
+                    raise error
+        else:
+            # callback to caller on proactive case
+            if callback is not None:
+                await callback(context)
diff --git a/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py b/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py
index aaac1119b..930f29d44 100644
--- a/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py
+++ b/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py
@@ -1,37 +1,66 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License.
 
+# pylint: disable=too-many-lines
+
 import asyncio
 import base64
 import json
 import os
+import uuid
+from http import HTTPStatus
 from typing import List, Callable, Awaitable, Union, Dict
 from msrest.serialization import Model
-from botbuilder.schema import (
-    Activity,
-    ConversationAccount,
-    ConversationParameters,
-    ConversationReference,
-    TokenResponse,
-)
+
 from botframework.connector import Channels, EmulatorApiClient
 from botframework.connector.aio import ConnectorClient
 from botframework.connector.auth import (
+    AuthenticationConfiguration,
     AuthenticationConstants,
     ChannelValidation,
+    ChannelProvider,
+    ClaimsIdentity,
     GovernmentChannelValidation,
     GovernmentConstants,
     MicrosoftAppCredentials,
     JwtTokenValidation,
+    CredentialProvider,
     SimpleCredentialProvider,
+    SkillValidation,
+    AppCredentials,
+    SimpleChannelProvider,
+    MicrosoftGovernmentAppCredentials,
 )
 from botframework.connector.token_api import TokenApiClient
-from botframework.connector.token_api.models import TokenStatus
+from botframework.connector.token_api.models import (
+    TokenStatus,
+    TokenExchangeRequest,
+    SignInUrlResponse,
+)
+from botbuilder.schema import (
+    Activity,
+    ActivityEventNames,
+    ActivityTypes,
+    ChannelAccount,
+    ConversationAccount,
+    ConversationParameters,
+    ConversationReference,
+    ExpectedReplies,
+    TokenResponse,
+    ResourceResponse,
+    DeliveryModes,
+    CallerIdConstants,
+)
 
 from . import __version__
 from .bot_adapter import BotAdapter
+from .oauth import (
+    ConnectorClientBuilder,
+    ExtendedUserTokenProvider,
+)
 from .turn_context import TurnContext
-from .user_token_provider import UserTokenProvider
+from .invoke_response import InvokeResponse
+from .conversation_reference_extension import get_continuation_activity
 
 USER_AGENT = f"Microsoft-BotFramework/3.1 (BotBuilder Python/{__version__})"
 OAUTH_ENDPOINT = "https://api.botframework.com"
@@ -39,10 +68,25 @@
 
 
 class TokenExchangeState(Model):
+    """TokenExchangeState
+
+    :param connection_name: The connection name that was used.
+    :type connection_name: str
+    :param conversation: Gets or sets a reference to the conversation.
+    :type conversation: ~botframework.connector.models.ConversationReference
+    :param relates_to: Gets or sets a reference to a related parent conversation for this token exchange.
+    :type relates_to: ~botframework.connector.models.ConversationReference
+    :param bot_ur: The URL of the bot messaging endpoint.
+    :type bot_ur: str
+    :param ms_app_id: The bot's registered application ID.
+    :type ms_app_id: str
+    """
+
     _attribute_map = {
         "connection_name": {"key": "connectionName", "type": "str"},
         "conversation": {"key": "conversation", "type": "ConversationReference"},
-        "bot_url": {"key": "botUrl", "type": "str"},
+        "relates_to": {"key": "relatesTo", "type": "ConversationReference"},
+        "bot_url": {"key": "connectionName", "type": "str"},
         "ms_app_id": {"key": "msAppId", "type": "str"},
     }
 
@@ -50,7 +94,8 @@ def __init__(
         self,
         *,
         connection_name: str = None,
-        conversation: ConversationReference = None,
+        conversation=None,
+        relates_to=None,
         bot_url: str = None,
         ms_app_id: str = None,
         **kwargs,
@@ -58,6 +103,7 @@ def __init__(
         super(TokenExchangeState, self).__init__(**kwargs)
         self.connection_name = connection_name
         self.conversation = conversation
+        self.relates_to = relates_to
         self.bot_url = bot_url
         self.ms_app_id = ms_app_id
 
@@ -66,41 +112,98 @@ class BotFrameworkAdapterSettings:
     def __init__(
         self,
         app_id: str,
-        app_password: str,
+        app_password: str = None,
         channel_auth_tenant: str = None,
         oauth_endpoint: str = None,
         open_id_metadata: str = None,
-        channel_service: str = None,
+        channel_provider: ChannelProvider = None,
+        auth_configuration: AuthenticationConfiguration = None,
+        app_credentials: AppCredentials = None,
+        credential_provider: CredentialProvider = None,
     ):
+        """
+        Contains the settings used to initialize a :class:`BotFrameworkAdapter` instance.
+
+        :param app_id: The bot application ID.
+        :type app_id: str
+        :param app_password: The bot application password.
+        the value os the `MicrosoftAppPassword` parameter in the `config.py` file.
+        :type app_password: str
+        :param channel_auth_tenant: The channel tenant to use in conversation
+        :type channel_auth_tenant: str
+        :param oauth_endpoint:
+        :type oauth_endpoint: str
+        :param open_id_metadata:
+        :type open_id_metadata: str
+        :param channel_provider: The channel provider
+        :type channel_provider: :class:`botframework.connector.auth.ChannelProvider`.  Defaults to SimpleChannelProvider
+        if one isn't specified.
+        :param auth_configuration:
+        :type auth_configuration: :class:`botframework.connector.auth.AuthenticationConfiguration`
+        :param credential_provider: Defaults to SimpleCredentialProvider if one isn't specified.
+        :param app_credentials: Allows for a custom AppCredentials.  Used, for example, for CertificateAppCredentials.
+        """
+
         self.app_id = app_id
         self.app_password = app_password
+        self.app_credentials = app_credentials
         self.channel_auth_tenant = channel_auth_tenant
         self.oauth_endpoint = oauth_endpoint
-        self.open_id_metadata = open_id_metadata
-        self.channel_service = channel_service
+        self.channel_provider = (
+            channel_provider if channel_provider else SimpleChannelProvider()
+        )
+        self.credential_provider = (
+            credential_provider
+            if credential_provider
+            else SimpleCredentialProvider(self.app_id, self.app_password)
+        )
+        self.auth_configuration = auth_configuration or AuthenticationConfiguration()
+
+        # If no open_id_metadata values were passed in the settings, check the
+        # process' Environment Variable.
+        self.open_id_metadata = (
+            open_id_metadata
+            if open_id_metadata
+            else os.environ.get(AuthenticationConstants.BOT_OPEN_ID_METADATA_KEY)
+        )
+
+
+class BotFrameworkAdapter(
+    BotAdapter, ExtendedUserTokenProvider, ConnectorClientBuilder
+):
+    """
+    Defines an adapter to connect a bot to a service endpoint.
 
+    .. remarks::
+        The bot adapter encapsulates authentication processes and sends activities to and
+        receives activities from the Bot Connector Service. When your bot receives an activity,
+        the adapter creates a context object, passes it to your bot's application logic, and
+        sends responses back to the user's channel.
+        The adapter processes and directs incoming activities in through the bot middleware
+        pipeline to your bot’s logic and then back out again.
+        As each activity flows in and out of the bot, each piece of middleware can inspect or act
+        upon the activity, both before and after the bot logic runs.
+    """
 
-class BotFrameworkAdapter(BotAdapter, UserTokenProvider):
     _INVOKE_RESPONSE_KEY = "BotFrameworkAdapter.InvokeResponse"
 
     def __init__(self, settings: BotFrameworkAdapterSettings):
+        """
+        Initializes a new instance of the :class:`BotFrameworkAdapter` class.
+
+        :param settings: The settings to initialize the adapter
+        :type settings: :class:`BotFrameworkAdapterSettings`
+        """
         super(BotFrameworkAdapter, self).__init__()
         self.settings = settings or BotFrameworkAdapterSettings("", "")
-        self.settings.channel_service = self.settings.channel_service or os.environ.get(
-            AuthenticationConstants.CHANNEL_SERVICE
-        )
-        self.settings.open_id_metadata = (
-            self.settings.open_id_metadata
-            or os.environ.get(AuthenticationConstants.BOT_OPEN_ID_METADATA_KEY)
-        )
-        self._credentials = MicrosoftAppCredentials(
-            self.settings.app_id,
-            self.settings.app_password,
-            self.settings.channel_auth_tenant,
-        )
+
+        self._credentials = self.settings.app_credentials
         self._credential_provider = SimpleCredentialProvider(
             self.settings.app_id, self.settings.app_password
         )
+
+        self._channel_provider = self.settings.channel_provider
+
         self._is_emulating_oauth_cards = False
 
         if self.settings.open_id_metadata:
@@ -109,85 +212,211 @@ def __init__(self, settings: BotFrameworkAdapterSettings):
                 self.settings.open_id_metadata
             )
 
-        if JwtTokenValidation.is_government(self.settings.channel_service):
-            self._credentials.oauth_endpoint = (
-                GovernmentConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL
-            )
-            self._credentials.oauth_scope = (
-                GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
-            )
+        # There is a significant boost in throughput if we reuse a ConnectorClient
+        self._connector_client_cache: Dict[str, ConnectorClient] = {}
+
+        # Cache for appCredentials to speed up token acquisition (a token is not requested unless is expired)
+        self._app_credential_map: Dict[str, AppCredentials] = {}
 
     async def continue_conversation(
-        self, bot_id: str, reference: ConversationReference, callback: Callable
+        self,
+        reference: ConversationReference,
+        callback: Callable,
+        bot_id: str = None,
+        claims_identity: ClaimsIdentity = None,
+        audience: str = None,
     ):
         """
-        Continues a conversation with a user. This is often referred to as the bots "Proactive Messaging"
-        flow as its lets the bot proactively send messages to a conversation or user that its already
-        communicated with. Scenarios like sending notifications or coupons to a user are enabled by this
-        method.
-        :param bot_id:
-        :param reference:
-        :param callback:
-        :return:
+        Continues a conversation with a user.
+
+        :param reference: A reference to the conversation to continue
+        :type reference: :class:`botbuilder.schema.ConversationReference
+        :param callback: The method to call for the resulting bot turn
+        :type callback: :class:`typing.Callable`
+        :param bot_id: The application Id of the bot. This is the appId returned by the Azure portal registration,
+        and is generally found in the `MicrosoftAppId` parameter in `config.py`.
+        :type bot_id: :class:`typing.str`
+        :param claims_identity: The bot claims identity
+        :type claims_identity: :class:`botframework.connector.auth.ClaimsIdentity`
+        :param audience:
+        :type audience: :class:`typing.str`
+
+        :raises: It raises an argument null exception.
+
+        :return: A task that represents the work queued to execute.
+
+        .. remarks::
+            This is often referred to as the bots *proactive messaging* flow as it lets the bot proactively
+            send messages to a conversation or user that are already in a communication.
+            Scenarios such as sending notifications or coupons to a user are enabled by this function.
         """
 
-        # TODO: proactive messages
+        if not reference:
+            raise TypeError(
+                "Expected reference: ConversationReference but got None instead"
+            )
+        if not callback:
+            raise TypeError("Expected callback: Callable but got None instead")
+
+        # This has to have either a bot_id, in which case a ClaimsIdentity will be created, or
+        # a ClaimsIdentity.  In either case, if an audience isn't supplied one will be created.
+        if not (bot_id or claims_identity):
+            raise TypeError("Expected bot_id or claims_identity")
+
+        if bot_id and not claims_identity:
+            claims_identity = ClaimsIdentity(
+                claims={
+                    AuthenticationConstants.AUDIENCE_CLAIM: bot_id,
+                    AuthenticationConstants.APP_ID_CLAIM: bot_id,
+                },
+                is_authenticated=True,
+            )
+
+        if not audience:
+            audience = self.__get_botframework_oauth_scope()
 
-        if not bot_id:
-            raise TypeError("Expected bot_id: str but got None instead")
+        context = TurnContext(self, get_continuation_activity(reference))
+        context.turn_state[BotAdapter.BOT_IDENTITY_KEY] = claims_identity
+        context.turn_state[BotAdapter.BOT_CALLBACK_HANDLER_KEY] = callback
+        context.turn_state[BotAdapter.BOT_OAUTH_SCOPE_KEY] = audience
 
-        request = TurnContext.apply_conversation_reference(
-            Activity(), reference, is_incoming=True
+        # If we receive a valid app id in the incoming token claims, add the channel service URL to the
+        # trusted services list so we can send messages back.
+        # The service URL for skills is trusted because it is applied by the SkillHandler based on the original
+        # request received by the root bot
+        app_id_from_claims = JwtTokenValidation.get_app_id_from_claims(
+            claims_identity.claims
+        )
+        if app_id_from_claims:
+            if SkillValidation.is_skill_claim(
+                claims_identity.claims
+            ) or await self._credential_provider.is_valid_appid(app_id_from_claims):
+                AppCredentials.trust_service_url(reference.service_url)
+
+        client = await self.create_connector_client(
+            reference.service_url, claims_identity, audience
         )
-        context = self.create_context(request)
+        context.turn_state[BotAdapter.BOT_CONNECTOR_CLIENT_KEY] = client
+
         return await self.run_pipeline(context, callback)
 
     async def create_conversation(
         self,
         reference: ConversationReference,
         logic: Callable[[TurnContext], Awaitable] = None,
+        conversation_parameters: ConversationParameters = None,
+        channel_id: str = None,
+        service_url: str = None,
+        credentials: AppCredentials = None,
     ):
         """
-        Starts a new conversation with a user. This is typically used to Direct Message (DM) a member
-        of a group.
-        :param reference:
-        :param logic:
-        :return:
+        Starts a new conversation with a user. Used to direct message to a member of a group.
+
+        :param reference: The conversation reference that contains the tenant
+        :type reference: :class:`botbuilder.schema.ConversationReference`
+        :param logic: The logic to use for the creation of the conversation
+        :type logic: :class:`typing.Callable`
+        :param conversation_parameters: The information to use to create the conversation
+        :type conversation_parameters:
+        :param channel_id: The ID for the channel.
+        :type channel_id: :class:`typing.str`
+        :param service_url: The channel's service URL endpoint.
+        :type service_url: :class:`typing.str`
+        :param credentials: The application credentials for the bot.
+        :type credentials: :class:`botframework.connector.auth.AppCredentials`
+
+        :raises: It raises a generic exception error.
+
+        :return: A task representing the work queued to execute.
+
+        .. remarks::
+            To start a conversation, your bot must know its account information and the user's
+            account information on that channel.
+            Most channels only support initiating a direct message (non-group) conversation.
+            The adapter attempts to create a new conversation on the channel, and
+            then sends a conversation update activity through its middleware pipeline
+            to the the callback method.
+            If the conversation is established with the specified users, the ID of the activity
+            will contain the ID of the new conversation.
         """
         try:
-            if reference.service_url is None:
-                raise TypeError(
-                    "BotFrameworkAdapter.create_conversation(): reference.service_url cannot be None."
-                )
+            if not service_url:
+                service_url = reference.service_url
+                if not service_url:
+                    raise TypeError(
+                        "BotFrameworkAdapter.create_conversation(): service_url or reference.service_url is required."
+                    )
 
-            # Create conversation
-            parameters = ConversationParameters(bot=reference.bot)
-            client = self.create_connector_client(reference.service_url)
+            if not channel_id:
+                channel_id = reference.channel_id
+                if not channel_id:
+                    raise TypeError(
+                        "BotFrameworkAdapter.create_conversation(): channel_id or reference.channel_id is required."
+                    )
+
+            parameters = (
+                conversation_parameters
+                if conversation_parameters
+                else ConversationParameters(
+                    bot=reference.bot, members=[reference.user], is_group=False
+                )
+            )
 
             # Mix in the tenant ID if specified. This is required for MS Teams.
-            if reference.conversation is not None and reference.conversation.tenant_id:
+            if reference.conversation and reference.conversation.tenant_id:
                 # Putting tenant_id in channel_data is a temporary while we wait for the Teams API to be updated
                 parameters.channel_data = {
-                    "tenant": {"id": reference.conversation.tenant_id}
+                    "tenant": {"tenantId": reference.conversation.tenant_id}
                 }
 
                 # Permanent solution is to put tenant_id in parameters.tenant_id
                 parameters.tenant_id = reference.conversation.tenant_id
 
+            # This is different from C# where credentials are required in the method call.
+            # Doing this for compatibility.
+            app_credentials = (
+                credentials
+                if credentials
+                else await self.__get_app_credentials(
+                    self.settings.app_id, self.__get_botframework_oauth_scope()
+                )
+            )
+
+            # Create conversation
+            client = self._get_or_create_connector_client(service_url, app_credentials)
+
             resource_response = await client.conversations.create_conversation(
                 parameters
             )
-            request = TurnContext.apply_conversation_reference(
-                Activity(), reference, is_incoming=True
+
+            event_activity = Activity(
+                type=ActivityTypes.event,
+                name=ActivityEventNames.create_conversation,
+                channel_id=channel_id,
+                service_url=service_url,
+                id=resource_response.activity_id
+                if resource_response.activity_id
+                else str(uuid.uuid4()),
+                conversation=ConversationAccount(
+                    id=resource_response.id, tenant_id=parameters.tenant_id,
+                ),
+                channel_data=parameters.channel_data,
+                recipient=parameters.bot,
             )
-            request.conversation = ConversationAccount(
-                id=resource_response.id, tenant_id=parameters.tenant_id
+
+            context = self._create_context(event_activity)
+            context.turn_state[BotAdapter.BOT_CONNECTOR_CLIENT_KEY] = client
+
+            claims_identity = ClaimsIdentity(
+                claims={
+                    AuthenticationConstants.AUDIENCE_CLAIM: app_credentials.microsoft_app_id,
+                    AuthenticationConstants.APP_ID_CLAIM: app_credentials.microsoft_app_id,
+                    AuthenticationConstants.SERVICE_URL_CLAIM: service_url,
+                },
+                is_authenticated=True,
             )
-            request.channel_data = parameters.channel_data
-            if resource_response.service_url:
-                request.service_url = resource_response.service_url
+            context.turn_state[BotAdapter.BOT_IDENTITY_KEY] = claims_identity
 
-            context = self.create_context(request)
             return await self.run_pipeline(context, logic)
 
         except Exception as error:
@@ -195,19 +424,52 @@ async def create_conversation(
 
     async def process_activity(self, req, auth_header: str, logic: Callable):
         """
-        Processes an activity received by the bots web server. This includes any messages sent from a
-        user and is the method that drives what's often referred to as the bots "Reactive Messaging"
-        flow.
-        :param req:
-        :param auth_header:
-        :param logic:
-        :return:
+        Creates a turn context and runs the middleware pipeline for an incoming activity.
+
+        :param req: The incoming activity
+        :type req: :class:`typing.str`
+        :param auth_header: The HTTP authentication header of the request
+        :type auth_header: :class:`typing.str`
+        :param logic: The logic to execute at the end of the adapter's middleware pipeline.
+        :type logic: :class:`typing.Callable`
+
+        :return: A task that represents the work queued to execute.
+
+        .. remarks::
+            This class processes an activity received by the bots web server. This includes any messages
+            sent from a user and is the method that drives what's often referred to as the
+            bots *reactive messaging* flow.
+            Call this method to reactively send a message to a conversation.
+            If the task completes successfully, then an :class:`InvokeResponse` is returned;
+            otherwise. `null` is returned.
         """
         activity = await self.parse_request(req)
         auth_header = auth_header or ""
+        identity = await self._authenticate_request(activity, auth_header)
+        return await self.process_activity_with_identity(activity, identity, logic)
 
-        await self.authenticate_request(activity, auth_header)
-        context = self.create_context(activity)
+    async def process_activity_with_identity(
+        self, activity: Activity, identity: ClaimsIdentity, logic: Callable
+    ):
+        context = self._create_context(activity)
+
+        activity.caller_id = await self.__generate_callerid(identity)
+        context.turn_state[BotAdapter.BOT_IDENTITY_KEY] = identity
+        context.turn_state[BotAdapter.BOT_CALLBACK_HANDLER_KEY] = logic
+
+        # The OAuthScope is also stored on the TurnState to get the correct AppCredentials if fetching
+        # a token is required.
+        scope = (
+            JwtTokenValidation.get_app_id_from_claims(identity.claims)
+            if SkillValidation.is_skill_claim(identity.claims)
+            else self.__get_botframework_oauth_scope()
+        )
+        context.turn_state[BotAdapter.BOT_OAUTH_SCOPE_KEY] = scope
+
+        client = await self.create_connector_client(
+            activity.service_url, identity, scope
+        )
+        context.turn_state[BotAdapter.BOT_CONNECTOR_CLIENT_KEY] = client
 
         # Fix to assign tenant_id from channelData to Conversation.tenant_id.
         # MS Teams currently sends the tenant ID in channelData and the correct behavior is to expose
@@ -218,6 +480,7 @@ async def process_activity(self, req, auth_header: str, logic: Callable):
             Channels.ms_teams == context.activity.channel_id
             and context.activity.conversation is not None
             and not context.activity.conversation.tenant_id
+            and context.activity.channel_data
         ):
             teams_channel_data = context.activity.channel_data
             if teams_channel_data.get("tenant", {}).get("id", None):
@@ -225,26 +488,83 @@ async def process_activity(self, req, auth_header: str, logic: Callable):
                     teams_channel_data["tenant"]["id"]
                 )
 
-        return await self.run_pipeline(context, logic)
+        await self.run_pipeline(context, logic)
+
+        # Handle ExpectedReplies scenarios where the all the activities have been buffered and sent back at once
+        # in an invoke response.
+        # Return the buffered activities in the response.  In this case, the invoker
+        # should deserialize accordingly:
+        #    activities = ExpectedReplies().deserialize(response.body).activities
+        if context.activity.delivery_mode == DeliveryModes.expect_replies:
+            expected_replies = ExpectedReplies(
+                activities=context.buffered_reply_activities
+            ).serialize()
+            return InvokeResponse(status=int(HTTPStatus.OK), body=expected_replies)
+
+        # Handle Invoke scenarios, which deviate from the request/request model in that
+        # the Bot will return a specific body and return code.
+        if activity.type == ActivityTypes.invoke:
+            invoke_response = context.turn_state.get(
+                BotFrameworkAdapter._INVOKE_RESPONSE_KEY  # pylint: disable=protected-access
+            )
+            if invoke_response is None:
+                return InvokeResponse(status=int(HTTPStatus.NOT_IMPLEMENTED))
+            return invoke_response.value
+
+        return None
+
+    async def __generate_callerid(self, claims_identity: ClaimsIdentity) -> str:
+        # Is the bot accepting all incoming messages?
+        is_auth_disabled = await self._credential_provider.is_authentication_disabled()
+        if is_auth_disabled:
+            # Return None so that the callerId is cleared.
+            return None
+
+        # Is the activity from another bot?
+        if SkillValidation.is_skill_claim(claims_identity.claims):
+            app_id = JwtTokenValidation.get_app_id_from_claims(claims_identity.claims)
+            return f"{CallerIdConstants.bot_to_bot_prefix}{app_id}"
+
+        # Is the activity from Public Azure?
+        if not self._channel_provider or self._channel_provider.is_public_azure():
+            return CallerIdConstants.public_azure_channel
+
+        # Is the activity from Azure Gov?
+        if self._channel_provider and self._channel_provider.is_government():
+            return CallerIdConstants.us_gov_channel
+
+        # Return None so that the callerId is cleared.
+        return None
 
-    async def authenticate_request(self, request: Activity, auth_header: str):
+    async def _authenticate_request(
+        self, request: Activity, auth_header: str
+    ) -> ClaimsIdentity:
         """
         Allows for the overriding of authentication in unit tests.
-        :param request:
-        :param auth_header:
-        :return:
+
+        :param request: The request to authenticate
+        :type request: :class:`botbuilder.schema.Activity`
+        :param auth_header: The authentication header
+
+        :raises: A permission exception error.
+
+        :return: The request claims identity
+        :rtype: :class:`botframework.connector.auth.ClaimsIdentity`
         """
         claims = await JwtTokenValidation.authenticate_request(
             request,
             auth_header,
             self._credential_provider,
-            self.settings.channel_service,
+            await self.settings.channel_provider.get_channel_service(),
+            self.settings.auth_configuration,
         )
 
         if not claims.is_authenticated:
-            raise Exception("Unauthorized Access. Request is not authorized")
+            raise PermissionError("Unauthorized Access. Request is not authorized")
+
+        return claims
 
-    def create_context(self, activity):
+    def _create_context(self, activity):
         """
         Allows for the overriding of the context object in unit tests and derived adapters.
         :param activity:
@@ -300,12 +620,25 @@ async def update_activity(self, context: TurnContext, activity: Activity):
         """
         Replaces an activity that was previously sent to a channel. It should be noted that not all
         channels support this feature.
-        :param context:
-        :param activity:
-        :return:
+
+        :param context: The context object for the turn
+        :type context: :class:`TurnContext'
+        :param activity: New replacement activity
+        :type activity: :class:`botbuilder.schema.Activity`
+
+        :raises: A generic exception error
+
+        :return: A task that represents the work queued to execute
+
+        .. remarks::
+            If the activity is successfully sent, the task result contains
+            a :class:`botbuilder.schema.ResourceResponse` object containing the ID that
+            the receiving channel assigned to the activity.
+            Before calling this function, set the ID of the replacement activity to the ID
+            of the activity to replace.
         """
         try:
-            client = self.create_connector_client(activity.service_url)
+            client = context.turn_state[BotAdapter.BOT_CONNECTOR_CLIENT_KEY]
             return await client.conversations.update_activity(
                 activity.conversation.id, activity.id, activity
             )
@@ -318,21 +651,35 @@ async def delete_activity(
         """
         Deletes an activity that was previously sent to a channel. It should be noted that not all
         channels support this feature.
-        :param context:
-        :param reference:
-        :return:
+
+        :param context: The context object for the turn
+        :type context: :class:`TurnContext'
+        :param reference: Conversation reference for the activity to delete
+        :type reference: :class:`botbuilder.schema.ConversationReference`
+
+        :raises: A exception error
+
+        :return: A task that represents the work queued to execute
+
+        .. note::
+
+            The activity_id of the :class:`botbuilder.schema.ConversationReference` identifies the activity to delete.
         """
         try:
-            client = self.create_connector_client(reference.service_url)
+            client = context.turn_state[BotAdapter.BOT_CONNECTOR_CLIENT_KEY]
             await client.conversations.delete_activity(
                 reference.conversation.id, reference.activity_id
             )
         except Exception as error:
             raise error
 
-    async def send_activities(self, context: TurnContext, activities: List[Activity]):
+    async def send_activities(
+        self, context: TurnContext, activities: List[Activity]
+    ) -> List[ResourceResponse]:
         try:
+            responses: List[ResourceResponse] = []
             for activity in activities:
+                response: ResourceResponse = None
                 if activity.type == "delay":
                     try:
                         delay_in_ms = float(activity.value) / 1000
@@ -345,17 +692,39 @@ async def send_activities(self, context: TurnContext, activities: List[Activity]
                     else:
                         await asyncio.sleep(delay_in_ms)
                 elif activity.type == "invokeResponse":
-                    context.turn_state.add(self._INVOKE_RESPONSE_KEY)
-                elif activity.reply_to_id:
-                    client = self.create_connector_client(activity.service_url)
-                    await client.conversations.reply_to_activity(
-                        activity.conversation.id, activity.reply_to_id, activity
-                    )
+                    context.turn_state[self._INVOKE_RESPONSE_KEY] = activity
                 else:
-                    client = self.create_connector_client(activity.service_url)
-                    await client.conversations.send_to_conversation(
-                        activity.conversation.id, activity
-                    )
+                    if not getattr(activity, "service_url", None):
+                        raise TypeError(
+                            "BotFrameworkAdapter.send_activity(): service_url can not be None."
+                        )
+                    if (
+                        not hasattr(activity, "conversation")
+                        or not activity.conversation
+                        or not getattr(activity.conversation, "id", None)
+                    ):
+                        raise TypeError(
+                            "BotFrameworkAdapter.send_activity(): conversation.id can not be None."
+                        )
+
+                    if activity.type == "trace" and activity.channel_id != "emulator":
+                        pass
+                    elif activity.reply_to_id:
+                        client = context.turn_state[BotAdapter.BOT_CONNECTOR_CLIENT_KEY]
+                        response = await client.conversations.reply_to_activity(
+                            activity.conversation.id, activity.reply_to_id, activity
+                        )
+                    else:
+                        client = context.turn_state[BotAdapter.BOT_CONNECTOR_CLIENT_KEY]
+                        response = await client.conversations.send_to_conversation(
+                            activity.conversation.id, activity
+                        )
+
+                if not response:
+                    response = ResourceResponse(id=activity.id or "")
+
+                responses.append(response)
+            return responses
         except Exception as error:
             raise error
 
@@ -364,9 +733,15 @@ async def delete_conversation_member(
     ) -> None:
         """
         Deletes a member from the current conversation.
-        :param context:
-        :param member_id:
-        :return:
+
+        :param context: The context object for the turn
+        :type context: :class:`botbuilder.core.TurnContext`
+        :param member_id: The ID of the member to remove from the conversation
+        :type member_id: str
+
+        :raises: A exception error
+
+        :return: A task that represents the work queued to execute. ChannelAccount:
         """
-        Lists the Conversations in which this bot has participated for a given channel server. The channel server
-        returns results in pages and each page will include a `continuationToken` that can be used to fetch the next
-        page of results from the server.
-        :param service_url:
-        :param continuation_token:
-        :return:
+        Retrieve a member of a current conversation.
+
+        :param context: The context object for the turn
+        :type context: :class:`botbuilder.core.TurnContext`
+        :param member_id: The member Id
+        :type member_id: str
+
+        :raises: A TypeError if missing member_id, service_url, or conversation.id
+
+        :return: A member of the current conversation
+        """
+        if not context.activity.service_url:
+            raise TypeError(
+                "BotFrameworkAdapter.get_conversation_member(): missing service_url"
+            )
+        if not context.activity.conversation or not context.activity.conversation.id:
+            raise TypeError(
+                "BotFrameworkAdapter.get_conversation_member(): missing conversation or "
+                "conversation.id"
+            )
+        if not member_id:
+            raise TypeError(
+                "BotFrameworkAdapter.get_conversation_member(): missing memberId"
+            )
+
+        client = context.turn_state[BotAdapter.BOT_CONNECTOR_CLIENT_KEY]
+        return await client.conversations.get_conversation_member(
+            context.activity.conversation.id, member_id
+        )
+
+    async def get_conversations(
+        self,
+        service_url: str,
+        credentials: AppCredentials,
+        continuation_token: str = None,
+    ):
         """
-        client = self.create_connector_client(service_url)
+        Lists the Conversations in which this bot has participated for a given channel server.
+
+        :param service_url: The URL of the channel server to query. This can be retrieved from
+        `context.activity.serviceUrl`
+        :type service_url: str
+
+        :param continuation_token: The continuation token from the previous page of results
+        :type continuation_token: str
+
+        :raises: A generic exception error
+
+        :return: A task that represents the work queued to execute
+
+        .. remarks::
+            The channel server returns results in pages and each page will include a `continuationToken` that
+            can be used to fetch the next page of results from the server.
+            If the task completes successfully, the result contains a page of the members of the current conversation.
+            This overload may be called from outside the context of a conversation, as only the bot's service URL and
+            credentials are required.
+        """
+        client = self._get_or_create_connector_client(service_url, credentials)
         return await client.conversations.get_conversations(continuation_token)
 
     async def get_user_token(
-        self, context: TurnContext, connection_name: str, magic_code: str = None
+        self,
+        context: TurnContext,
+        connection_name: str,
+        magic_code: str = None,
+        oauth_app_credentials: AppCredentials = None,  # pylint: disable=unused-argument
     ) -> TokenResponse:
+
+        """
+        Attempts to retrieve the token for a user that's in a login flow.
+
+        :param context: Context for the current turn of conversation with the user
+        :type context: :class:`botbuilder.core.TurnContext`
+        :param connection_name: Name of the auth connection to use
+        :type connection_name: str
+        :param magic_code" (Optional) user entered code to validate
+        :str magic_code" str
+        :param oauth_app_credentials: (Optional) AppCredentials for OAuth.
+        :type oauth_app_credentials: :class:`botframework.connector.auth.AppCredential`
+
+        :raises: An exception error
+
+        :returns: Token Response
+        :rtype: :class:'botbuilder.schema.TokenResponse`
+
+        """
+
         if (
             context.activity.from_property is None
             or not context.activity.from_property.id
@@ -480,24 +936,39 @@ async def get_user_token(
                 "get_user_token() requires a connection_name but none was provided."
             )
 
-        self.check_emulating_oauth_cards(context)
-        user_id = context.activity.from_property.id
-        url = self.oauth_api_url(context)
-        client = self.create_token_api_client(url)
+        client = await self._create_token_api_client(context, oauth_app_credentials)
 
         result = client.user_token.get_token(
-            user_id, connection_name, context.activity.channel_id, magic_code
+            context.activity.from_property.id,
+            connection_name,
+            context.activity.channel_id,
+            magic_code,
         )
 
-        # TODO check form of response
         if result is None or result.token is None:
             return None
 
         return result
 
     async def sign_out_user(
-        self, context: TurnContext, connection_name: str = None, user_id: str = None
-    ) -> str:
+        self,
+        context: TurnContext,
+        connection_name: str = None,  # pylint: disable=unused-argument
+        user_id: str = None,
+        oauth_app_credentials: AppCredentials = None,
+    ):
+        """
+        Signs the user out with the token server.
+
+        :param context: Context for the current turn of conversation with the user
+        :type context: :class:`botbuilder.core.TurnContext`
+        :param connection_name: Name of the auth connection to use
+        :type connection_name: str
+        :param user_id: User id of user to sign out
+        :type user_id: str
+        :param oauth_app_credentials: (Optional) AppCredentials for OAuth.
+        :type oauth_app_credentials: :class:`botframework.connector.auth.AppCredential`
+        """
         if not context.activity.from_property or not context.activity.from_property.id:
             raise Exception(
                 "BotFrameworkAdapter.sign_out_user(): missing from_property or from_property.id"
@@ -505,24 +976,40 @@ async def sign_out_user(
         if not user_id:
             user_id = context.activity.from_property.id
 
-        self.check_emulating_oauth_cards(context)
-        url = self.oauth_api_url(context)
-        client = self.create_token_api_client(url)
+        client = await self._create_token_api_client(context, oauth_app_credentials)
         client.user_token.sign_out(
             user_id, connection_name, context.activity.channel_id
         )
 
     async def get_oauth_sign_in_link(
-        self, context: TurnContext, connection_name: str
+        self,
+        context: TurnContext,
+        connection_name: str,
+        final_redirect: str = None,  # pylint: disable=unused-argument
+        oauth_app_credentials: AppCredentials = None,
     ) -> str:
-        self.check_emulating_oauth_cards(context)
+        """
+        Gets the raw sign-in link to be sent to the user for sign-in for a connection name.
+
+        :param context: Context for the current turn of conversation with the user
+        :type context: :class:`botbuilder.core.TurnContext`
+        :param connection_name: Name of the auth connection to use
+        :type connection_name: str
+        :param final_redirect: The final URL that the OAuth flow will redirect to.
+        :param oauth_app_credentials: (Optional) AppCredentials for OAuth.
+        :type oauth_app_credentials: :class:`botframework.connector.auth.AppCredential`
+
+        :return: If the task completes successfully, the result contains the raw sign-in link
+        """
+
+        client = await self._create_token_api_client(context, oauth_app_credentials)
+
         conversation = TurnContext.get_conversation_reference(context.activity)
-        url = self.oauth_api_url(context)
-        client = self.create_token_api_client(url)
         state = TokenExchangeState(
             connection_name=connection_name,
             conversation=conversation,
             ms_app_id=client.config.credentials.microsoft_app_id,
+            relates_to=context.activity.relates_to,
         )
 
         final_state = base64.b64encode(
@@ -532,8 +1019,31 @@ async def get_oauth_sign_in_link(
         return client.bot_sign_in.get_sign_in_url(final_state)
 
     async def get_token_status(
-        self, context: TurnContext, user_id: str = None, include_filter: str = None
+        self,
+        context: TurnContext,
+        connection_name: str = None,
+        user_id: str = None,
+        include_filter: str = None,
+        oauth_app_credentials: AppCredentials = None,
     ) -> List[TokenStatus]:
+        """
+        Retrieves the token status for each configured connection for the given user.
+
+        :param context: Context for the current turn of conversation with the user
+        :type context: :class:`botbuilder.core.TurnContext`
+        :param connection_name: Name of the auth connection to use
+        :type connection_name: str
+        :param user_id: The user Id for which tokens are retrieved. If passing in None the userId is taken
+        :type user_id: str
+        :param include_filter: (Optional) Comma separated list of connection's to include.
+        Blank will return token status for all configured connections.
+        :type include_filter: str
+        :param oauth_app_credentials: (Optional) AppCredentials for OAuth.
+        :type oauth_app_credentials: :class:`botframework.connector.auth.AppCredential`
+
+        :returns: Array of :class:`botframework.connector.token_api.modelsTokenStatus`
+        """
+
         if not user_id and (
             not context.activity.from_property or not context.activity.from_property.id
         ):
@@ -541,58 +1051,266 @@ async def get_token_status(
                 "BotFrameworkAdapter.get_token_status(): missing from_property or from_property.id"
             )
 
-        self.check_emulating_oauth_cards(context)
-        user_id = user_id or context.activity.from_property.id
-        url = self.oauth_api_url(context)
-        client = self.create_token_api_client(url)
+        client = await self._create_token_api_client(context, oauth_app_credentials)
 
-        # TODO check form of response
+        user_id = user_id or context.activity.from_property.id
         return client.user_token.get_token_status(
             user_id, context.activity.channel_id, include_filter
         )
 
     async def get_aad_tokens(
-        self, context: TurnContext, connection_name: str, resource_urls: List[str]
+        self,
+        context: TurnContext,
+        connection_name: str,
+        resource_urls: List[str],
+        user_id: str = None,  # pylint: disable=unused-argument
+        oauth_app_credentials: AppCredentials = None,
     ) -> Dict[str, TokenResponse]:
+        """
+        Retrieves Azure Active Directory tokens for particular resources on a configured connection.
+
+        :param context: Context for the current turn of conversation with the user
+        :type context: :class:`botbuilder.core.TurnContext`
+        :param connection_name: The name of the Azure Active Directory connection configured with this bot
+        :type connection_name: str
+        :param resource_urls: The list of resource URLs to retrieve tokens for
+        :type resource_urls: :class:`typing.List`
+        :param user_id: The user Id for which tokens are retrieved. If passing in null the userId is taken
+        from the Activity in the TurnContext.
+        :type user_id: str
+        :param oauth_app_credentials: (Optional) AppCredentials for OAuth.
+        :type oauth_app_credentials: :class:`botframework.connector.auth.AppCredential`
+
+        :returns: Dictionary of resource Urls to the corresponding :class:'botbuilder.schema.TokenResponse`
+        :rtype: :class:`typing.Dict`
+        """
         if not context.activity.from_property or not context.activity.from_property.id:
             raise Exception(
                 "BotFrameworkAdapter.get_aad_tokens(): missing from_property or from_property.id"
             )
 
-        self.check_emulating_oauth_cards(context)
-        user_id = context.activity.from_property.id
-        url = self.oauth_api_url(context)
-        client = self.create_token_api_client(url)
-
-        # TODO check form of response
+        client = await self._create_token_api_client(context, oauth_app_credentials)
         return client.user_token.get_aad_tokens(
-            user_id, connection_name, context.activity.channel_id, resource_urls
+            context.activity.from_property.id,
+            connection_name,
+            context.activity.channel_id,
+            resource_urls,
         )
 
-    def create_connector_client(self, service_url: str) -> ConnectorClient:
+    async def create_connector_client(
+        self, service_url: str, identity: ClaimsIdentity = None, audience: str = None
+    ) -> ConnectorClient:
         """
-        Allows for mocking of the connector client in unit tests.
-        :param service_url:
-        :return:
+        Implementation of ConnectorClientProvider.create_connector_client.
+
+        :param service_url: The service URL
+        :param identity: The claims identity
+        :param audience:
+
+        :return: An instance of the :class:`ConnectorClient` class
         """
-        client = ConnectorClient(self._credentials, base_url=service_url)
-        client.config.add_user_agent(USER_AGENT)
+
+        if not identity:
+            # This is different from C# where an exception is raised.  In this case
+            # we are creating a ClaimsIdentity to retain compatibility with this
+            # method.
+            identity = ClaimsIdentity(
+                claims={
+                    AuthenticationConstants.AUDIENCE_CLAIM: self.settings.app_id,
+                    AuthenticationConstants.APP_ID_CLAIM: self.settings.app_id,
+                },
+                is_authenticated=True,
+            )
+
+        # For requests from channel App Id is in Audience claim of JWT token. For emulator it is in AppId claim.
+        # For unauthenticated requests we have anonymous claimsIdentity provided auth is disabled.
+        # For Activities coming from Emulator AppId claim contains the Bot's AAD AppId.
+        bot_app_id = identity.claims.get(
+            AuthenticationConstants.AUDIENCE_CLAIM
+        ) or identity.claims.get(AuthenticationConstants.APP_ID_CLAIM)
+
+        # Anonymous claims and non-skill claims should fall through without modifying the scope.
+        credentials = None
+        if bot_app_id:
+            scope = audience
+            if not scope:
+                scope = (
+                    JwtTokenValidation.get_app_id_from_claims(identity.claims)
+                    if SkillValidation.is_skill_claim(identity.claims)
+                    else self.__get_botframework_oauth_scope()
+                )
+
+            credentials = await self.__get_app_credentials(bot_app_id, scope)
+
+        return self._get_or_create_connector_client(service_url, credentials)
+
+    def _get_or_create_connector_client(
+        self, service_url: str, credentials: AppCredentials
+    ) -> ConnectorClient:
+        if not credentials:
+            credentials = MicrosoftAppCredentials.empty()
+
+        # Get ConnectorClient from cache or create.
+        client_key = BotFrameworkAdapter.key_for_connector_client(
+            service_url, credentials.microsoft_app_id, credentials.oauth_scope
+        )
+        client = self._connector_client_cache.get(client_key)
+        if not client:
+            client = ConnectorClient(credentials, base_url=service_url)
+            client.config.add_user_agent(USER_AGENT)
+            self._connector_client_cache[client_key] = client
+
         return client
 
-    def create_token_api_client(self, service_url: str) -> TokenApiClient:
-        client = TokenApiClient(self._credentials, service_url)
+    async def get_sign_in_resource_from_user(
+        self,
+        turn_context: TurnContext,
+        connection_name: str,
+        user_id: str,
+        final_redirect: str = None,
+    ) -> SignInUrlResponse:
+        return await self.get_sign_in_resource_from_user_and_credentials(
+            turn_context, None, connection_name, user_id, final_redirect
+        )
+
+    async def get_sign_in_resource_from_user_and_credentials(
+        self,
+        turn_context: TurnContext,
+        oauth_app_credentials: AppCredentials,
+        connection_name: str,
+        user_id: str,
+        final_redirect: str = None,
+    ) -> SignInUrlResponse:
+        if not connection_name:
+            raise TypeError(
+                "BotFrameworkAdapter.get_sign_in_resource_from_user_and_credentials(): missing connection_name"
+            )
+        if not user_id:
+            raise TypeError(
+                "BotFrameworkAdapter.get_sign_in_resource_from_user_and_credentials(): missing user_id"
+            )
+
+        activity = turn_context.activity
+
+        app_id = self.__get_app_id(turn_context)
+        token_exchange_state = TokenExchangeState(
+            connection_name=connection_name,
+            conversation=ConversationReference(
+                activity_id=activity.id,
+                bot=activity.recipient,
+                channel_id=activity.channel_id,
+                conversation=activity.conversation,
+                locale=activity.locale,
+                service_url=activity.service_url,
+                user=activity.from_property,
+            ),
+            relates_to=activity.relates_to,
+            ms_app_id=app_id,
+        )
+
+        state = base64.b64encode(
+            json.dumps(token_exchange_state.serialize()).encode(
+                encoding="UTF-8", errors="strict"
+            )
+        ).decode()
+
+        client = await self._create_token_api_client(
+            turn_context, oauth_app_credentials
+        )
+
+        return client.bot_sign_in.get_sign_in_resource(
+            state, final_redirect=final_redirect
+        )
+
+    async def exchange_token(
+        self,
+        turn_context: TurnContext,
+        connection_name: str,
+        user_id: str,
+        exchange_request: TokenExchangeRequest,
+    ) -> TokenResponse:
+        return await self.exchange_token_from_credentials(
+            turn_context, None, connection_name, user_id, exchange_request
+        )
+
+    async def exchange_token_from_credentials(
+        self,
+        turn_context: TurnContext,
+        oauth_app_credentials: AppCredentials,
+        connection_name: str,
+        user_id: str,
+        exchange_request: TokenExchangeRequest,
+    ) -> TokenResponse:
+        # pylint: disable=no-member
+
+        if not connection_name:
+            raise TypeError(
+                "BotFrameworkAdapter.exchange_token(): missing connection_name"
+            )
+        if not user_id:
+            raise TypeError("BotFrameworkAdapter.exchange_token(): missing user_id")
+        if exchange_request and not exchange_request.token and not exchange_request.uri:
+            raise TypeError(
+                "BotFrameworkAdapter.exchange_token(): Either a Token or Uri property is required"
+                " on the TokenExchangeRequest"
+            )
+
+        client = await self._create_token_api_client(
+            turn_context, oauth_app_credentials
+        )
+
+        result = client.user_token.exchange_async(
+            user_id,
+            connection_name,
+            turn_context.activity.channel_id,
+            exchange_request.uri,
+            exchange_request.token,
+        )
+
+        if isinstance(result, TokenResponse):
+            return result
+        raise TypeError(f"exchange_async returned improper result: {type(result)}")
+
+    @staticmethod
+    def key_for_connector_client(service_url: str, app_id: str, scope: str):
+        return f"{service_url if service_url else ''}:{app_id if app_id else ''}:{scope if scope else ''}"
+
+    async def _create_token_api_client(
+        self, context: TurnContext, oauth_app_credentials: AppCredentials = None,
+    ) -> TokenApiClient:
+        if (
+            not self._is_emulating_oauth_cards
+            and context.activity.channel_id == "emulator"
+            and await self._credential_provider.is_authentication_disabled()
+        ):
+            self._is_emulating_oauth_cards = True
+
+        app_id = self.__get_app_id(context)
+        scope = self.__get_botframework_oauth_scope()
+        app_credentials = oauth_app_credentials or await self.__get_app_credentials(
+            app_id, scope
+        )
+
+        if (
+            not self._is_emulating_oauth_cards
+            and context.activity.channel_id == "emulator"
+            and await self._credential_provider.is_authentication_disabled()
+        ):
+            self._is_emulating_oauth_cards = True
+
+        # TODO: token_api_client cache
+
+        url = self.__oauth_api_url(context)
+        client = TokenApiClient(app_credentials, url)
         client.config.add_user_agent(USER_AGENT)
 
-        return client
+        if self._is_emulating_oauth_cards:
+            # intentionally not awaiting this call
+            EmulatorApiClient.emulate_oauth_cards(app_credentials, url, True)
 
-    async def emulate_oauth_cards(
-        self, context_or_service_url: Union[TurnContext, str], emulate: bool
-    ):
-        self._is_emulating_oauth_cards = emulate
-        url = self.oauth_api_url(context_or_service_url)
-        await EmulatorApiClient.emulate_oauth_cards(self._credentials, url, emulate)
+        return client
 
-    def oauth_api_url(self, context_or_service_url: Union[TurnContext, str]) -> str:
+    def __oauth_api_url(self, context_or_service_url: Union[TurnContext, str]) -> str:
         url = None
         if self._is_emulating_oauth_cards:
             url = (
@@ -606,19 +1324,76 @@ def oauth_api_url(self, context_or_service_url: Union[TurnContext, str]) -> str:
             else:
                 url = (
                     US_GOV_OAUTH_ENDPOINT
-                    if JwtTokenValidation.is_government(self.settings.channel_service)
+                    if self.settings.channel_provider.is_government()
                     else OAUTH_ENDPOINT
                 )
 
         return url
 
-    def check_emulating_oauth_cards(self, context: TurnContext):
-        if (
-            not self._is_emulating_oauth_cards
-            and context.activity.channel_id == "emulator"
-            and (
-                not self._credentials.microsoft_app_id
-                or not self._credentials.microsoft_app_password
+    @staticmethod
+    def key_for_app_credentials(app_id: str, scope: str):
+        return f"{app_id}:{scope}"
+
+    async def __get_app_credentials(
+        self, app_id: str, oauth_scope: str
+    ) -> AppCredentials:
+        if not app_id:
+            return MicrosoftAppCredentials.empty()
+
+        # get from the cache if it's there
+        cache_key = BotFrameworkAdapter.key_for_app_credentials(app_id, oauth_scope)
+        app_credentials = self._app_credential_map.get(cache_key)
+        if app_credentials:
+            return app_credentials
+
+        # If app credentials were provided, use them as they are the preferred choice moving forward
+        if self._credentials:
+            self._app_credential_map[cache_key] = self._credentials
+            return self._credentials
+
+        # Credentials not found in cache, build them
+        app_credentials = await self.__build_credentials(app_id, oauth_scope)
+
+        # Cache the credentials for later use
+        self._app_credential_map[cache_key] = app_credentials
+
+        return app_credentials
+
+    async def __build_credentials(
+        self, app_id: str, oauth_scope: str = None
+    ) -> AppCredentials:
+        app_password = await self._credential_provider.get_app_password(app_id)
+
+        if self._channel_provider.is_government():
+            return MicrosoftGovernmentAppCredentials(
+                app_id,
+                app_password,
+                self.settings.channel_auth_tenant,
+                scope=oauth_scope,
             )
+
+        return MicrosoftAppCredentials(
+            app_id,
+            app_password,
+            self.settings.channel_auth_tenant,
+            oauth_scope=oauth_scope,
+        )
+
+    def __get_botframework_oauth_scope(self) -> str:
+        if (
+            self.settings.channel_provider
+            and self.settings.channel_provider.is_government()
         ):
-            self._is_emulating_oauth_cards = True
+            return GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
+        return AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
+
+    def __get_app_id(self, context: TurnContext) -> str:
+        identity = context.turn_state[BotAdapter.BOT_IDENTITY_KEY]
+        if not identity:
+            raise Exception("An IIdentity is required in TurnState for this operation.")
+
+        app_id = identity.claims.get(AuthenticationConstants.AUDIENCE_CLAIM)
+        if not app_id:
+            raise Exception("Unable to get the bot AppId from the audience claim.")
+
+        return app_id
diff --git a/libraries/botbuilder-core/botbuilder/core/bot_state.py b/libraries/botbuilder-core/botbuilder/core/bot_state.py
index dc835a9bd..867fb07e0 100644
--- a/libraries/botbuilder-core/botbuilder/core/bot_state.py
+++ b/libraries/botbuilder-core/botbuilder/core/bot_state.py
@@ -4,7 +4,9 @@
 from abc import abstractmethod
 from copy import deepcopy
 from typing import Callable, Dict, Union
+from jsonpickle.pickler import Pickler
 from botbuilder.core.state_property_accessor import StatePropertyAccessor
+from .bot_assert import BotAssert
 from .turn_context import TurnContext
 from .storage import Storage
 from .property_manager import PropertyManager
@@ -16,6 +18,7 @@ class CachedBotState:
     """
 
     def __init__(self, state: Dict[str, object] = None):
+
         self.state = state if state is not None else {}
         self.hash = self.compute_hash(state)
 
@@ -24,42 +27,84 @@ def is_changed(self) -> bool:
         return self.hash != self.compute_hash(self.state)
 
     def compute_hash(self, obj: object) -> str:
-        # TODO: Should this be compatible with C# JsonConvert.SerializeObject ?
-        return str(obj)
+        return str(Pickler().flatten(obj))
 
 
 class BotState(PropertyManager):
+    """
+    Defines a state management object and automates the reading and writing of
+    associated state properties to a storage layer.
+
+    .. remarks::
+        Each state management object defines a scope for a storage layer.
+        State properties are created within a state management scope, and the Bot Framework
+        defines these scopes: :class:`ConversationState`, :class:`UserState`, and :class:`PrivateConversationState`.
+        You can define additional scopes for your bot.
+    """
+
     def __init__(self, storage: Storage, context_service_key: str):
+        """
+        Initializes a new instance of the :class:`BotState` class.
+
+        :param storage: The storage layer this state management object will use to store and retrieve state
+        :type storage:  :class:`bptbuilder.core.Storage`
+        :param context_service_key: The key for the state cache for this :class:`BotState`
+        :type context_service_key: str
+
+        .. remarks::
+            This constructor creates a state management object and associated scope. The object uses
+            the :param storage: to persist state property values and the :param context_service_key: to cache state
+            within the context for each turn.
+
+        :raises: It raises an argument null exception.
+        """
         self.state_key = "state"
         self._storage = storage
         self._context_service_key = context_service_key
 
+    def get_cached_state(self, turn_context: TurnContext):
+        """
+        Gets the cached bot state instance that wraps the raw cached data for this "BotState"
+        from the turn context.
+
+        :param turn_context: The context object for this turn.
+        :type turn_context: :class:`TurnContext`
+        :return: The cached bot state instance.
+        """
+        BotAssert.context_not_none(turn_context)
+        return turn_context.turn_state.get(self._context_service_key)
+
     def create_property(self, name: str) -> StatePropertyAccessor:
         """
-        Create a property definition and register it with this BotState.
-        :param name: The name of the property.
-        :param force:
-        :return: If successful, the state property accessor created.
+        Creates a property definition and registers it with this :class:`BotState`.
+
+        :param name: The name of the property
+        :type name: str
+        :return: If successful, the state property accessor created
+        :rtype: :class:`StatePropertyAccessor`
         """
         if not name:
             raise TypeError("BotState.create_property(): name cannot be None or empty.")
         return BotStatePropertyAccessor(self, name)
 
     def get(self, turn_context: TurnContext) -> Dict[str, object]:
-        cached = turn_context.turn_state.get(self._context_service_key)
+        BotAssert.context_not_none(turn_context)
+        cached = self.get_cached_state(turn_context)
 
         return getattr(cached, "state", None)
 
     async def load(self, turn_context: TurnContext, force: bool = False) -> None:
         """
-        Reads in  the current state object and caches it in the context object for this turm.
-        :param turn_context: The context object for this turn.
-        :param force: Optional. True to bypass the cache.
+        Reads the current state object and caches it in the context object for this turn.
+
+        :param turn_context: The context object for this turn
+        :type turn_context: :class:`TurnContext`
+        :param force: Optional, true to bypass the cache
+        :type force: bool
         """
-        if turn_context is None:
-            raise TypeError("BotState.load(): turn_context cannot be None.")
+        BotAssert.context_not_none(turn_context)
 
-        cached_state = turn_context.turn_state.get(self._context_service_key)
+        cached_state = self.get_cached_state(turn_context)
         storage_key = self.get_storage_key(turn_context)
 
         if force or not cached_state or not cached_state.state:
@@ -71,15 +116,17 @@ async def save_changes(
         self, turn_context: TurnContext, force: bool = False
     ) -> None:
         """
-        If it has changed, writes to storage the state object that is cached in the current context object
-        for this turn.
-        :param turn_context: The context object for this turn.
-        :param force: Optional. True to save state to storage whether or not there are changes.
+        Saves the state cached in the current context for this turn.
+        If the state has changed, it saves the state cached in the current context for this turn.
+
+        :param turn_context: The context object for this turn
+        :type turn_context: :class:`TurnContext`
+        :param force: Optional, true to save state to storage whether or not there are changes
+        :type force: bool
         """
-        if turn_context is None:
-            raise TypeError("BotState.save_changes(): turn_context cannot be None.")
+        BotAssert.context_not_none(turn_context)
 
-        cached_state = turn_context.turn_state.get(self._context_service_key)
+        cached_state = self.get_cached_state(turn_context)
 
         if force or (cached_state is not None and cached_state.is_changed):
             storage_key = self.get_storage_key(turn_context)
@@ -90,12 +137,16 @@ async def save_changes(
     async def clear_state(self, turn_context: TurnContext):
         """
         Clears any state currently stored in this state scope.
-        NOTE: that save_changes must be called in order for the cleared state to be persisted to the underlying store.
-        :param turn_context: The context object for this turn.
+
+        :param turn_context: The context object for this turn
+        :type turn_context: :class:`TurnContext`
+
         :return: None
+
+        .. remarks::
+            This function must be called in order for the cleared state to be persisted to the underlying store.
         """
-        if turn_context is None:
-            raise TypeError("BotState.clear_state(): turn_context cannot be None.")
+        BotAssert.context_not_none(turn_context)
 
         #  Explicitly setting the hash will mean IsChanged is always true. And that will force a Save.
         cache_value = CachedBotState()
@@ -104,12 +155,14 @@ async def clear_state(self, turn_context: TurnContext):
 
     async def delete(self, turn_context: TurnContext) -> None:
         """
-        Delete any state currently stored in this state scope.
-        :param turn_context: The context object for this turn.
+        Deletes any state currently stored in this state scope.
+
+        :param turn_context: The context object for this turn
+        :type turn_context: :class:`TurnContext`
+
         :return: None
         """
-        if turn_context is None:
-            raise TypeError("BotState.delete(): turn_context cannot be None.")
+        BotAssert.context_not_none(turn_context)
 
         turn_context.turn_state.pop(self._context_service_key)
 
@@ -121,15 +174,22 @@ def get_storage_key(self, turn_context: TurnContext) -> str:
         raise NotImplementedError()
 
     async def get_property_value(self, turn_context: TurnContext, property_name: str):
-        if turn_context is None:
-            raise TypeError(
-                "BotState.get_property_value(): turn_context cannot be None."
-            )
+        """
+        Gets the value of the specified property in the turn context.
+
+        :param turn_context: The context object for this turn
+        :type turn_context: :class:`TurnContext`
+        :param property_name: The property name
+        :type property_name: str
+
+        :return: The value of the property
+        """
+        BotAssert.context_not_none(turn_context)
         if not property_name:
             raise TypeError(
                 "BotState.get_property_value(): property_name cannot be None."
             )
-        cached_state = turn_context.turn_state.get(self._context_service_key)
+        cached_state = self.get_cached_state(turn_context)
 
         # if there is no value, this will throw, to signal to IPropertyAccesor that a default value should be computed
         # This allows this to work with value types
@@ -140,47 +200,74 @@ async def delete_property_value(
     ) -> None:
         """
         Deletes a property from the state cache in the turn context.
-        :param turn_context: The context object for this turn.
-        :param property_name: The name of the property to delete.
+
+        :param turn_context: The context object for this turn
+        :type turn_context: :TurnContext`
+        :param property_name: The name of the property to delete
+        :type property_name: str
+
         :return: None
         """
-
-        if turn_context is None:
-            raise TypeError("BotState.delete_property(): turn_context cannot be None.")
+        BotAssert.context_not_none(turn_context)
         if not property_name:
             raise TypeError("BotState.delete_property(): property_name cannot be None.")
-        cached_state = turn_context.turn_state.get(self._context_service_key)
+        cached_state = self.get_cached_state(turn_context)
         del cached_state.state[property_name]
 
     async def set_property_value(
         self, turn_context: TurnContext, property_name: str, value: object
     ) -> None:
         """
-        Deletes a property from the state cache in the turn context.
-        :param turn_context: The context object for this turn.
-        :param property_name: The value to set on the property.
+        Sets a property to the specified value in the turn context.
+
+        :param turn_context: The context object for this turn
+        :type turn_context: :class:`TurnContext`
+        :param property_name: The property name
+        :type property_name: str
+        :param value: The value to assign to the property
+        :type value: Object
+
         :return: None
         """
-
-        if turn_context is None:
-            raise TypeError("BotState.delete_property(): turn_context cannot be None.")
+        BotAssert.context_not_none(turn_context)
         if not property_name:
             raise TypeError("BotState.delete_property(): property_name cannot be None.")
-        cached_state = turn_context.turn_state.get(self._context_service_key)
+        cached_state = self.get_cached_state(turn_context)
         cached_state.state[property_name] = value
 
 
-##
 class BotStatePropertyAccessor(StatePropertyAccessor):
+    """
+    Defines methods for accessing a state property created in a :class:`BotState` object.
+    """
+
     def __init__(self, bot_state: BotState, name: str):
+        """
+        Initializes a new instance of the :class:`BotStatePropertyAccessor` class.
+
+        :param bot_state: The state object to access
+        :type bot_state:  :class:`BotState`
+        :param name: The name of the state property to access
+        :type name: str
+
+        """
         self._bot_state = bot_state
         self._name = name
 
     @property
     def name(self) -> str:
+        """
+        The name of the property.
+        """
         return self._name
 
     async def delete(self, turn_context: TurnContext) -> None:
+        """
+        Deletes the property.
+
+        :param turn_context: The context object for this turn
+        :type turn_context: :class:`TurnContext`
+        """
         await self._bot_state.load(turn_context, False)
         await self._bot_state.delete_property_value(turn_context, self._name)
 
@@ -189,6 +276,13 @@ async def get(
         turn_context: TurnContext,
         default_value_or_factory: Union[Callable, object] = None,
     ) -> object:
+        """
+        Gets the property value.
+
+        :param turn_context: The context object for this turn
+        :type turn_context: :class:`TurnContext`
+        :param default_value_or_factory: Defines the default value for the property
+        """
         await self._bot_state.load(turn_context, False)
         try:
             result = await self._bot_state.get_property_value(turn_context, self._name)
@@ -207,5 +301,13 @@ async def get(
             return result
 
     async def set(self, turn_context: TurnContext, value: object) -> None:
+        """
+        Sets the property value.
+
+        :param turn_context: The context object for this turn
+        :type turn_context: :class:`TurnContext`
+
+        :param value: The value to assign to the property
+        """
         await self._bot_state.load(turn_context, False)
         await self._bot_state.set_property_value(turn_context, self._name, value)
diff --git a/libraries/botbuilder-core/botbuilder/core/bot_state_set.py b/libraries/botbuilder-core/botbuilder/core/bot_state_set.py
index 8a86aaba0..99016af48 100644
--- a/libraries/botbuilder-core/botbuilder/core/bot_state_set.py
+++ b/libraries/botbuilder-core/botbuilder/core/bot_state_set.py
@@ -1,3 +1,6 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
 from asyncio import wait
 from typing import List
 from .bot_state import BotState
diff --git a/libraries/botbuilder-core/botbuilder/core/bot_telemetry_client.py b/libraries/botbuilder-core/botbuilder/core/bot_telemetry_client.py
index c797b000b..0b935e943 100644
--- a/libraries/botbuilder-core/botbuilder/core/bot_telemetry_client.py
+++ b/libraries/botbuilder-core/botbuilder/core/bot_telemetry_client.py
@@ -144,7 +144,7 @@ def track_request(
         :param url: The actual URL for this request (to show in individual request instances).
         :param success: True if the request ended in success, False otherwise.
         :param start_time: the start time of the request. \
-        The value should look the same as the one returned by :func:`datetime.isoformat()` (defaults to: None)
+        The value should look the same as the one returned by :func:`datetime.isoformat`. (defaults to: None)
         :param duration: the number of milliseconds that this request lasted. (defaults to: None)
         :param response_code: the response code that this request returned. (defaults to: None)
         :param http_method: the HTTP method that triggered this request. (defaults to: None)
diff --git a/libraries/botbuilder-core/botbuilder/core/channel_service_handler.py b/libraries/botbuilder-core/botbuilder/core/channel_service_handler.py
new file mode 100644
index 000000000..9ed7104df
--- /dev/null
+++ b/libraries/botbuilder-core/botbuilder/core/channel_service_handler.py
@@ -0,0 +1,501 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from typing import List
+
+from botbuilder.schema import (
+    Activity,
+    AttachmentData,
+    ChannelAccount,
+    ConversationParameters,
+    ConversationsResult,
+    ConversationResourceResponse,
+    PagedMembersResult,
+    ResourceResponse,
+    Transcript,
+)
+
+from botframework.connector.auth import (
+    AuthenticationConfiguration,
+    ChannelProvider,
+    ClaimsIdentity,
+    CredentialProvider,
+    JwtTokenValidation,
+    SkillValidation,
+)
+
+
+class BotActionNotImplementedError(Exception):
+    """Raised when an action is not implemented"""
+
+
+class ChannelServiceHandler:
+    """
+    Initializes a new instance of the  class,
+    using a credential provider.
+    """
+
+    def __init__(
+        self,
+        credential_provider: CredentialProvider,
+        auth_config: AuthenticationConfiguration,
+        channel_provider: ChannelProvider = None,
+    ):
+        if not credential_provider:
+            raise TypeError("credential_provider can't be None")
+
+        if not auth_config:
+            raise TypeError("auth_config can't be None")
+
+        self._credential_provider = credential_provider
+        self._auth_config = auth_config
+        self._channel_provider = channel_provider
+
+    async def handle_send_to_conversation(
+        self, auth_header, conversation_id, activity
+    ) -> ResourceResponse:
+        claims_identity = await self._authenticate(auth_header)
+        return await self.on_send_to_conversation(
+            claims_identity, conversation_id, activity
+        )
+
+    async def handle_reply_to_activity(
+        self, auth_header, conversation_id, activity_id, activity
+    ) -> ResourceResponse:
+        claims_identity = await self._authenticate(auth_header)
+        return await self.on_reply_to_activity(
+            claims_identity, conversation_id, activity_id, activity
+        )
+
+    async def handle_update_activity(
+        self, auth_header, conversation_id, activity_id, activity
+    ) -> ResourceResponse:
+        claims_identity = await self._authenticate(auth_header)
+        return await self.on_update_activity(
+            claims_identity, conversation_id, activity_id, activity
+        )
+
+    async def handle_delete_activity(self, auth_header, conversation_id, activity_id):
+        claims_identity = await self._authenticate(auth_header)
+        await self.on_delete_activity(claims_identity, conversation_id, activity_id)
+
+    async def handle_get_activity_members(
+        self, auth_header, conversation_id, activity_id
+    ) -> List[ChannelAccount]:
+        claims_identity = await self._authenticate(auth_header)
+        return await self.on_get_activity_members(
+            claims_identity, conversation_id, activity_id
+        )
+
+    async def handle_create_conversation(
+        self, auth_header, parameters: ConversationParameters
+    ) -> ConversationResourceResponse:
+        claims_identity = await self._authenticate(auth_header)
+        return await self.on_create_conversation(claims_identity, parameters)
+
+    async def handle_get_conversations(
+        self, auth_header, continuation_token: str = ""
+    ) -> ConversationsResult:
+        claims_identity = await self._authenticate(auth_header)
+        return await self.on_get_conversations(claims_identity, continuation_token)
+
+    async def handle_get_conversation_members(
+        self, auth_header, conversation_id
+    ) -> List[ChannelAccount]:
+        claims_identity = await self._authenticate(auth_header)
+        return await self.on_get_conversation_members(claims_identity, conversation_id)
+
+    async def handle_get_conversation_member(
+        self, auth_header, conversation_id, member_id
+    ) -> ChannelAccount:
+        claims_identity = await self._authenticate(auth_header)
+        return await self.on_get_conversation_member(
+            claims_identity, conversation_id, member_id
+        )
+
+    async def handle_get_conversation_paged_members(
+        self,
+        auth_header,
+        conversation_id,
+        page_size: int = 0,
+        continuation_token: str = "",
+    ) -> PagedMembersResult:
+        claims_identity = await self._authenticate(auth_header)
+        return await self.on_get_conversation_paged_members(
+            claims_identity, conversation_id, page_size, continuation_token
+        )
+
+    async def handle_delete_conversation_member(
+        self, auth_header, conversation_id, member_id
+    ):
+        claims_identity = await self._authenticate(auth_header)
+        await self.on_delete_conversation_member(
+            claims_identity, conversation_id, member_id
+        )
+
+    async def handle_send_conversation_history(
+        self, auth_header, conversation_id, transcript: Transcript
+    ) -> ResourceResponse:
+        claims_identity = await self._authenticate(auth_header)
+        return await self.on_send_conversation_history(
+            claims_identity, conversation_id, transcript
+        )
+
+    async def handle_upload_attachment(
+        self, auth_header, conversation_id, attachment_upload: AttachmentData
+    ) -> ResourceResponse:
+        claims_identity = await self._authenticate(auth_header)
+        return await self.on_upload_attachment(
+            claims_identity, conversation_id, attachment_upload
+        )
+
+    async def on_get_conversations(
+        self, claims_identity: ClaimsIdentity, continuation_token: str = "",
+    ) -> ConversationsResult:
+        """
+        get_conversations() API for Skill
+
+        List the Conversations in which this bot has participated.
+
+        GET from this method with a skip token
+
+        The return value is a ConversationsResult, which contains an array of
+        ConversationMembers and a skip token.  If the skip token is not empty, then
+        there are further values to be returned. Call this method again with the
+        returned token to get more values.
+
+        Each ConversationMembers object contains the ID of the conversation and an
+        array of ChannelAccounts that describe the members of the conversation.
+
+        :param claims_identity:
+        :param conversation_id:
+        :param continuation_token:
+        :return:
+        """
+        raise BotActionNotImplementedError()
+
+    async def on_create_conversation(
+        self, claims_identity: ClaimsIdentity, parameters: ConversationParameters,
+    ) -> ConversationResourceResponse:
+        """
+        create_conversation() API for Skill
+
+        Create a new Conversation.
+
+        POST to this method with a
+        * Bot being the bot creating the conversation
+        * IsGroup set to true if this is not a direct message (default is false)
+        * Array containing the members to include in the conversation
+
+        The return value is a ResourceResponse which contains a conversation id
+        which is suitable for use
+        in the message payload and REST API uris.
+
+        Most channels only support the semantics of bots initiating a direct
+        message conversation.  An example of how to do that would be:
+
+        var resource = await connector.conversations.CreateConversation(new
+        ConversationParameters(){ Bot = bot, members = new ChannelAccount[] { new
+        ChannelAccount("user1") } );
+        await connect.Conversations.SendToConversationAsync(resource.Id, new
+        Activity() ... ) ;
+
+        end.
+
+        :param claims_identity:
+        :param parameters:
+        :return:
+        """
+        raise BotActionNotImplementedError()
+
+    async def on_send_to_conversation(
+        self, claims_identity: ClaimsIdentity, conversation_id: str, activity: Activity,
+    ) -> ResourceResponse:
+        """
+        send_to_conversation() API for Skill
+
+        This method allows you to send an activity to the end of a conversation.
+
+        This is slightly different from ReplyToActivity().
+        * SendToConversation(conversationId) - will append the activity to the end
+        of the conversation according to the timestamp or semantics of the channel.
+        * ReplyToActivity(conversationId,ActivityId) - adds the activity as a reply
+        to another activity, if the channel supports it. If the channel does not
+        support nested replies, ReplyToActivity falls back to SendToConversation.
+
+        Use ReplyToActivity when replying to a specific activity in the
+        conversation.
+
+        Use SendToConversation in all other cases.
+
+        :param claims_identity:
+        :param conversation_id:
+        :param activity:
+        :return:
+        """
+        raise BotActionNotImplementedError()
+
+    async def on_send_conversation_history(
+        self,
+        claims_identity: ClaimsIdentity,
+        conversation_id: str,
+        transcript: Transcript,
+    ) -> ResourceResponse:
+        """
+        send_conversation_history() API for Skill.
+
+        This method allows you to upload the historic activities to the
+        conversation.
+
+        Sender must ensure that the historic activities have unique ids and
+        appropriate timestamps. The ids are used by the client to deal with
+        duplicate activities and the timestamps are used by the client to render
+        the activities in the right order.
+
+        :param claims_identity:
+        :param conversation_id:
+        :param transcript:
+        :return:
+        """
+        raise BotActionNotImplementedError()
+
+    async def on_update_activity(
+        self,
+        claims_identity: ClaimsIdentity,
+        conversation_id: str,
+        activity_id: str,
+        activity: Activity,
+    ) -> ResourceResponse:
+        """
+        update_activity() API for Skill.
+
+        Edit an existing activity.
+
+        Some channels allow you to edit an existing activity to reflect the new
+        state of a bot conversation.
+
+        For example, you can remove buttons after someone has clicked "Approve"
+        button.
+
+        :param claims_identity:
+        :param conversation_id:
+        :param activity_id:
+        :param activity:
+        :return:
+        """
+        raise BotActionNotImplementedError()
+
+    async def on_reply_to_activity(
+        self,
+        claims_identity: ClaimsIdentity,
+        conversation_id: str,
+        activity_id: str,
+        activity: Activity,
+    ) -> ResourceResponse:
+        """
+        reply_to_activity() API for Skill.
+
+        This method allows you to reply to an activity.
+
+        This is slightly different from SendToConversation().
+        * SendToConversation(conversationId) - will append the activity to the end
+        of the conversation according to the timestamp or semantics of the channel.
+        * ReplyToActivity(conversationId,ActivityId) - adds the activity as a reply
+        to another activity, if the channel supports it. If the channel does not
+        support nested replies, ReplyToActivity falls back to SendToConversation.
+
+        Use ReplyToActivity when replying to a specific activity in the
+        conversation.
+
+        Use SendToConversation in all other cases.
+
+        :param claims_identity:
+        :param conversation_id:
+        :param activity_id:
+        :param activity:
+        :return:
+        """
+        raise BotActionNotImplementedError()
+
+    async def on_delete_activity(
+        self, claims_identity: ClaimsIdentity, conversation_id: str, activity_id: str,
+    ):
+        """
+        delete_activity() API for Skill.
+
+        Delete an existing activity.
+
+        Some channels allow you to delete an existing activity, and if successful
+        this method will remove the specified activity.
+
+        :param claims_identity:
+        :param conversation_id:
+        :param activity_id:
+        :return:
+        """
+        raise BotActionNotImplementedError()
+
+    async def on_get_conversation_members(
+        self, claims_identity: ClaimsIdentity, conversation_id: str,
+    ) -> List[ChannelAccount]:
+        """
+        get_conversation_members() API for Skill.
+
+        Enumerate the members of a conversation.
+
+        This REST API takes a ConversationId and returns a list of ChannelAccount
+        objects representing the members of the conversation.
+
+        :param claims_identity:
+        :param conversation_id:
+        :return:
+        """
+        raise BotActionNotImplementedError()
+
+    async def on_get_conversation_member(
+        self, claims_identity: ClaimsIdentity, conversation_id: str, member_id: str,
+    ) -> ChannelAccount:
+        """
+        get_conversation_member() API for Skill.
+
+        Enumerate the members of a conversation.
+
+        This REST API takes a ConversationId and returns a list of ChannelAccount
+        objects representing the members of the conversation.
+
+        :param claims_identity:
+        :param conversation_id:
+        :param member_id:
+        :return:
+        """
+        raise BotActionNotImplementedError()
+
+    async def on_get_conversation_paged_members(
+        self,
+        claims_identity: ClaimsIdentity,
+        conversation_id: str,
+        page_size: int = None,
+        continuation_token: str = "",
+    ) -> PagedMembersResult:
+        """
+        get_conversation_paged_members() API for Skill.
+
+        Enumerate the members of a conversation one page at a time.
+
+        This REST API takes a ConversationId. Optionally a page_size and/or
+        continuation_token can be provided. It returns a PagedMembersResult, which
+        contains an array
+        of ChannelAccounts representing the members of the conversation and a
+        continuation token that can be used to get more values.
+
+        One page of ChannelAccounts records are returned with each call. The number
+        of records in a page may vary between channels and calls. The page_size
+        parameter can be used as
+        a suggestion. If there are no additional results the response will not
+        contain a continuation token. If there are no members in the conversation
+        the Members will be empty or not present in the response.
+
+        A response to a request that has a continuation token from a prior request
+        may rarely return members from a previous request.
+
+        :param claims_identity:
+        :param conversation_id:
+        :param page_size:
+        :param continuation_token:
+        :return:
+        """
+        raise BotActionNotImplementedError()
+
+    async def on_delete_conversation_member(
+        self, claims_identity: ClaimsIdentity, conversation_id: str, member_id: str,
+    ):
+        """
+        delete_conversation_member() API for Skill.
+
+        Deletes a member from a conversation.
+
+        This REST API takes a ConversationId and a memberId (of type string) and
+        removes that member from the conversation. If that member was the last
+        member
+        of the conversation, the conversation will also be deleted.
+
+        :param claims_identity:
+        :param conversation_id:
+        :param member_id:
+        :return:
+        """
+        raise BotActionNotImplementedError()
+
+    async def on_get_activity_members(
+        self, claims_identity: ClaimsIdentity, conversation_id: str, activity_id: str,
+    ) -> List[ChannelAccount]:
+        """
+        get_activity_members() API for Skill.
+
+        Enumerate the members of an activity.
+
+        This REST API takes a ConversationId and a ActivityId, returning an array
+        of ChannelAccount objects representing the members of the particular
+        activity in the conversation.
+
+        :param claims_identity:
+        :param conversation_id:
+        :param activity_id:
+        :return:
+        """
+        raise BotActionNotImplementedError()
+
+    async def on_upload_attachment(
+        self,
+        claims_identity: ClaimsIdentity,
+        conversation_id: str,
+        attachment_upload: AttachmentData,
+    ) -> ResourceResponse:
+        """
+        upload_attachment() API for Skill.
+
+        Upload an attachment directly into a channel's blob storage.
+
+        This is useful because it allows you to store data in a compliant store
+        when dealing with enterprises.
+
+        The response is a ResourceResponse which contains an AttachmentId which is
+        suitable for using with the attachments API.
+
+        :param claims_identity:
+        :param conversation_id:
+        :param attachment_upload:
+        :return:
+        """
+        raise BotActionNotImplementedError()
+
+    async def _authenticate(self, auth_header: str) -> ClaimsIdentity:
+        """
+        Helper to authenticate the header.
+
+        This code is very similar to the code in JwtTokenValidation.authenticate_request,
+        we should move this code somewhere in that library when we refactor auth,
+        for now we keep it private to avoid adding more public static functions that we will need to deprecate later.
+        """
+        if not auth_header:
+            is_auth_disabled = (
+                await self._credential_provider.is_authentication_disabled()
+            )
+            if not is_auth_disabled:
+                # No auth header. Auth is required. Request is not authorized.
+                raise PermissionError()
+
+            # In the scenario where Auth is disabled, we still want to have the
+            # IsAuthenticated flag set in the ClaimsIdentity. To do this requires
+            # adding in an empty claim.
+            # Since ChannelServiceHandler calls are always a skill callback call, we set the skill claim too.
+            return SkillValidation.create_anonymous_skill_claim()
+
+        # Validate the header and extract claims.
+        return await JwtTokenValidation.validate_auth_header(
+            auth_header,
+            self._credential_provider,
+            self._channel_provider,
+            "unknown",
+            auth_configuration=self._auth_config,
+        )
diff --git a/libraries/botbuilder-core/botbuilder/core/component_registration.py b/libraries/botbuilder-core/botbuilder/core/component_registration.py
new file mode 100644
index 000000000..03023abbf
--- /dev/null
+++ b/libraries/botbuilder-core/botbuilder/core/component_registration.py
@@ -0,0 +1,17 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from typing import Dict, Iterable, Type
+
+
+class ComponentRegistration:
+    @staticmethod
+    def get_components() -> Iterable["ComponentRegistration"]:
+        return _components.values()
+
+    @staticmethod
+    def add(component_registration: "ComponentRegistration"):
+        _components[component_registration.__class__] = component_registration
+
+
+_components: Dict[Type, ComponentRegistration] = {}
diff --git a/libraries/botbuilder-core/botbuilder/core/conversation_reference_extension.py b/libraries/botbuilder-core/botbuilder/core/conversation_reference_extension.py
index 6dd4172e9..a04ce237d 100644
--- a/libraries/botbuilder-core/botbuilder/core/conversation_reference_extension.py
+++ b/libraries/botbuilder-core/botbuilder/core/conversation_reference_extension.py
@@ -1,13 +1,18 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License.
 import uuid
-from botbuilder.schema import Activity, ActivityTypes, ConversationReference
+from botbuilder.schema import (
+    Activity,
+    ActivityEventNames,
+    ActivityTypes,
+    ConversationReference,
+)
 
 
 def get_continuation_activity(reference: ConversationReference) -> Activity:
     return Activity(
         type=ActivityTypes.event,
-        name="ContinueConversation",
+        name=ActivityEventNames.continue_conversation,
         id=str(uuid.uuid1()),
         channel_id=reference.channel_id,
         service_url=reference.service_url,
diff --git a/libraries/botbuilder-core/botbuilder/core/conversation_state.py b/libraries/botbuilder-core/botbuilder/core/conversation_state.py
index 94bc8fedb..174ca0883 100644
--- a/libraries/botbuilder-core/botbuilder/core/conversation_state.py
+++ b/libraries/botbuilder-core/botbuilder/core/conversation_state.py
@@ -7,16 +7,43 @@
 
 
 class ConversationState(BotState):
-    """Conversation State
-    Reads and writes conversation state for your bot to storage.
+    """
+    Defines a state management object for conversation state.
+
+    .. remarks::
+        Conversation state is available in any turn in a specific conversation, regardless of the user, such as
+        in a group conversation.
     """
 
     no_key_error_message = "ConversationState: channelId and/or conversation missing from context.activity."
 
     def __init__(self, storage: Storage):
-        super(ConversationState, self).__init__(storage, "ConversationState")
+        """
+        Creates a :class:`ConversationState` instance.
+
+        Creates a new instance of the :class:`ConversationState` class.
+        :param storage: The storage containing the conversation state.
+        :type storage: :class:`Storage`
+        """
+        super(ConversationState, self).__init__(storage, "Internal.ConversationState")
 
     def get_storage_key(self, turn_context: TurnContext) -> object:
+        """
+        Gets the key to use when reading and writing state to and from storage.
+
+        :param turn_context: The context object for this turn.
+        :type turn_context: :class:`TurnContext`
+
+        :raise: :class:`TypeError` if the :meth:`TurnContext.activity` for the current turn is missing
+        :class:`botbuilder.schema.Activity` channelId or conversation information or the conversation's
+        account id is missing.
+
+        :return: The storage key.
+        :rtype: str
+
+        .. remarks::
+            Conversation state includes the channel ID and conversation ID as part of its storage key.
+        """
         channel_id = turn_context.activity.channel_id or self.__raise_type_error(
             "invalid activity-missing channel_id"
         )
@@ -31,4 +58,7 @@ def get_storage_key(self, turn_context: TurnContext) -> object:
         return storage_key
 
     def __raise_type_error(self, err: str = "NoneType found while expecting value"):
+        """ Raise type error exception
+        :raises: :class:`TypeError`
+        """
         raise TypeError(err)
diff --git a/libraries/botbuilder-core/botbuilder/core/healthcheck.py b/libraries/botbuilder-core/botbuilder/core/healthcheck.py
new file mode 100644
index 000000000..c9f5afb49
--- /dev/null
+++ b/libraries/botbuilder-core/botbuilder/core/healthcheck.py
@@ -0,0 +1,31 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.schema import HealthCheckResponse, HealthResults
+from botbuilder.core.bot_framework_adapter import USER_AGENT
+from botframework.connector import ConnectorClient
+
+
+class HealthCheck:
+    @staticmethod
+    def create_healthcheck_response(
+        connector_client: ConnectorClient,
+    ) -> HealthCheckResponse:
+        # A derived class may override this, however, the default is that the bot is healthy given
+        # we have got to here.
+        health_results = HealthResults(success=True)
+
+        if connector_client:
+            health_results.authorization = "{} {}".format(
+                "Bearer", connector_client.config.credentials.get_access_token()
+            )
+            health_results.user_agent = USER_AGENT
+
+        success_message = "Health check succeeded."
+        health_results.messages = (
+            [success_message]
+            if health_results.authorization
+            else [success_message, "Callbacks are not authorized."]
+        )
+
+        return HealthCheckResponse(health_results=health_results)
diff --git a/libraries/botbuilder-core/botbuilder/core/inspection/trace_activity.py b/libraries/botbuilder-core/botbuilder/core/inspection/trace_activity.py
index 409c4b503..307ef64cd 100644
--- a/libraries/botbuilder-core/botbuilder/core/inspection/trace_activity.py
+++ b/libraries/botbuilder-core/botbuilder/core/inspection/trace_activity.py
@@ -42,7 +42,7 @@ def from_state(bot_state: Union[BotState, Dict]) -> Activity:
 
 
 def from_conversation_reference(
-    conversation_reference: ConversationReference
+    conversation_reference: ConversationReference,
 ) -> Activity:
     return Activity(
         type=ActivityTypes.trace,
diff --git a/libraries/botbuilder-core/botbuilder/core/integration/__init__.py b/libraries/botbuilder-core/botbuilder/core/integration/__init__.py
new file mode 100644
index 000000000..db24c43d3
--- /dev/null
+++ b/libraries/botbuilder-core/botbuilder/core/integration/__init__.py
@@ -0,0 +1,14 @@
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+# --------------------------------------------------------------------------
+
+from .aiohttp_channel_service import aiohttp_channel_service_routes
+from .aiohttp_channel_service_exception_middleware import aiohttp_error_middleware
+
+__all__ = [
+    "aiohttp_channel_service_routes",
+    "aiohttp_error_middleware",
+]
diff --git a/libraries/botbuilder-core/botbuilder/core/integration/aiohttp_channel_service.py b/libraries/botbuilder-core/botbuilder/core/integration/aiohttp_channel_service.py
new file mode 100644
index 000000000..c4e8b3b2f
--- /dev/null
+++ b/libraries/botbuilder-core/botbuilder/core/integration/aiohttp_channel_service.py
@@ -0,0 +1,185 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+import json
+from typing import List, Union, Type
+
+from aiohttp.web import RouteTableDef, Request, Response
+from msrest.serialization import Model
+
+from botbuilder.schema import (
+    Activity,
+    AttachmentData,
+    ConversationParameters,
+    Transcript,
+)
+
+from botbuilder.core import ChannelServiceHandler
+
+
+async def deserialize_from_body(
+    request: Request, target_model: Type[Model]
+) -> Activity:
+    if "application/json" in request.headers["Content-Type"]:
+        body = await request.json()
+    else:
+        return Response(status=415)
+
+    return target_model().deserialize(body)
+
+
+def get_serialized_response(model_or_list: Union[Model, List[Model]]) -> Response:
+    if isinstance(model_or_list, Model):
+        json_obj = model_or_list.serialize()
+    else:
+        json_obj = [model.serialize() for model in model_or_list]
+
+    return Response(body=json.dumps(json_obj), content_type="application/json")
+
+
+def aiohttp_channel_service_routes(
+    handler: ChannelServiceHandler, base_url: str = ""
+) -> RouteTableDef:
+    # pylint: disable=unused-variable
+    routes = RouteTableDef()
+
+    @routes.post(base_url + "/v3/conversations/{conversation_id}/activities")
+    async def send_to_conversation(request: Request):
+        activity = await deserialize_from_body(request, Activity)
+        result = await handler.handle_send_to_conversation(
+            request.headers.get("Authorization"),
+            request.match_info["conversation_id"],
+            activity,
+        )
+
+        return get_serialized_response(result)
+
+    @routes.post(
+        base_url + "/v3/conversations/{conversation_id}/activities/{activity_id}"
+    )
+    async def reply_to_activity(request: Request):
+        activity = await deserialize_from_body(request, Activity)
+        result = await handler.handle_reply_to_activity(
+            request.headers.get("Authorization"),
+            request.match_info["conversation_id"],
+            request.match_info["activity_id"],
+            activity,
+        )
+
+        return get_serialized_response(result)
+
+    @routes.put(
+        base_url + "/v3/conversations/{conversation_id}/activities/{activity_id}"
+    )
+    async def update_activity(request: Request):
+        activity = await deserialize_from_body(request, Activity)
+        result = await handler.handle_update_activity(
+            request.headers.get("Authorization"),
+            request.match_info["conversation_id"],
+            request.match_info["activity_id"],
+            activity,
+        )
+
+        return get_serialized_response(result)
+
+    @routes.delete(
+        base_url + "/v3/conversations/{conversation_id}/activities/{activity_id}"
+    )
+    async def delete_activity(request: Request):
+        await handler.handle_delete_activity(
+            request.headers.get("Authorization"),
+            request.match_info["conversation_id"],
+            request.match_info["activity_id"],
+        )
+
+        return Response()
+
+    @routes.get(
+        base_url
+        + "/v3/conversations/{conversation_id}/activities/{activity_id}/members"
+    )
+    async def get_activity_members(request: Request):
+        result = await handler.handle_get_activity_members(
+            request.headers.get("Authorization"),
+            request.match_info["conversation_id"],
+            request.match_info["activity_id"],
+        )
+
+        return get_serialized_response(result)
+
+    @routes.post(base_url + "/")
+    async def create_conversation(request: Request):
+        conversation_parameters = deserialize_from_body(request, ConversationParameters)
+        result = await handler.handle_create_conversation(
+            request.headers.get("Authorization"), conversation_parameters
+        )
+
+        return get_serialized_response(result)
+
+    @routes.get(base_url + "/")
+    async def get_conversation(request: Request):
+        # TODO: continuation token?
+        result = await handler.handle_get_conversations(
+            request.headers.get("Authorization")
+        )
+
+        return get_serialized_response(result)
+
+    @routes.get(base_url + "/v3/conversations/{conversation_id}/members")
+    async def get_conversation_members(request: Request):
+        result = await handler.handle_get_conversation_members(
+            request.headers.get("Authorization"), request.match_info["conversation_id"],
+        )
+
+        return get_serialized_response(result)
+
+    @routes.get(base_url + "/v3/conversations/{conversation_id}/members/{member_id}")
+    async def get_conversation_member(request: Request):
+        result = await handler.handle_get_conversation_member(
+            request.headers.get("Authorization"),
+            request.match_info["conversation_id", "member_id"],
+        )
+
+        return get_serialized_response(result)
+
+    @routes.get(base_url + "/v3/conversations/{conversation_id}/pagedmembers")
+    async def get_conversation_paged_members(request: Request):
+        # TODO: continuation token? page size?
+        result = await handler.handle_get_conversation_paged_members(
+            request.headers.get("Authorization"), request.match_info["conversation_id"],
+        )
+
+        return get_serialized_response(result)
+
+    @routes.delete(base_url + "/v3/conversations/{conversation_id}/members/{member_id}")
+    async def delete_conversation_member(request: Request):
+        result = await handler.handle_delete_conversation_member(
+            request.headers.get("Authorization"),
+            request.match_info["conversation_id"],
+            request.match_info["member_id"],
+        )
+
+        return get_serialized_response(result)
+
+    @routes.post(base_url + "/v3/conversations/{conversation_id}/activities/history")
+    async def send_conversation_history(request: Request):
+        transcript = deserialize_from_body(request, Transcript)
+        result = await handler.handle_send_conversation_history(
+            request.headers.get("Authorization"),
+            request.match_info["conversation_id"],
+            transcript,
+        )
+
+        return get_serialized_response(result)
+
+    @routes.post(base_url + "/v3/conversations/{conversation_id}/attachments")
+    async def upload_attachment(request: Request):
+        attachment_data = deserialize_from_body(request, AttachmentData)
+        result = await handler.handle_upload_attachment(
+            request.headers.get("Authorization"),
+            request.match_info["conversation_id"],
+            attachment_data,
+        )
+
+        return get_serialized_response(result)
+
+    return routes
diff --git a/libraries/botbuilder-core/botbuilder/core/integration/aiohttp_channel_service_exception_middleware.py b/libraries/botbuilder-core/botbuilder/core/integration/aiohttp_channel_service_exception_middleware.py
new file mode 100644
index 000000000..7c5091121
--- /dev/null
+++ b/libraries/botbuilder-core/botbuilder/core/integration/aiohttp_channel_service_exception_middleware.py
@@ -0,0 +1,29 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from aiohttp.web import (
+    middleware,
+    HTTPNotImplemented,
+    HTTPUnauthorized,
+    HTTPNotFound,
+    HTTPInternalServerError,
+)
+
+from botbuilder.core import BotActionNotImplementedError
+
+
+@middleware
+async def aiohttp_error_middleware(request, handler):
+    try:
+        response = await handler(request)
+        return response
+    except BotActionNotImplementedError:
+        raise HTTPNotImplemented()
+    except NotImplementedError:
+        raise HTTPNotImplemented()
+    except PermissionError:
+        raise HTTPUnauthorized()
+    except KeyError:
+        raise HTTPNotFound()
+    except Exception:
+        raise HTTPInternalServerError()
diff --git a/libraries/botbuilder-core/botbuilder/core/invoke_response.py b/libraries/botbuilder-core/botbuilder/core/invoke_response.py
index 408662707..fa0b74577 100644
--- a/libraries/botbuilder-core/botbuilder/core/invoke_response.py
+++ b/libraries/botbuilder-core/botbuilder/core/invoke_response.py
@@ -1,20 +1,34 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-
-class InvokeResponse:
-    """
-    Tuple class containing an HTTP Status Code and a JSON Serializable
-    object. The HTTP Status code is, in the invoke activity scenario, what will
-    be set in the resulting POST. The Body of the resulting POST will be
-    the JSON Serialized content from the Body property.
-    """
-
-    def __init__(self, status: int = None, body: object = None):
-        """
-        Gets or sets the HTTP status and/or body code for the response
-        :param status: The HTTP status code.
-        :param body: The body content for the response.
-        """
-        self.status = status
-        self.body = body
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+
+class InvokeResponse:
+    """
+    Tuple class containing an HTTP Status Code and a JSON serializable
+    object. The HTTP Status code is, in the invoke activity scenario, what will
+    be set in the resulting POST. The Body of the resulting POST will be
+    JSON serialized content.
+
+    The body content is defined by the producer.  The caller must know what
+    the content is and deserialize as needed.
+    """
+
+    def __init__(self, status: int = None, body: object = None):
+        """
+        Gets or sets the HTTP status and/or body code for the response
+        :param status: The HTTP status code.
+        :param body: The JSON serializable body content for the response.  This object
+        must be serializable by the core Python json routines.  The caller is responsible
+        for serializing more complex/nested objects into native classes (lists and
+        dictionaries of strings are acceptable).
+        """
+        self.status = status
+        self.body = body
+
+    def is_successful_status_code(self) -> bool:
+        """
+        Gets a value indicating whether the invoke response was successful.
+        :return: A value that indicates if the HTTP response was successful. true if status is in
+        the Successful range (200-299); otherwise false.
+        """
+        return 200 <= self.status <= 299
diff --git a/libraries/botbuilder-core/botbuilder/core/memory_storage.py b/libraries/botbuilder-core/botbuilder/core/memory_storage.py
index 73ff77bc4..c61b053c7 100644
--- a/libraries/botbuilder-core/botbuilder/core/memory_storage.py
+++ b/libraries/botbuilder-core/botbuilder/core/memory_storage.py
@@ -22,6 +22,8 @@ async def delete(self, keys: List[str]):
 
     async def read(self, keys: List[str]):
         data = {}
+        if not keys:
+            return data
         try:
             for key in keys:
                 if key in self.memory:
@@ -32,37 +34,54 @@ async def read(self, keys: List[str]):
         return data
 
     async def write(self, changes: Dict[str, StoreItem]):
+        if changes is None:
+            raise Exception("Changes are required when writing")
+        if not changes:
+            return
         try:
             # iterate over the changes
             for (key, change) in changes.items():
-                new_value = change
+                new_value = deepcopy(change)
                 old_state_etag = None
 
                 # Check if the a matching key already exists in self.memory
                 # If it exists then we want to cache its original value from memory
                 if key in self.memory:
                     old_state = self.memory[key]
-                    if not isinstance(old_state, StoreItem):
-                        if "eTag" in old_state:
-                            old_state_etag = old_state["eTag"]
-                    elif old_state.e_tag:
+                    if isinstance(old_state, dict):
+                        old_state_etag = old_state.get("e_tag", None)
+                    elif hasattr(old_state, "e_tag"):
                         old_state_etag = old_state.e_tag
 
                 new_state = new_value
 
                 # Set ETag if applicable
-                if hasattr(new_value, "e_tag"):
-                    if (
-                        old_state_etag is not None
-                        and new_value.e_tag != "*"
-                        and new_value.e_tag < old_state_etag
-                    ):
-                        raise KeyError(
-                            "Etag conflict.\nOriginal: %s\r\nCurrent: %s"
-                            % (new_value.e_tag, old_state_etag)
-                        )
-                    new_state.e_tag = str(self._e_tag)
-                    self._e_tag += 1
+                new_value_etag = None
+                if isinstance(new_value, dict):
+                    new_value_etag = new_value.get("e_tag", None)
+                elif hasattr(new_value, "e_tag"):
+                    new_value_etag = new_value.e_tag
+                if new_value_etag == "":
+                    raise Exception("memory_storage.write(): etag missing")
+                if (
+                    old_state_etag is not None
+                    and new_value_etag is not None
+                    and new_value_etag != "*"
+                    and new_value_etag < old_state_etag
+                ):
+                    raise KeyError(
+                        "Etag conflict.\nOriginal: %s\r\nCurrent: %s"
+                        % (new_value_etag, old_state_etag)
+                    )
+
+                # If the original object didn't have an e_tag, don't set one (C# behavior)
+                if old_state_etag:
+                    if isinstance(new_state, dict):
+                        new_state["e_tag"] = str(self._e_tag)
+                    else:
+                        new_state.e_tag = str(self._e_tag)
+
+                self._e_tag += 1
                 self.memory[key] = deepcopy(new_state)
 
         except Exception as error:
diff --git a/libraries/botbuilder-core/botbuilder/core/memory_transcript_store.py b/libraries/botbuilder-core/botbuilder/core/memory_transcript_store.py
index e8953e0ae..325cf32f6 100644
--- a/libraries/botbuilder-core/botbuilder/core/memory_transcript_store.py
+++ b/libraries/botbuilder-core/botbuilder/core/memory_transcript_store.py
@@ -6,6 +6,7 @@
 from botbuilder.schema import Activity
 from .transcript_logger import PagedResult, TranscriptInfo, TranscriptStore
 
+
 # pylint: disable=line-too-long
 class MemoryTranscriptStore(TranscriptStore):
     """This provider is most useful for simulating production storage when running locally against the
@@ -59,7 +60,9 @@ async def get_transcript_activities(
                         [
                             x
                             for x in sorted(
-                                transcript, key=lambda x: x.timestamp, reverse=False
+                                transcript,
+                                key=lambda x: x.timestamp or str(datetime.datetime.min),
+                                reverse=False,
                             )
                             if x.timestamp >= start_date
                         ]
@@ -72,9 +75,11 @@ async def get_transcript_activities(
                     paged_result.items = [
                         x
                         for x in sorted(
-                            transcript, key=lambda x: x.timestamp, reverse=False
+                            transcript,
+                            key=lambda x: x.timestamp or datetime.datetime.min,
+                            reverse=False,
                         )
-                        if x.timestamp >= start_date
+                        if (x.timestamp or datetime.datetime.min) >= start_date
                     ][:20]
                     if paged_result.items.count == 20:
                         paged_result.continuation_token = paged_result.items[-1].id
diff --git a/libraries/botbuilder-core/botbuilder/core/null_telemetry_client.py b/libraries/botbuilder-core/botbuilder/core/null_telemetry_client.py
index dc9954385..6cb3e5789 100644
--- a/libraries/botbuilder-core/botbuilder/core/null_telemetry_client.py
+++ b/libraries/botbuilder-core/botbuilder/core/null_telemetry_client.py
@@ -118,7 +118,7 @@ def track_request(
         :param url: The actual URL for this request (to show in individual request instances).
         :param success: True if the request ended in success, False otherwise.
         :param start_time: the start time of the request. The value should look the same as the one returned \
-        by :func:`datetime.isoformat()` (defaults to: None)
+        by :func:`datetime.isoformat`. (defaults to: None)
         :param duration: the number of milliseconds that this request lasted. (defaults to: None)
         :param response_code: the response code that this request returned. (defaults to: None)
         :param http_method: the HTTP method that triggered this request. (defaults to: None)
diff --git a/libraries/botbuilder-core/botbuilder/core/oauth/__init__.py b/libraries/botbuilder-core/botbuilder/core/oauth/__init__.py
new file mode 100644
index 000000000..4fd090b48
--- /dev/null
+++ b/libraries/botbuilder-core/botbuilder/core/oauth/__init__.py
@@ -0,0 +1,12 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from .extended_user_token_provider import ExtendedUserTokenProvider
+from .user_token_provider import UserTokenProvider
+from .connector_client_builder import ConnectorClientBuilder
+
+__all__ = [
+    "ConnectorClientBuilder",
+    "ExtendedUserTokenProvider",
+    "UserTokenProvider",
+]
diff --git a/libraries/botbuilder-core/botbuilder/core/oauth/connector_client_builder.py b/libraries/botbuilder-core/botbuilder/core/oauth/connector_client_builder.py
new file mode 100644
index 000000000..e5256040f
--- /dev/null
+++ b/libraries/botbuilder-core/botbuilder/core/oauth/connector_client_builder.py
@@ -0,0 +1,26 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+from abc import ABC, abstractmethod
+
+from botframework.connector import ConnectorClient
+from botframework.connector.auth import ClaimsIdentity
+
+
+class ConnectorClientBuilder(ABC):
+    """
+    Abstraction to build connector clients.
+    """
+
+    @abstractmethod
+    async def create_connector_client(
+        self, service_url: str, identity: ClaimsIdentity = None, audience: str = None
+    ) -> ConnectorClient:
+        """
+        Creates the connector client asynchronous.
+
+        :param service_url: The service URL.
+        :param identity: The claims claimsIdentity.
+        :param audience: The target audience for the connector.
+        :return: ConnectorClient instance
+        """
+        raise NotImplementedError()
diff --git a/libraries/botbuilder-core/botbuilder/core/oauth/extended_user_token_provider.py b/libraries/botbuilder-core/botbuilder/core/oauth/extended_user_token_provider.py
new file mode 100644
index 000000000..ad07c3989
--- /dev/null
+++ b/libraries/botbuilder-core/botbuilder/core/oauth/extended_user_token_provider.py
@@ -0,0 +1,190 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from abc import ABC
+from typing import Dict, List
+
+from botframework.connector.token_api.models import (
+    SignInUrlResponse,
+    TokenExchangeRequest,
+    TokenResponse,
+)
+from botframework.connector.auth import AppCredentials
+
+from botbuilder.core.turn_context import TurnContext
+from .user_token_provider import UserTokenProvider
+
+
+class ExtendedUserTokenProvider(UserTokenProvider, ABC):
+    # pylint: disable=unused-argument
+
+    async def get_sign_in_resource(
+        self, turn_context: TurnContext, connection_name: str
+    ) -> SignInUrlResponse:
+        """
+        Get the raw signin link to be sent to the user for signin for a connection name.
+
+        :param turn_context: Context for the current turn of conversation with the user.
+        :param connection_name: Name of the auth connection to use.
+
+
+        :return: A task that represents the work queued to execute.
+        .. remarks:: If the task completes successfully, the result contains the raw signin link.
+        """
+        return
+
+    async def get_sign_in_resource_from_user(
+        self,
+        turn_context: TurnContext,
+        connection_name: str,
+        user_id: str,
+        final_redirect: str = None,
+    ) -> SignInUrlResponse:
+        """
+        Get the raw signin link to be sent to the user for signin for a connection name.
+
+        :param turn_context: Context for the current turn of conversation with the user.
+        :param connection_name: Name of the auth connection to use.
+        :param user_id: The user id that will be associated with the token.
+        :param final_redirect: The final URL that the OAuth flow will redirect to.
+
+
+        :return: A task that represents the work queued to execute.
+        .. remarks:: If the task completes successfully, the result contains the raw signin link.
+        """
+        return
+
+    async def get_sign_in_resource_from_user_and_credentials(
+        self,
+        turn_context: TurnContext,
+        oauth_app_credentials: AppCredentials,
+        connection_name: str,
+        user_id: str,
+        final_redirect: str = None,
+    ) -> SignInUrlResponse:
+        """
+        Get the raw signin link to be sent to the user for signin for a connection name.
+
+        :param turn_context: Context for the current turn of conversation with the user.
+        :param oauth_app_credentials: Credentials for OAuth.
+        :param connection_name: Name of the auth connection to use.
+        :param user_id: The user id that will be associated with the token.
+        :param final_redirect: The final URL that the OAuth flow will redirect to.
+
+
+        :return: A task that represents the work queued to execute.
+        .. remarks:: If the task completes successfully, the result contains the raw signin link.
+        """
+        return
+
+    async def exchange_token(
+        self,
+        turn_context: TurnContext,
+        connection_name: str,
+        user_id: str,
+        exchange_request: TokenExchangeRequest,
+    ) -> TokenResponse:
+        """
+        Performs a token exchange operation such as for single sign-on.
+
+        :param turn_context: Context for the current turn of conversation with the user.
+        :param connection_name: Name of the auth connection to use.
+        :param user_id: The user id associated with the token..
+        :param exchange_request: The exchange request details, either a token to exchange or a uri to exchange.
+
+
+        :return: If the task completes, the exchanged token is returned.
+        """
+        return
+
+    async def exchange_token_from_credentials(
+        self,
+        turn_context: TurnContext,
+        oauth_app_credentials: AppCredentials,
+        connection_name: str,
+        user_id: str,
+        exchange_request: TokenExchangeRequest,
+    ) -> TokenResponse:
+        """
+        Performs a token exchange operation such as for single sign-on.
+
+        :param turn_context: Context for the current turn of conversation with the user.
+        :param oauth_app_credentials: AppCredentials for OAuth.
+        :param connection_name: Name of the auth connection to use.
+        :param user_id: The user id associated with the token..
+        :param exchange_request: The exchange request details, either a token to exchange or a uri to exchange.
+
+
+        :return: If the task completes, the exchanged token is returned.
+        """
+        return
+
+    async def get_user_token(
+        self,
+        context: TurnContext,
+        connection_name: str,
+        magic_code: str = None,
+        oauth_app_credentials: AppCredentials = None,
+    ) -> TokenResponse:
+        """
+        Retrieves the OAuth token for a user that is in a sign-in flow.
+        :param context:
+        :param connection_name:
+        :param magic_code:
+        :param oauth_app_credentials:
+        :return:
+        """
+        raise NotImplementedError()
+
+    async def sign_out_user(
+        self,
+        context: TurnContext,
+        connection_name: str = None,
+        user_id: str = None,
+        oauth_app_credentials: AppCredentials = None,
+    ):
+        """
+        Signs the user out with the token server.
+        :param context:
+        :param connection_name:
+        :param user_id:
+        :param oauth_app_credentials:
+        :return:
+        """
+        raise NotImplementedError()
+
+    async def get_oauth_sign_in_link(
+        self,
+        context: TurnContext,
+        connection_name: str,
+        final_redirect: str = None,
+        oauth_app_credentials: AppCredentials = None,
+    ) -> str:
+        """
+        Get the raw signin link to be sent to the user for signin for a connection name.
+        :param context:
+        :param connection_name:
+        :param final_redirect:
+        :param oauth_app_credentials:
+        :return:
+        """
+        raise NotImplementedError()
+
+    async def get_aad_tokens(
+        self,
+        context: TurnContext,
+        connection_name: str,
+        resource_urls: List[str],
+        user_id: str = None,
+        oauth_app_credentials: AppCredentials = None,
+    ) -> Dict[str, TokenResponse]:
+        """
+        Retrieves Azure Active Directory tokens for particular resources on a configured connection.
+        :param context:
+        :param connection_name:
+        :param resource_urls:
+        :param user_id:
+        :param oauth_app_credentials:
+        :return:
+        """
+        raise NotImplementedError()
diff --git a/libraries/botbuilder-core/botbuilder/core/oauth/user_token_provider.py b/libraries/botbuilder-core/botbuilder/core/oauth/user_token_provider.py
new file mode 100644
index 000000000..04e92efc2
--- /dev/null
+++ b/libraries/botbuilder-core/botbuilder/core/oauth/user_token_provider.py
@@ -0,0 +1,112 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from abc import ABC, abstractmethod
+from typing import Dict, List
+
+from botbuilder.core.turn_context import TurnContext
+from botbuilder.schema import TokenResponse
+from botframework.connector.auth import AppCredentials
+
+
+class UserTokenProvider(ABC):
+    @abstractmethod
+    async def get_user_token(
+        self,
+        context: TurnContext,
+        connection_name: str,
+        magic_code: str = None,
+        oauth_app_credentials: AppCredentials = None,
+    ) -> TokenResponse:
+        """
+        Retrieves the OAuth token for a user that is in a sign-in flow.
+        :param context: Context for the current turn of conversation with the user.
+        :param connection_name: Name of the auth connection to use.
+        :param magic_code: (Optional) Optional user entered code to validate.
+        :param oauth_app_credentials: (Optional) AppCredentials for OAuth.  If None is supplied, the
+        Bots credentials are used.
+        :return:
+        """
+        raise NotImplementedError()
+
+    @abstractmethod
+    async def sign_out_user(
+        self,
+        context: TurnContext,
+        connection_name: str = None,
+        user_id: str = None,
+        oauth_app_credentials: AppCredentials = None,
+    ):
+        """
+        Signs the user out with the token server.
+        :param context: Context for the current turn of conversation with the user.
+        :param connection_name: Name of the auth connection to use.
+        :param user_id: User id of user to sign out.
+        :param oauth_app_credentials: (Optional) AppCredentials for OAuth.  If None is supplied, the
+        Bots credentials are used.
+        :return:
+        """
+        raise NotImplementedError()
+
+    @abstractmethod
+    async def get_oauth_sign_in_link(
+        self,
+        context: TurnContext,
+        connection_name: str,
+        final_redirect: str = None,
+        oauth_app_credentials: AppCredentials = None,
+    ) -> str:
+        """
+        Get the raw signin link to be sent to the user for signin for a connection name.
+        :param context: Context for the current turn of conversation with the user.
+        :param connection_name: Name of the auth connection to use.
+        :param final_redirect: The final URL that the OAuth flow will redirect to.
+        :param oauth_app_credentials: (Optional) AppCredentials for OAuth.  If None is supplied, the
+        Bots credentials are used.
+        :return:
+        """
+        raise NotImplementedError()
+
+    @abstractmethod
+    async def get_token_status(
+        self,
+        context: TurnContext,
+        connection_name: str = None,
+        user_id: str = None,
+        include_filter: str = None,
+        oauth_app_credentials: AppCredentials = None,
+    ) -> Dict[str, TokenResponse]:
+        """
+        Retrieves Azure Active Directory tokens for particular resources on a configured connection.
+        :param context: Context for the current turn of conversation with the user.
+        :param connection_name: Name of the auth connection to use.
+        :param user_id: The user Id for which token status is retrieved.
+        :param include_filter: Optional comma separated list of connection's to include. Blank will return token status
+        for all configured connections.
+        :param oauth_app_credentials: (Optional) AppCredentials for OAuth.  If None is supplied, the
+        Bots credentials are used.
+        :return:
+        """
+        raise NotImplementedError()
+
+    @abstractmethod
+    async def get_aad_tokens(
+        self,
+        context: TurnContext,
+        connection_name: str,
+        resource_urls: List[str],
+        user_id: str = None,
+        oauth_app_credentials: AppCredentials = None,
+    ) -> Dict[str, TokenResponse]:
+        """
+        Retrieves Azure Active Directory tokens for particular resources on a configured connection.
+        :param context: Context for the current turn of conversation with the user.
+        :param connection_name: Name of the auth connection to use.
+        :param resource_urls: The list of resource URLs to retrieve tokens for.
+        :param user_id: The user Id for which tokens are retrieved. If passing in None the userId is taken
+        from the Activity in the TurnContext.
+        :param oauth_app_credentials: (Optional) AppCredentials for OAuth.  If None is supplied, the
+        Bots credentials are used.
+        :return:
+        """
+        raise NotImplementedError()
diff --git a/libraries/botbuilder-core/botbuilder/core/private_conversation_state.py b/libraries/botbuilder-core/botbuilder/core/private_conversation_state.py
index 6b13bc5f5..137d57b0a 100644
--- a/libraries/botbuilder-core/botbuilder/core/private_conversation_state.py
+++ b/libraries/botbuilder-core/botbuilder/core/private_conversation_state.py
@@ -1,3 +1,6 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
 from .bot_state import BotState
 from .turn_context import TurnContext
 from .storage import Storage
diff --git a/libraries/botbuilder-core/botbuilder/core/re_escape.py b/libraries/botbuilder-core/botbuilder/core/re_escape.py
new file mode 100644
index 000000000..b50472bb6
--- /dev/null
+++ b/libraries/botbuilder-core/botbuilder/core/re_escape.py
@@ -0,0 +1,25 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+# SPECIAL_CHARS
+# closing ')', '}' and ']'
+# '-' (a range in character set)
+# '&', '~', (extended character set operations)
+# '#' (comment) and WHITESPACE (ignored) in verbose mode
+SPECIAL_CHARS_MAP = {i: "\\" + chr(i) for i in b"()[]{}?*+-|^$\\.&~# \t\n\r\v\f"}
+
+
+def escape(pattern):
+    """
+    Escape special characters in a string.
+
+    This is a copy of the re.escape function in Python 3.8.  This was done
+    because the 3.6.x version didn't escape in the same way and handling
+    bot names with regex characters in it would fail in TurnContext.remove_mention_text
+    without escaping the text.
+    """
+    if isinstance(pattern, str):
+        return pattern.translate(SPECIAL_CHARS_MAP)
+
+    pattern = str(pattern, "latin1")
+    return pattern.translate(SPECIAL_CHARS_MAP).encode("latin1")
diff --git a/libraries/botbuilder-core/botbuilder/core/register_class_middleware.py b/libraries/botbuilder-core/botbuilder/core/register_class_middleware.py
new file mode 100644
index 000000000..38be1f46b
--- /dev/null
+++ b/libraries/botbuilder-core/botbuilder/core/register_class_middleware.py
@@ -0,0 +1,32 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+from typing import Callable, Awaitable
+
+from botbuilder.core import Middleware, TurnContext
+
+
+class RegisterClassMiddleware(Middleware):
+    """
+    Middleware for adding an object to or registering a service with the current turn context.
+    """
+
+    def __init__(self, service, key: str = None):
+        self.service = service
+        self._key = key
+
+    async def on_turn(
+        self, context: TurnContext, logic: Callable[[TurnContext], Awaitable]
+    ):
+        # C# has TurnStateCollection with has overrides for adding items
+        # to TurnState.  Python does not.  In C#'s case, there is an 'Add'
+        # to handle adding object, and that uses the fully qualified class name.
+        key = self._key or self.fullname(self.service)
+        context.turn_state[key] = self.service
+        await logic()
+
+    @staticmethod
+    def fullname(obj):
+        module = obj.__class__.__module__
+        if module is None or module == str.__class__.__module__:
+            return obj.__class__.__name__  # Avoid reporting __builtin__
+        return module + "." + obj.__class__.__name__
diff --git a/libraries/botbuilder-core/botbuilder/core/serializer_helper.py b/libraries/botbuilder-core/botbuilder/core/serializer_helper.py
new file mode 100644
index 000000000..766cd6291
--- /dev/null
+++ b/libraries/botbuilder-core/botbuilder/core/serializer_helper.py
@@ -0,0 +1,37 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from inspect import getmembers
+from typing import Type
+from enum import Enum
+
+from msrest.serialization import Model, Deserializer, Serializer
+
+import botbuilder.schema as schema
+import botbuilder.schema.teams as teams_schema
+
+DEPENDICIES = [
+    schema_cls
+    for key, schema_cls in getmembers(schema)
+    if isinstance(schema_cls, type) and issubclass(schema_cls, (Model, Enum))
+]
+DEPENDICIES += [
+    schema_cls
+    for key, schema_cls in getmembers(teams_schema)
+    if isinstance(schema_cls, type) and issubclass(schema_cls, (Model, Enum))
+]
+DEPENDICIES_DICT = {dependency.__name__: dependency for dependency in DEPENDICIES}
+
+
+def deserializer_helper(msrest_cls: Type[Model], dict_to_deserialize: dict) -> Model:
+    deserializer = Deserializer(DEPENDICIES_DICT)
+    return deserializer(msrest_cls.__name__, dict_to_deserialize)
+
+
+def serializer_helper(object_to_serialize: Model) -> dict:
+    if object_to_serialize is None:
+        return None
+
+    serializer = Serializer(DEPENDICIES_DICT)
+    # pylint: disable=protected-access
+    return serializer._serialize(object_to_serialize)
diff --git a/libraries/botbuilder-core/botbuilder/core/show_typing_middleware.py b/libraries/botbuilder-core/botbuilder/core/show_typing_middleware.py
index 42846b086..80b353b4f 100644
--- a/libraries/botbuilder-core/botbuilder/core/show_typing_middleware.py
+++ b/libraries/botbuilder-core/botbuilder/core/show_typing_middleware.py
@@ -1,92 +1,107 @@
-import time
-from functools import wraps
-from typing import Awaitable, Callable
-
-from botbuilder.schema import Activity, ActivityTypes
-
-from .middleware_set import Middleware
-from .turn_context import TurnContext
-
-
-def delay(span=0.0):
-    def wrap(func):
-        @wraps(func)
-        async def delayed():
-            time.sleep(span)
-            await func()
-
-        return delayed
-
-    return wrap
-
-
-class Timer:
-    clear_timer = False
-
-    async def set_timeout(self, func, time):
-        is_invocation_cancelled = False
-
-        @delay(time)
-        async def some_fn():  # pylint: disable=function-redefined
-            if not self.clear_timer:
-                await func()
-
-        await some_fn()
-        return is_invocation_cancelled
-
-    def set_clear_timer(self):
-        self.clear_timer = True
-
-
-class ShowTypingMiddleware(Middleware):
-    def __init__(self, delay: float = 0.5, period: float = 2.0):
-        if delay < 0:
-            raise ValueError("Delay must be greater than or equal to zero")
-
-        if period <= 0:
-            raise ValueError("Repeat period must be greater than zero")
-
-        self._delay = delay
-        self._period = period
-
-    async def on_turn(
-        self, context: TurnContext, logic: Callable[[TurnContext], Awaitable]
-    ):
-        finished = False
-        timer = Timer()
-
-        async def start_interval(context: TurnContext, delay: int, period: int):
-            async def aux():
-                if not finished:
-                    typing_activity = Activity(
-                        type=ActivityTypes.typing,
-                        relates_to=context.activity.relates_to,
-                    )
-
-                    conversation_reference = TurnContext.get_conversation_reference(
-                        context.activity
-                    )
-
-                    typing_activity = TurnContext.apply_conversation_reference(
-                        typing_activity, conversation_reference
-                    )
-
-                    await context.adapter.send_activities(context, [typing_activity])
-
-                    start_interval(context, period, period)
-
-            await timer.set_timeout(aux, delay)
-
-        def stop_interval():
-            nonlocal finished
-            finished = True
-            timer.set_clear_timer()
-
-        if context.activity.type == ActivityTypes.message:
-            finished = False
-            await start_interval(context, self._delay, self._period)
-
-        result = await logic()
-        stop_interval()
-
-        return result
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+import asyncio
+from typing import Awaitable, Callable
+
+from botbuilder.schema import Activity, ActivityTypes
+from botframework.connector.auth import ClaimsIdentity, SkillValidation
+
+from .bot_adapter import BotAdapter
+from .middleware_set import Middleware
+from .turn_context import TurnContext
+
+
+class Timer:
+    clear_timer = False
+
+    def set_timeout(self, func, span):
+        async def some_fn():  # pylint: disable=function-redefined
+            await asyncio.sleep(span)
+            if not self.clear_timer:
+                await func()
+
+        asyncio.ensure_future(some_fn())
+
+    def set_clear_timer(self):
+        self.clear_timer = True
+
+
+class ShowTypingMiddleware(Middleware):
+    """
+    When added, this middleware will send typing activities back to the user when a Message activity
+    is received to let them know that the bot has received the message and is working on the response.
+    You can specify a delay before the first typing activity is sent and then a frequency, which
+    determines how often another typing activity is sent. Typing activities will continue to be sent
+    until your bot sends another message back to the user.
+    """
+
+    def __init__(self, delay: float = 0.5, period: float = 2.0):
+        """
+        Initializes the middleware.
+
+        :param delay: Delay in seconds for the first typing indicator to be sent.
+        :param period: Delay in seconds for subsequent typing indicators.
+        """
+
+        if delay < 0:
+            raise ValueError("Delay must be greater than or equal to zero")
+
+        if period <= 0:
+            raise ValueError("Repeat period must be greater than zero")
+
+        self._delay = delay
+        self._period = period
+
+    async def on_turn(
+        self, context: TurnContext, logic: Callable[[TurnContext], Awaitable]
+    ):
+        timer = Timer()
+
+        def start_interval(context: TurnContext, delay, period):
+            async def aux():
+                typing_activity = Activity(
+                    type=ActivityTypes.typing, relates_to=context.activity.relates_to,
+                )
+
+                conversation_reference = TurnContext.get_conversation_reference(
+                    context.activity
+                )
+
+                typing_activity = TurnContext.apply_conversation_reference(
+                    typing_activity, conversation_reference
+                )
+
+                asyncio.ensure_future(
+                    context.adapter.send_activities(context, [typing_activity])
+                )
+
+                # restart the timer, with the 'period' value for the delay
+                timer.set_timeout(aux, period)
+
+            # first time through we use the 'delay' value for the timer.
+            timer.set_timeout(aux, delay)
+
+        def stop_interval():
+            timer.set_clear_timer()
+
+        # Start a timer to periodically send the typing activity
+        # (bots running as skills should not send typing activity)
+        if (
+            context.activity.type == ActivityTypes.message
+            and not ShowTypingMiddleware._is_skill_bot(context)
+        ):
+            start_interval(context, self._delay, self._period)
+
+        # call the bot logic
+        result = await logic()
+
+        stop_interval()
+
+        return result
+
+    @staticmethod
+    def _is_skill_bot(context: TurnContext) -> bool:
+        claims_identity = context.turn_state.get(BotAdapter.BOT_IDENTITY_KEY)
+        return isinstance(
+            claims_identity, ClaimsIdentity
+        ) and SkillValidation.is_skill_claim(claims_identity.claims)
diff --git a/libraries/botbuilder-core/botbuilder/core/skills/__init__.py b/libraries/botbuilder-core/botbuilder/core/skills/__init__.py
new file mode 100644
index 000000000..ce949b12a
--- /dev/null
+++ b/libraries/botbuilder-core/botbuilder/core/skills/__init__.py
@@ -0,0 +1,22 @@
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+# --------------------------------------------------------------------------
+
+from .bot_framework_skill import BotFrameworkSkill
+from .bot_framework_client import BotFrameworkClient
+from .conversation_id_factory import ConversationIdFactoryBase
+from .skill_handler import SkillHandler
+from .skill_conversation_id_factory_options import SkillConversationIdFactoryOptions
+from .skill_conversation_reference import SkillConversationReference
+
+__all__ = [
+    "BotFrameworkSkill",
+    "BotFrameworkClient",
+    "ConversationIdFactoryBase",
+    "SkillConversationIdFactoryOptions",
+    "SkillConversationReference",
+    "SkillHandler",
+]
diff --git a/libraries/botbuilder-core/botbuilder/core/skills/bot_framework_client.py b/libraries/botbuilder-core/botbuilder/core/skills/bot_framework_client.py
new file mode 100644
index 000000000..5213aba70
--- /dev/null
+++ b/libraries/botbuilder-core/botbuilder/core/skills/bot_framework_client.py
@@ -0,0 +1,20 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from abc import ABC
+
+from botbuilder.schema import Activity
+from botbuilder.core import InvokeResponse
+
+
+class BotFrameworkClient(ABC):
+    def post_activity(
+        self,
+        from_bot_id: str,
+        to_bot_id: str,
+        to_url: str,
+        service_url: str,
+        conversation_id: str,
+        activity: Activity,
+    ) -> InvokeResponse:
+        raise NotImplementedError()
diff --git a/libraries/botbuilder-core/botbuilder/core/skills/bot_framework_skill.py b/libraries/botbuilder-core/botbuilder/core/skills/bot_framework_skill.py
new file mode 100644
index 000000000..8819d6674
--- /dev/null
+++ b/libraries/botbuilder-core/botbuilder/core/skills/bot_framework_skill.py
@@ -0,0 +1,14 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+
+class BotFrameworkSkill:
+    """
+    Registration for a BotFrameworkHttpProtocol based Skill endpoint.
+    """
+
+    # pylint: disable=invalid-name
+    def __init__(self, id: str = None, app_id: str = None, skill_endpoint: str = None):
+        self.id = id
+        self.app_id = app_id
+        self.skill_endpoint = skill_endpoint
diff --git a/libraries/botbuilder-core/botbuilder/core/skills/conversation_id_factory.py b/libraries/botbuilder-core/botbuilder/core/skills/conversation_id_factory.py
new file mode 100644
index 000000000..bb00c1ac7
--- /dev/null
+++ b/libraries/botbuilder-core/botbuilder/core/skills/conversation_id_factory.py
@@ -0,0 +1,65 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from abc import ABC, abstractmethod
+from typing import Union
+from botbuilder.schema import ConversationReference
+from .skill_conversation_id_factory_options import SkillConversationIdFactoryOptions
+from .skill_conversation_reference import SkillConversationReference
+
+
+class ConversationIdFactoryBase(ABC):
+    """
+    Handles creating conversation ids for skill and should be subclassed.
+
+    .. remarks::
+        Derive from this class to handle creation of conversation ids, retrieval of
+        SkillConversationReferences and deletion.
+    """
+
+    @abstractmethod
+    async def create_skill_conversation_id(
+        self,
+        options_or_conversation_reference: Union[
+            SkillConversationIdFactoryOptions, ConversationReference
+        ],
+    ) -> str:
+        """
+        Using the options passed in, creates a conversation id and :class:`SkillConversationReference`,
+         storing them for future use.
+
+        :param options_or_conversation_reference: The options contain properties useful for generating a
+         :class:`SkillConversationReference` and conversation id.
+        :type options_or_conversation_reference:
+         :class:`Union[SkillConversationIdFactoryOptions, ConversationReference]`
+
+        :returns: A skill conversation id.
+
+        .. note::
+            :class:`SkillConversationIdFactoryOptions` is the preferred parameter type, while the
+             :class:`SkillConversationReference` type is provided for backwards compatability.
+        """
+        raise NotImplementedError()
+
+    @abstractmethod
+    async def get_conversation_reference(
+        self, skill_conversation_id: str
+    ) -> Union[SkillConversationReference, ConversationReference]:
+        """
+        Retrieves a :class:`SkillConversationReference` using a conversation id passed in.
+
+        :param skill_conversation_id: The conversation id for which to retrieve the :class:`SkillConversationReference`.
+        :type skill_conversation_id: str
+
+        .. note::
+            SkillConversationReference is the preferred return type, while the :class:`SkillConversationReference`
+            type is provided for backwards compatability.
+        """
+        raise NotImplementedError()
+
+    @abstractmethod
+    async def delete_conversation_reference(self, skill_conversation_id: str):
+        """
+        Removes any reference to objects keyed on the conversation id passed in.
+        """
+        raise NotImplementedError()
diff --git a/libraries/botbuilder-core/botbuilder/core/skills/skill_conversation_id_factory_options.py b/libraries/botbuilder-core/botbuilder/core/skills/skill_conversation_id_factory_options.py
new file mode 100644
index 000000000..43d19c600
--- /dev/null
+++ b/libraries/botbuilder-core/botbuilder/core/skills/skill_conversation_id_factory_options.py
@@ -0,0 +1,19 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.schema import Activity
+from .bot_framework_skill import BotFrameworkSkill
+
+
+class SkillConversationIdFactoryOptions:
+    def __init__(
+        self,
+        from_bot_oauth_scope: str,
+        from_bot_id: str,
+        activity: Activity,
+        bot_framework_skill: BotFrameworkSkill,
+    ):
+        self.from_bot_oauth_scope = from_bot_oauth_scope
+        self.from_bot_id = from_bot_id
+        self.activity = activity
+        self.bot_framework_skill = bot_framework_skill
diff --git a/libraries/botbuilder-core/botbuilder/core/skills/skill_conversation_reference.py b/libraries/botbuilder-core/botbuilder/core/skills/skill_conversation_reference.py
new file mode 100644
index 000000000..341fb8104
--- /dev/null
+++ b/libraries/botbuilder-core/botbuilder/core/skills/skill_conversation_reference.py
@@ -0,0 +1,13 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+from botbuilder.schema import ConversationReference
+
+
+class SkillConversationReference:
+    """
+    ConversationReference implementation for Skills ConversationIdFactory.
+    """
+
+    def __init__(self, conversation_reference: ConversationReference, oauth_scope: str):
+        self.conversation_reference = conversation_reference
+        self.oauth_scope = oauth_scope
diff --git a/libraries/botbuilder-core/botbuilder/core/skills/skill_handler.py b/libraries/botbuilder-core/botbuilder/core/skills/skill_handler.py
new file mode 100644
index 000000000..be417b046
--- /dev/null
+++ b/libraries/botbuilder-core/botbuilder/core/skills/skill_handler.py
@@ -0,0 +1,303 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from uuid import uuid4
+
+from botbuilder.core import Bot, BotAdapter, ChannelServiceHandler, TurnContext
+from botbuilder.schema import (
+    Activity,
+    ActivityTypes,
+    ResourceResponse,
+    CallerIdConstants,
+)
+from botframework.connector.auth import (
+    AuthenticationConfiguration,
+    AuthenticationConstants,
+    ChannelProvider,
+    ClaimsIdentity,
+    CredentialProvider,
+    GovernmentConstants,
+    JwtTokenValidation,
+)
+from .skill_conversation_reference import SkillConversationReference
+from .conversation_id_factory import ConversationIdFactoryBase
+
+
+class SkillHandler(ChannelServiceHandler):
+
+    SKILL_CONVERSATION_REFERENCE_KEY = (
+        "botbuilder.core.skills.SkillConversationReference"
+    )
+
+    def __init__(
+        self,
+        adapter: BotAdapter,
+        bot: Bot,
+        conversation_id_factory: ConversationIdFactoryBase,
+        credential_provider: CredentialProvider,
+        auth_configuration: AuthenticationConfiguration,
+        channel_provider: ChannelProvider = None,
+        logger: object = None,
+    ):
+        super().__init__(credential_provider, auth_configuration, channel_provider)
+
+        if not adapter:
+            raise TypeError("adapter can't be None")
+        if not bot:
+            raise TypeError("bot can't be None")
+        if not conversation_id_factory:
+            raise TypeError("conversation_id_factory can't be None")
+
+        self._adapter = adapter
+        self._bot = bot
+        self._conversation_id_factory = conversation_id_factory
+        self._logger = logger
+
+    async def on_send_to_conversation(
+        self, claims_identity: ClaimsIdentity, conversation_id: str, activity: Activity,
+    ) -> ResourceResponse:
+        """
+        send_to_conversation() API for Skill
+
+        This method allows you to send an activity to the end of a conversation.
+
+        This is slightly different from ReplyToActivity().
+        * SendToConversation(conversation_id) - will append the activity to the end
+        of the conversation according to the timestamp or semantics of the channel.
+        * ReplyToActivity(conversation_id,ActivityId) - adds the activity as a reply
+        to another activity, if the channel supports it. If the channel does not
+        support nested replies, ReplyToActivity falls back to SendToConversation.
+
+        Use ReplyToActivity when replying to a specific activity in the
+        conversation.
+
+        Use SendToConversation in all other cases.
+        :param claims_identity: Claims identity for the bot.
+        :type claims_identity: :class:`botframework.connector.auth.ClaimsIdentity`
+        :param conversation_id:The conversation ID.
+        :type conversation_id: str
+        :param activity: Activity to send.
+        :type activity: Activity
+        :return:
+        """
+        return await self._process_activity(
+            claims_identity, conversation_id, None, activity,
+        )
+
+    async def on_reply_to_activity(
+        self,
+        claims_identity: ClaimsIdentity,
+        conversation_id: str,
+        activity_id: str,
+        activity: Activity,
+    ) -> ResourceResponse:
+        """
+        reply_to_activity() API for Skill.
+
+        This method allows you to reply to an activity.
+
+        This is slightly different from SendToConversation().
+        * SendToConversation(conversation_id) - will append the activity to the end
+        of the conversation according to the timestamp or semantics of the channel.
+        * ReplyToActivity(conversation_id,ActivityId) - adds the activity as a reply
+        to another activity, if the channel supports it. If the channel does not
+        support nested replies, ReplyToActivity falls back to SendToConversation.
+
+        Use ReplyToActivity when replying to a specific activity in the
+        conversation.
+
+        Use SendToConversation in all other cases.
+        :param claims_identity: Claims identity for the bot.
+        :type claims_identity: :class:`botframework.connector.auth.ClaimsIdentity`
+        :param conversation_id:The conversation ID.
+        :type conversation_id: str
+        :param activity_id: Activity ID to send.
+        :type activity_id: str
+        :param activity: Activity to send.
+        :type activity: Activity
+        :return:
+        """
+        return await self._process_activity(
+            claims_identity, conversation_id, activity_id, activity,
+        )
+
+    async def on_delete_activity(
+        self, claims_identity: ClaimsIdentity, conversation_id: str, activity_id: str
+    ):
+        skill_conversation_reference = await self._get_skill_conversation_reference(
+            conversation_id
+        )
+
+        async def callback(turn_context: TurnContext):
+            turn_context.turn_state[
+                self.SKILL_CONVERSATION_REFERENCE_KEY
+            ] = skill_conversation_reference
+            await turn_context.delete_activity(activity_id)
+
+        await self._adapter.continue_conversation(
+            skill_conversation_reference.conversation_reference,
+            callback,
+            claims_identity=claims_identity,
+            audience=skill_conversation_reference.oauth_scope,
+        )
+
+    async def on_update_activity(
+        self,
+        claims_identity: ClaimsIdentity,
+        conversation_id: str,
+        activity_id: str,
+        activity: Activity,
+    ) -> ResourceResponse:
+        skill_conversation_reference = await self._get_skill_conversation_reference(
+            conversation_id
+        )
+
+        resource_response: ResourceResponse = None
+
+        async def callback(turn_context: TurnContext):
+            nonlocal resource_response
+            turn_context.turn_state[
+                self.SKILL_CONVERSATION_REFERENCE_KEY
+            ] = skill_conversation_reference
+            activity.apply_conversation_reference(
+                skill_conversation_reference.conversation_reference
+            )
+            turn_context.activity.id = activity_id
+            turn_context.activity.caller_id = (
+                f"{CallerIdConstants.bot_to_bot_prefix}"
+                f"{JwtTokenValidation.get_app_id_from_claims(claims_identity.claims)}"
+            )
+            resource_response = await turn_context.update_activity(activity)
+
+        await self._adapter.continue_conversation(
+            skill_conversation_reference.conversation_reference,
+            callback,
+            claims_identity=claims_identity,
+            audience=skill_conversation_reference.oauth_scope,
+        )
+
+        return resource_response or ResourceResponse(id=str(uuid4()).replace("-", ""))
+
+    async def _get_skill_conversation_reference(
+        self, conversation_id: str
+    ) -> SkillConversationReference:
+        # Get the SkillsConversationReference
+        conversation_reference_result = await self._conversation_id_factory.get_conversation_reference(
+            conversation_id
+        )
+
+        # ConversationIdFactory can return either a SkillConversationReference (the newer way),
+        # or a ConversationReference (the old way, but still here for compatibility).  If a
+        # ConversationReference is returned, build a new SkillConversationReference to simplify
+        # the remainder of this method.
+        if isinstance(conversation_reference_result, SkillConversationReference):
+            skill_conversation_reference: SkillConversationReference = conversation_reference_result
+        else:
+            skill_conversation_reference: SkillConversationReference = SkillConversationReference(
+                conversation_reference=conversation_reference_result,
+                oauth_scope=(
+                    GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
+                    if self._channel_provider and self._channel_provider.is_government()
+                    else AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
+                ),
+            )
+
+        if not skill_conversation_reference:
+            raise KeyError("SkillConversationReference not found")
+
+        if not skill_conversation_reference.conversation_reference:
+            raise KeyError("conversationReference not found")
+
+        return skill_conversation_reference
+
+    async def _process_activity(
+        self,
+        claims_identity: ClaimsIdentity,
+        conversation_id: str,
+        reply_to_activity_id: str,
+        activity: Activity,
+    ) -> ResourceResponse:
+        skill_conversation_reference = await self._get_skill_conversation_reference(
+            conversation_id
+        )
+
+        # If an activity is sent, return the ResourceResponse
+        resource_response: ResourceResponse = None
+
+        async def callback(context: TurnContext):
+            nonlocal resource_response
+            context.turn_state[
+                SkillHandler.SKILL_CONVERSATION_REFERENCE_KEY
+            ] = skill_conversation_reference
+
+            TurnContext.apply_conversation_reference(
+                activity, skill_conversation_reference.conversation_reference
+            )
+
+            context.activity.id = reply_to_activity_id
+
+            app_id = JwtTokenValidation.get_app_id_from_claims(claims_identity.claims)
+            context.activity.caller_id = (
+                f"{CallerIdConstants.bot_to_bot_prefix}{app_id}"
+            )
+
+            if activity.type == ActivityTypes.end_of_conversation:
+                await self._conversation_id_factory.delete_conversation_reference(
+                    conversation_id
+                )
+                self._apply_eoc_to_turn_context_activity(context, activity)
+                await self._bot.on_turn(context)
+            elif activity.type == ActivityTypes.event:
+                self._apply_event_to_turn_context_activity(context, activity)
+                await self._bot.on_turn(context)
+            else:
+                resource_response = await context.send_activity(activity)
+
+        await self._adapter.continue_conversation(
+            skill_conversation_reference.conversation_reference,
+            callback,
+            claims_identity=claims_identity,
+            audience=skill_conversation_reference.oauth_scope,
+        )
+
+        if not resource_response:
+            resource_response = ResourceResponse(id=str(uuid4()))
+
+        return resource_response
+
+    @staticmethod
+    def _apply_eoc_to_turn_context_activity(
+        context: TurnContext, end_of_conversation_activity: Activity
+    ):
+        context.activity.type = end_of_conversation_activity.type
+        context.activity.text = end_of_conversation_activity.text
+        context.activity.code = end_of_conversation_activity.code
+
+        context.activity.reply_to_id = end_of_conversation_activity.reply_to_id
+        context.activity.value = end_of_conversation_activity.value
+        context.activity.entities = end_of_conversation_activity.entities
+        context.activity.locale = end_of_conversation_activity.locale
+        context.activity.local_timestamp = end_of_conversation_activity.local_timestamp
+        context.activity.timestamp = end_of_conversation_activity.timestamp
+        context.activity.channel_data = end_of_conversation_activity.channel_data
+        context.activity.additional_properties = (
+            end_of_conversation_activity.additional_properties
+        )
+
+    @staticmethod
+    def _apply_event_to_turn_context_activity(
+        context: TurnContext, event_activity: Activity
+    ):
+        context.activity.type = event_activity.type
+        context.activity.name = event_activity.name
+        context.activity.value = event_activity.value
+        context.activity.relates_to = event_activity.relates_to
+
+        context.activity.reply_to_id = event_activity.reply_to_id
+        context.activity.value = event_activity.value
+        context.activity.entities = event_activity.entities
+        context.activity.locale = event_activity.locale
+        context.activity.local_timestamp = event_activity.local_timestamp
+        context.activity.timestamp = event_activity.timestamp
+        context.activity.channel_data = event_activity.channel_data
+        context.activity.additional_properties = event_activity.additional_properties
diff --git a/libraries/botbuilder-core/botbuilder/core/teams/__init__.py b/libraries/botbuilder-core/botbuilder/core/teams/__init__.py
new file mode 100644
index 000000000..d9d4847e8
--- /dev/null
+++ b/libraries/botbuilder-core/botbuilder/core/teams/__init__.py
@@ -0,0 +1,22 @@
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+# --------------------------------------------------------------------------
+
+from .teams_activity_handler import TeamsActivityHandler
+from .teams_info import TeamsInfo
+from .teams_activity_extensions import (
+    teams_get_channel_id,
+    teams_get_team_info,
+    teams_notify_user,
+)
+
+__all__ = [
+    "TeamsActivityHandler",
+    "TeamsInfo",
+    "teams_get_channel_id",
+    "teams_get_team_info",
+    "teams_notify_user",
+]
diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_extensions.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_extensions.py
new file mode 100644
index 000000000..7b9c2fd0a
--- /dev/null
+++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_extensions.py
@@ -0,0 +1,69 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.schema import Activity
+from botbuilder.schema.teams import (
+    NotificationInfo,
+    TeamsChannelData,
+    TeamInfo,
+    TeamsMeetingInfo,
+)
+
+
+def teams_get_channel_data(activity: Activity) -> TeamsChannelData:
+    if not activity:
+        return None
+
+    if activity.channel_data:
+        return TeamsChannelData().deserialize(activity.channel_data)
+
+    return None
+
+
+def teams_get_channel_id(activity: Activity) -> str:
+    if not activity:
+        return None
+
+    if activity.channel_data:
+        channel_data = TeamsChannelData().deserialize(activity.channel_data)
+        return channel_data.channel.id if channel_data.channel else None
+
+    return None
+
+
+def teams_get_team_info(activity: Activity) -> TeamInfo:
+    if not activity:
+        return None
+
+    if activity.channel_data:
+        channel_data = TeamsChannelData().deserialize(activity.channel_data)
+        return channel_data.team
+
+    return None
+
+
+def teams_notify_user(
+    activity: Activity, alert_in_meeting: bool = None, external_resource_url: str = None
+):
+    if not activity:
+        return
+
+    if not activity.channel_data:
+        activity.channel_data = {}
+
+    channel_data = TeamsChannelData().deserialize(activity.channel_data)
+    channel_data.notification = NotificationInfo(alert=True)
+    channel_data.notification.alert_in_meeting = alert_in_meeting
+    channel_data.notification.external_resource_url = external_resource_url
+    activity.channel_data = channel_data
+
+
+def teams_get_meeting_info(activity: Activity) -> TeamsMeetingInfo:
+    if not activity:
+        return None
+
+    if activity.channel_data:
+        channel_data = TeamsChannelData().deserialize(activity.channel_data)
+        return channel_data.meeting
+
+    return None
diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py
new file mode 100644
index 000000000..5b2673a22
--- /dev/null
+++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py
@@ -0,0 +1,837 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+# pylint: disable=too-many-lines
+
+from http import HTTPStatus
+from botbuilder.schema import ChannelAccount, ErrorResponseException, SignInConstants
+from botbuilder.core import ActivityHandler, InvokeResponse
+from botbuilder.core.activity_handler import _InvokeResponseException
+from botbuilder.core.turn_context import TurnContext
+from botbuilder.core.teams.teams_info import TeamsInfo
+from botbuilder.schema.teams import (
+    AppBasedLinkQuery,
+    TeamInfo,
+    ChannelInfo,
+    FileConsentCardResponse,
+    TeamsChannelData,
+    TeamsChannelAccount,
+    MessagingExtensionAction,
+    MessagingExtensionQuery,
+    MessagingExtensionActionResponse,
+    MessagingExtensionResponse,
+    O365ConnectorCardActionQuery,
+    TaskModuleRequest,
+    TaskModuleResponse,
+)
+from botframework.connector import Channels
+from ..serializer_helper import deserializer_helper
+
+
+class TeamsActivityHandler(ActivityHandler):
+    async def on_invoke_activity(self, turn_context: TurnContext) -> InvokeResponse:
+        """
+        Invoked when an invoke activity is received from the connector.
+        Invoke activities can be used to communicate many different things.
+
+        :param turn_context: A context object for this turn.
+
+        :returns: An InvokeResponse that represents the work queued to execute.
+
+        .. remarks::
+            Invoke activities communicate programmatic commands from a client or channel to a bot.
+            The meaning of an invoke activity is defined by the "invoke_activity.name" property,
+            which is meaningful within the scope of a channel.
+        """
+        try:
+            if (
+                not turn_context.activity.name
+                and turn_context.activity.channel_id == Channels.ms_teams
+            ):
+                return await self.on_teams_card_action_invoke(turn_context)
+
+            if (
+                turn_context.activity.name
+                == SignInConstants.token_exchange_operation_name
+            ):
+                await self.on_teams_signin_token_exchange(turn_context)
+                return self._create_invoke_response()
+
+            if turn_context.activity.name == "fileConsent/invoke":
+                return await self.on_teams_file_consent(
+                    turn_context,
+                    deserializer_helper(
+                        FileConsentCardResponse, turn_context.activity.value
+                    ),
+                )
+
+            if turn_context.activity.name == "actionableMessage/executeAction":
+                await self.on_teams_o365_connector_card_action(
+                    turn_context,
+                    deserializer_helper(
+                        O365ConnectorCardActionQuery, turn_context.activity.value
+                    ),
+                )
+                return self._create_invoke_response()
+
+            if turn_context.activity.name == "composeExtension/queryLink":
+                return self._create_invoke_response(
+                    await self.on_teams_app_based_link_query(
+                        turn_context,
+                        deserializer_helper(
+                            AppBasedLinkQuery, turn_context.activity.value
+                        ),
+                    )
+                )
+
+            if turn_context.activity.name == "composeExtension/query":
+                return self._create_invoke_response(
+                    await self.on_teams_messaging_extension_query(
+                        turn_context,
+                        deserializer_helper(
+                            MessagingExtensionQuery, turn_context.activity.value
+                        ),
+                    )
+                )
+
+            if turn_context.activity.name == "composeExtension/selectItem":
+                return self._create_invoke_response(
+                    await self.on_teams_messaging_extension_select_item(
+                        turn_context, turn_context.activity.value
+                    )
+                )
+
+            if turn_context.activity.name == "composeExtension/submitAction":
+                return self._create_invoke_response(
+                    await self.on_teams_messaging_extension_submit_action_dispatch(
+                        turn_context,
+                        deserializer_helper(
+                            MessagingExtensionAction, turn_context.activity.value
+                        ),
+                    )
+                )
+
+            if turn_context.activity.name == "composeExtension/fetchTask":
+                return self._create_invoke_response(
+                    await self.on_teams_messaging_extension_fetch_task(
+                        turn_context,
+                        deserializer_helper(
+                            MessagingExtensionAction, turn_context.activity.value,
+                        ),
+                    )
+                )
+
+            if turn_context.activity.name == "composeExtension/querySettingUrl":
+                return self._create_invoke_response(
+                    await self.on_teams_messaging_extension_configuration_query_settings_url(
+                        turn_context,
+                        deserializer_helper(
+                            MessagingExtensionQuery, turn_context.activity.value
+                        ),
+                    )
+                )
+
+            if turn_context.activity.name == "composeExtension/setting":
+                await self.on_teams_messaging_extension_configuration_setting(
+                    turn_context, turn_context.activity.value
+                )
+                return self._create_invoke_response()
+
+            if turn_context.activity.name == "composeExtension/onCardButtonClicked":
+                await self.on_teams_messaging_extension_card_button_clicked(
+                    turn_context, turn_context.activity.value
+                )
+                return self._create_invoke_response()
+
+            if turn_context.activity.name == "task/fetch":
+                return self._create_invoke_response(
+                    await self.on_teams_task_module_fetch(
+                        turn_context,
+                        deserializer_helper(
+                            TaskModuleRequest, turn_context.activity.value
+                        ),
+                    )
+                )
+
+            if turn_context.activity.name == "task/submit":
+                return self._create_invoke_response(
+                    await self.on_teams_task_module_submit(
+                        turn_context,
+                        deserializer_helper(
+                            TaskModuleRequest, turn_context.activity.value
+                        ),
+                    )
+                )
+
+            return await super().on_invoke_activity(turn_context)
+
+        except _InvokeResponseException as invoke_exception:
+            return invoke_exception.create_invoke_response()
+
+    async def on_sign_in_invoke(self, turn_context: TurnContext):
+        """
+        Invoked when a signIn invoke activity is received from the connector.
+
+        :param turn_context: A context object for this turn.
+
+        :returns: A task that represents the work queued to execute.
+        """
+        return await self.on_teams_signin_verify_state(turn_context)
+
+    async def on_teams_card_action_invoke(
+        self, turn_context: TurnContext
+    ) -> InvokeResponse:
+        """
+        Invoked when an card action invoke activity is received from the connector.
+
+        :param turn_context: A context object for this turn.
+
+        :returns: An InvokeResponse that represents the work queued to execute.
+        """
+        raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED)
+
+    async def on_teams_signin_verify_state(self, turn_context: TurnContext):
+        """
+        Invoked when a signIn verify state activity is received from the connector.
+
+        :param turn_context: A context object for this turn.
+
+        :returns: A task that represents the work queued to execute.
+        """
+        raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED)
+
+    async def on_teams_signin_token_exchange(self, turn_context: TurnContext):
+        raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED)
+
+    async def on_teams_file_consent(
+        self,
+        turn_context: TurnContext,
+        file_consent_card_response: FileConsentCardResponse,
+    ) -> InvokeResponse:
+        """
+        Invoked when a file consent card activity is received from the connector.
+
+        :param turn_context: A context object for this turn.
+        :param file_consent_card_response: The response representing the value of the invoke
+        activity sent when the user acts on a file consent card.
+
+        :returns: An InvokeResponse depending on the action of the file consent card.
+        """
+        if file_consent_card_response.action == "accept":
+            await self.on_teams_file_consent_accept(
+                turn_context, file_consent_card_response
+            )
+            return self._create_invoke_response()
+
+        if file_consent_card_response.action == "decline":
+            await self.on_teams_file_consent_decline(
+                turn_context, file_consent_card_response
+            )
+            return self._create_invoke_response()
+
+        raise _InvokeResponseException(
+            HTTPStatus.BAD_REQUEST,
+            f"{file_consent_card_response.action} is not a supported Action.",
+        )
+
+    async def on_teams_file_consent_accept(  # pylint: disable=unused-argument
+        self,
+        turn_context: TurnContext,
+        file_consent_card_response: FileConsentCardResponse,
+    ):
+        """
+        Invoked when a file consent card is accepted by the user.
+
+        :param turn_context: A context object for this turn.
+        :param file_consent_card_response: The response representing the value of the invoke
+        activity sent when the user accepts a file consent card.
+
+        :returns: A task that represents the work queued to execute.
+        """
+        raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED)
+
+    async def on_teams_file_consent_decline(  # pylint: disable=unused-argument
+        self,
+        turn_context: TurnContext,
+        file_consent_card_response: FileConsentCardResponse,
+    ):
+        """
+        Invoked when a file consent card is declined by the user.
+
+        :param turn_context: A context object for this turn.
+        :param file_consent_card_response: The response representing the value of the invoke
+        activity sent when the user declines a file consent card.
+
+        :returns: A task that represents the work queued to execute.
+        """
+        raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED)
+
+    async def on_teams_o365_connector_card_action(  # pylint: disable=unused-argument
+        self, turn_context: TurnContext, query: O365ConnectorCardActionQuery
+    ):
+        """
+        Invoked when a O365 Connector Card Action activity is received from the connector.
+
+        :param turn_context: A context object for this turn.
+        :param query: The O365 connector card HttpPOST invoke query.
+
+        :returns: A task that represents the work queued to execute.
+        """
+        raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED)
+
+    async def on_teams_app_based_link_query(  # pylint: disable=unused-argument
+        self, turn_context: TurnContext, query: AppBasedLinkQuery
+    ) -> MessagingExtensionResponse:
+        """
+        Invoked when an app based link query activity is received from the connector.
+
+        :param turn_context: A context object for this turn.
+        :param query: The invoke request body type for app-based link query.
+
+        :returns: The Messaging Extension Response for the query.
+        """
+        raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED)
+
+    async def on_teams_messaging_extension_query(  # pylint: disable=unused-argument
+        self, turn_context: TurnContext, query: MessagingExtensionQuery
+    ) -> MessagingExtensionResponse:
+        """
+        Invoked when a Messaging Extension Query activity is received from the connector.
+
+        :param turn_context: A context object for this turn.
+        :param query: The query for the search command.
+
+        :returns: The Messaging Extension Response for the query.
+        """
+        raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED)
+
+    async def on_teams_messaging_extension_select_item(  # pylint: disable=unused-argument
+        self, turn_context: TurnContext, query
+    ) -> MessagingExtensionResponse:
+        """
+        Invoked when a messaging extension select item activity is received from the connector.
+
+        :param turn_context: A context object for this turn.
+        :param query: The object representing the query.
+
+        :returns: The Messaging Extension Response for the query.
+        """
+        raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED)
+
+    async def on_teams_messaging_extension_submit_action_dispatch(
+        self, turn_context: TurnContext, action: MessagingExtensionAction
+    ) -> MessagingExtensionActionResponse:
+        """
+        Invoked when a messaging extension submit action dispatch activity is received from the connector.
+
+        :param turn_context: A context object for this turn.
+        :param action: The messaging extension action.
+
+        :returns: The Messaging Extension Action Response for the action.
+        """
+        if not action.bot_message_preview_action:
+            return await self.on_teams_messaging_extension_submit_action(
+                turn_context, action
+            )
+
+        if action.bot_message_preview_action == "edit":
+            return await self.on_teams_messaging_extension_bot_message_preview_edit(
+                turn_context, action
+            )
+
+        if action.bot_message_preview_action == "send":
+            return await self.on_teams_messaging_extension_bot_message_preview_send(
+                turn_context, action
+            )
+
+        raise _InvokeResponseException(
+            status_code=HTTPStatus.BAD_REQUEST,
+            body=f"{action.bot_message_preview_action} is not a supported BotMessagePreviewAction",
+        )
+
+    async def on_teams_messaging_extension_bot_message_preview_edit(  # pylint: disable=unused-argument
+        self, turn_context: TurnContext, action: MessagingExtensionAction
+    ) -> MessagingExtensionActionResponse:
+        """
+        Invoked when a messaging extension bot message preview edit activity is received from the connector.
+
+        :param turn_context: A context object for this turn.
+        :param action: The messaging extension action.
+
+        :returns: The Messaging Extension Action Response for the action.
+        """
+        raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED)
+
+    async def on_teams_messaging_extension_bot_message_preview_send(  # pylint: disable=unused-argument
+        self, turn_context: TurnContext, action: MessagingExtensionAction
+    ) -> MessagingExtensionActionResponse:
+        """
+        Invoked when a messaging extension bot message preview send activity is received from the connector.
+
+        :param turn_context: A context object for this turn.
+        :param action: The messaging extension action.
+
+        :returns: The Messaging Extension Action Response for the action.
+        """
+        raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED)
+
+    async def on_teams_messaging_extension_submit_action(  # pylint: disable=unused-argument
+        self, turn_context: TurnContext, action: MessagingExtensionAction
+    ) -> MessagingExtensionActionResponse:
+        """
+        Invoked when a messaging extension submit action activity is received from the connector.
+
+        :param turn_context: A context object for this turn.
+        :param action: The messaging extension action.
+
+        :returns: The Messaging Extension Action Response for the action.
+        """
+        raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED)
+
+    async def on_teams_messaging_extension_fetch_task(  # pylint: disable=unused-argument
+        self, turn_context: TurnContext, action: MessagingExtensionAction
+    ) -> MessagingExtensionActionResponse:
+        """
+        Invoked when a Messaging Extension Fetch activity is received from the connector.
+
+        :param turn_context: A context object for this turn.
+        :param action: The messaging extension action.
+
+        :returns: The Messaging Extension Action Response for the action.
+        """
+        raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED)
+
+    async def on_teams_messaging_extension_configuration_query_settings_url(  # pylint: disable=unused-argument
+        self, turn_context: TurnContext, query: MessagingExtensionQuery
+    ) -> MessagingExtensionResponse:
+        """
+        Invoked when a messaging extension configuration query setting url activity is received from the connector.
+
+        :param turn_context: A context object for this turn.
+        :param query: The Messaging extension query.
+
+        :returns: The Messaging Extension Response for the query.
+        """
+        raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED)
+
+    async def on_teams_messaging_extension_configuration_setting(  # pylint: disable=unused-argument
+        self, turn_context: TurnContext, settings
+    ):
+        """
+        Override this in a derived class to provide logic for when a configuration is set for a messaging extension.
+
+        :param turn_context: A context object for this turn.
+        :param settings: Object representing the configuration settings.
+
+        :returns: A task that represents the work queued to execute.
+        """
+        raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED)
+
+    async def on_teams_messaging_extension_card_button_clicked(  # pylint: disable=unused-argument
+        self, turn_context: TurnContext, card_data
+    ):
+        """
+        Override this in a derived class to provide logic for when a card button is clicked in a messaging extension.
+
+        :param turn_context: A context object for this turn.
+        :param card_data: Object representing the card data.
+
+        :returns: A task that represents the work queued to execute.
+        """
+        raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED)
+
+    async def on_teams_task_module_fetch(  # pylint: disable=unused-argument
+        self, turn_context: TurnContext, task_module_request: TaskModuleRequest
+    ) -> TaskModuleResponse:
+        """
+        Override this in a derived class to provide logic for when a task module is fetched.
+
+        :param turn_context: A context object for this turn.
+        :param task_module_request: The task module invoke request value payload.
+
+        :returns: A Task Module Response for the request.
+        """
+        raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED)
+
+    async def on_teams_task_module_submit(  # pylint: disable=unused-argument
+        self, turn_context: TurnContext, task_module_request: TaskModuleRequest
+    ) -> TaskModuleResponse:
+        """
+        Override this in a derived class to provide logic for when a task module is submitted.
+
+        :param turn_context: A context object for this turn.
+        :param task_module_request: The task module invoke request value payload.
+
+        :returns: A Task Module Response for the request.
+        """
+        raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED)
+
+    async def on_conversation_update_activity(self, turn_context: TurnContext):
+        """
+        Invoked when a conversation update activity is received from the channel.
+        Conversation update activities are useful when it comes to responding to users
+        being added to or removed from the channel.
+        For example, a bot could respond to a user being added by greeting the user.
+
+        :param turn_context: A context object for this turn.
+
+        :returns: A task that represents the work queued to execute.
+
+        .. remarks::
+            In a derived class, override this method to add logic that applies
+            to all conversation update activities.
+        """
+        if turn_context.activity.channel_id == Channels.ms_teams:
+            channel_data = TeamsChannelData().deserialize(
+                turn_context.activity.channel_data
+            )
+            if turn_context.activity.members_added:
+                return await self.on_teams_members_added_dispatch(
+                    turn_context.activity.members_added, channel_data.team, turn_context
+                )
+
+            if turn_context.activity.members_removed:
+                return await self.on_teams_members_removed_dispatch(
+                    turn_context.activity.members_removed,
+                    channel_data.team,
+                    turn_context,
+                )
+
+            if channel_data:
+                if channel_data.event_type == "channelCreated":
+                    return await self.on_teams_channel_created(
+                        ChannelInfo().deserialize(channel_data.channel),
+                        channel_data.team,
+                        turn_context,
+                    )
+                if channel_data.event_type == "channelDeleted":
+                    return await self.on_teams_channel_deleted(
+                        channel_data.channel, channel_data.team, turn_context
+                    )
+                if channel_data.event_type == "channelRenamed":
+                    return await self.on_teams_channel_renamed(
+                        channel_data.channel, channel_data.team, turn_context
+                    )
+                if channel_data.event_type == "teamArchived":
+                    return await self.on_teams_team_archived(
+                        channel_data.team, turn_context
+                    )
+                if channel_data.event_type == "teamDeleted":
+                    return await self.on_teams_team_deleted(
+                        channel_data.team, turn_context
+                    )
+                if channel_data.event_type == "teamHardDeleted":
+                    return await self.on_teams_team_hard_deleted(
+                        channel_data.team, turn_context
+                    )
+                if channel_data.event_type == "channelRestored":
+                    return await self.on_teams_channel_restored(
+                        channel_data.channel, channel_data.team, turn_context
+                    )
+                if channel_data.event_type == "teamRenamed":
+                    return await self.on_teams_team_renamed(
+                        channel_data.team, turn_context
+                    )
+                if channel_data.event_type == "teamRestored":
+                    return await self.on_teams_team_restored(
+                        channel_data.team, turn_context
+                    )
+                if channel_data.event_type == "teamUnarchived":
+                    return await self.on_teams_team_unarchived(
+                        channel_data.team, turn_context
+                    )
+
+        return await super().on_conversation_update_activity(turn_context)
+
+    async def on_teams_channel_created(  # pylint: disable=unused-argument
+        self, channel_info: ChannelInfo, team_info: TeamInfo, turn_context: TurnContext
+    ):
+        """
+        Invoked when a Channel Created event activity is received from the connector.
+        Channel Created correspond to the user creating a new channel.
+
+        :param channel_info: The channel info object which describes the channel.
+        :param team_info: The team info object representing the team.
+        :param turn_context: A context object for this turn.
+
+        :returns: A task that represents the work queued to execute.
+        """
+        return
+
+    async def on_teams_team_archived(  # pylint: disable=unused-argument
+        self, team_info: TeamInfo, turn_context: TurnContext
+    ):
+        """
+        Invoked when a Team Archived event activity is received from the connector.
+        Team Archived correspond to the user archiving a team.
+
+        :param team_info: The team info object representing the team.
+        :param turn_context: A context object for this turn.
+
+        :returns: A task that represents the work queued to execute.
+        """
+        return
+
+    async def on_teams_team_deleted(  # pylint: disable=unused-argument
+        self, team_info: TeamInfo, turn_context: TurnContext
+    ):
+        """
+        Invoked when a Team Deleted event activity is received from the connector.
+        Team Deleted corresponds to the user deleting a team.
+
+        :param team_info: The team info object representing the team.
+        :param turn_context: A context object for this turn.
+
+        :returns: A task that represents the work queued to execute.
+        """
+        return
+
+    async def on_teams_team_hard_deleted(  # pylint: disable=unused-argument
+        self, team_info: TeamInfo, turn_context: TurnContext
+    ):
+        """
+        Invoked when a Team Hard Deleted event activity is received from the connector.
+        Team Hard Deleted corresponds to the user hard deleting a team.
+
+        :param team_info: The team info object representing the team.
+        :param turn_context: A context object for this turn.
+
+        :returns: A task that represents the work queued to execute.
+        """
+        return
+
+    async def on_teams_team_renamed(  # pylint: disable=unused-argument
+        self, team_info: TeamInfo, turn_context: TurnContext
+    ):
+        """
+        Invoked when a Team Renamed event activity is received from the connector.
+        Team Renamed correspond to the user renaming an existing team.
+
+        :param team_info: The team info object representing the team.
+        :param turn_context: A context object for this turn.
+
+        :returns: A task that represents the work queued to execute.
+        """
+        return await self.on_teams_team_renamed_activity(team_info, turn_context)
+
+    async def on_teams_team_renamed_activity(  # pylint: disable=unused-argument
+        self, team_info: TeamInfo, turn_context: TurnContext
+    ):
+        """
+        DEPRECATED. Please use on_teams_team_renamed(). This method will remain in place throughout
+        v4 so as not to break existing bots.
+
+        Invoked when a Team Renamed event activity is received from the connector.
+        Team Renamed correspond to the user renaming an existing team.
+
+        :param team_info: The team info object representing the team.
+        :param turn_context: A context object for this turn.
+
+        :returns: A task that represents the work queued to execute.
+        """
+        return
+
+    async def on_teams_team_restored(  # pylint: disable=unused-argument
+        self, team_info: TeamInfo, turn_context: TurnContext
+    ):
+        """
+        Invoked when a Team Restored event activity is received from the connector.
+        Team Restored corresponds to the user restoring a team.
+
+        :param team_info: The team info object representing the team.
+        :param turn_context: A context object for this turn.
+
+        :returns: A task that represents the work queued to execute.
+        """
+        return
+
+    async def on_teams_team_unarchived(  # pylint: disable=unused-argument
+        self, team_info: TeamInfo, turn_context: TurnContext
+    ):
+        """
+        Invoked when a Team Unarchived event activity is received from the connector.
+        Team Unarchived correspond to the user unarchiving a team.
+
+        :param team_info: The team info object representing the team.
+        :param turn_context: A context object for this turn.
+
+        :returns: A task that represents the work queued to execute.
+        """
+        return
+
+    async def on_teams_members_added_dispatch(  # pylint: disable=unused-argument
+        self,
+        members_added: [ChannelAccount],
+        team_info: TeamInfo,
+        turn_context: TurnContext,
+    ):
+        """
+        Override this in a derived class to provide logic for when members other than the bot
+        join the channel, such as your bot's welcome logic.
+        It will get the associated members with the provided accounts.
+
+        :param members_added: A list of all the accounts added to the channel, as
+        described by the conversation update activity.
+        :param team_info: The team info object representing the team.
+        :param turn_context: A context object for this turn.
+
+        :returns: A task that represents the work queued to execute.
+        """
+        team_members_added = []
+        for member in members_added:
+            is_bot = (
+                turn_context.activity.recipient is not None
+                and member.id == turn_context.activity.recipient.id
+            )
+            if member.additional_properties != {} or is_bot:
+                team_members_added.append(
+                    deserializer_helper(TeamsChannelAccount, member)
+                )
+            else:
+                team_member = None
+                try:
+                    team_member = await TeamsInfo.get_member(turn_context, member.id)
+                    team_members_added.append(team_member)
+                except ErrorResponseException as ex:
+                    if (
+                        ex.error
+                        and ex.error.error
+                        and ex.error.error.code == "ConversationNotFound"
+                    ):
+                        new_teams_channel_account = TeamsChannelAccount(
+                            id=member.id,
+                            name=member.name,
+                            aad_object_id=member.aad_object_id,
+                            role=member.role,
+                        )
+                        team_members_added.append(new_teams_channel_account)
+                    else:
+                        raise ex
+
+        return await self.on_teams_members_added(
+            team_members_added, team_info, turn_context
+        )
+
+    async def on_teams_members_added(  # pylint: disable=unused-argument
+        self,
+        teams_members_added: [TeamsChannelAccount],
+        team_info: TeamInfo,
+        turn_context: TurnContext,
+    ):
+        """
+        Override this in a derived class to provide logic for when members other than the bot
+        join the channel, such as your bot's welcome logic.
+
+        :param teams_members_added: A list of all the members added to the channel, as
+        described by the conversation update activity.
+        :param team_info: The team info object representing the team.
+        :param turn_context: A context object for this turn.
+
+        :returns: A task that represents the work queued to execute.
+        """
+        teams_members_added = [
+            ChannelAccount().deserialize(member.serialize())
+            for member in teams_members_added
+        ]
+        return await super().on_members_added_activity(
+            teams_members_added, turn_context
+        )
+
+    async def on_teams_members_removed_dispatch(  # pylint: disable=unused-argument
+        self,
+        members_removed: [ChannelAccount],
+        team_info: TeamInfo,
+        turn_context: TurnContext,
+    ):
+        """
+        Override this in a derived class to provide logic for when members other than the bot
+        leave the channel, such as your bot's good-bye logic.
+        It will get the associated members with the provided accounts.
+
+        :param members_removed: A list of all the accounts removed from the channel, as
+        described by the conversation update activity.
+        :param team_info: The team info object representing the team.
+        :param turn_context: A context object for this turn.
+
+        :returns: A task that represents the work queued to execute.
+        """
+        teams_members_removed = []
+        for member in members_removed:
+            new_account_json = member.serialize()
+            if "additional_properties" in new_account_json:
+                del new_account_json["additional_properties"]
+            teams_members_removed.append(
+                TeamsChannelAccount().deserialize(new_account_json)
+            )
+
+        return await self.on_teams_members_removed(
+            teams_members_removed, team_info, turn_context
+        )
+
+    async def on_teams_members_removed(  # pylint: disable=unused-argument
+        self,
+        teams_members_removed: [TeamsChannelAccount],
+        team_info: TeamInfo,
+        turn_context: TurnContext,
+    ):
+        """
+        Override this in a derived class to provide logic for when members other than the bot
+        leave the channel, such as your bot's good-bye logic.
+
+        :param teams_members_removed: A list of all the members removed from the channel, as
+        described by the conversation update activity.
+        :param team_info: The team info object representing the team.
+        :param turn_context: A context object for this turn.
+
+        :returns: A task that represents the work queued to execute.
+        """
+        members_removed = [
+            ChannelAccount().deserialize(member.serialize())
+            for member in teams_members_removed
+        ]
+        return await super().on_members_removed_activity(members_removed, turn_context)
+
+    async def on_teams_channel_deleted(  # pylint: disable=unused-argument
+        self, channel_info: ChannelInfo, team_info: TeamInfo, turn_context: TurnContext
+    ):
+        """
+        Invoked when a Channel Deleted event activity is received from the connector.
+        Channel Deleted correspond to the user deleting an existing channel.
+
+        :param channel_info: The channel info object which describes the channel.
+        :param team_info: The team info object representing the team.
+        :param turn_context: A context object for this turn.
+
+        :returns: A task that represents the work queued to execute.
+        """
+        return
+
+    async def on_teams_channel_renamed(  # pylint: disable=unused-argument
+        self, channel_info: ChannelInfo, team_info: TeamInfo, turn_context: TurnContext
+    ):
+        """
+        Invoked when a Channel Renamed event activity is received from the connector.
+        Channel Renamed correspond to the user renaming an existing channel.
+
+        :param channel_info: The channel info object which describes the channel.
+        :param team_info: The team info object representing the team.
+        :param turn_context: A context object for this turn.
+
+        :returns: A task that represents the work queued to execute.
+        """
+        return
+
+    async def on_teams_channel_restored(  # pylint: disable=unused-argument
+        self, channel_info: ChannelInfo, team_info: TeamInfo, turn_context: TurnContext
+    ):
+        """
+        Invoked when a Channel Restored event activity is received from the connector.
+        Channel Restored correspond to the user restoring a previously deleted channel.
+
+        :param channel_info: The channel info object which describes the channel.
+        :param team_info: The team info object representing the team.
+        :param turn_context: A context object for this turn.
+
+        :returns: A task that represents the work queued to execute.
+        """
+        return
diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_helper.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_helper.py
new file mode 100644
index 000000000..766cd6291
--- /dev/null
+++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_helper.py
@@ -0,0 +1,37 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from inspect import getmembers
+from typing import Type
+from enum import Enum
+
+from msrest.serialization import Model, Deserializer, Serializer
+
+import botbuilder.schema as schema
+import botbuilder.schema.teams as teams_schema
+
+DEPENDICIES = [
+    schema_cls
+    for key, schema_cls in getmembers(schema)
+    if isinstance(schema_cls, type) and issubclass(schema_cls, (Model, Enum))
+]
+DEPENDICIES += [
+    schema_cls
+    for key, schema_cls in getmembers(teams_schema)
+    if isinstance(schema_cls, type) and issubclass(schema_cls, (Model, Enum))
+]
+DEPENDICIES_DICT = {dependency.__name__: dependency for dependency in DEPENDICIES}
+
+
+def deserializer_helper(msrest_cls: Type[Model], dict_to_deserialize: dict) -> Model:
+    deserializer = Deserializer(DEPENDICIES_DICT)
+    return deserializer(msrest_cls.__name__, dict_to_deserialize)
+
+
+def serializer_helper(object_to_serialize: Model) -> dict:
+    if object_to_serialize is None:
+        return None
+
+    serializer = Serializer(DEPENDICIES_DICT)
+    # pylint: disable=protected-access
+    return serializer._serialize(object_to_serialize)
diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py
new file mode 100644
index 000000000..6533f38d6
--- /dev/null
+++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py
@@ -0,0 +1,317 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from typing import List, Tuple
+
+from botframework.connector.aio import ConnectorClient
+from botframework.connector.teams.teams_connector_client import TeamsConnectorClient
+from botbuilder.schema import ConversationParameters, ConversationReference
+from botbuilder.core.teams.teams_activity_extensions import (
+    teams_get_meeting_info,
+    teams_get_channel_data,
+)
+from botbuilder.core.turn_context import Activity, TurnContext
+from botbuilder.schema.teams import (
+    ChannelInfo,
+    TeamDetails,
+    TeamsChannelData,
+    TeamsChannelAccount,
+    TeamsPagedMembersResult,
+    TeamsMeetingParticipant,
+)
+
+
+class TeamsInfo:
+    @staticmethod
+    async def send_message_to_teams_channel(
+        turn_context: TurnContext, activity: Activity, teams_channel_id: str
+    ) -> Tuple[ConversationReference, str]:
+        if not turn_context:
+            raise ValueError("The turn_context cannot be None")
+        if not activity:
+            raise ValueError("The activity cannot be None")
+        if not teams_channel_id:
+            raise ValueError("The teams_channel_id cannot be None or empty")
+
+        old_ref = TurnContext.get_conversation_reference(turn_context.activity)
+        conversation_parameters = ConversationParameters(
+            is_group=True,
+            channel_data={"channel": {"id": teams_channel_id}},
+            activity=activity,
+        )
+
+        result = await turn_context.adapter.create_conversation(
+            old_ref, TeamsInfo._create_conversation_callback, conversation_parameters
+        )
+        return (result[0], result[1])
+
+    @staticmethod
+    async def _create_conversation_callback(
+        new_turn_context,
+    ) -> Tuple[ConversationReference, str]:
+        new_activity_id = new_turn_context.activity.id
+        conversation_reference = TurnContext.get_conversation_reference(
+            new_turn_context.activity
+        )
+        return (conversation_reference, new_activity_id)
+
+    @staticmethod
+    async def get_team_details(
+        turn_context: TurnContext, team_id: str = ""
+    ) -> TeamDetails:
+        if not team_id:
+            team_id = TeamsInfo.get_team_id(turn_context)
+
+        if not team_id:
+            raise TypeError(
+                "TeamsInfo.get_team_details: method is only valid within the scope of MS Teams Team."
+            )
+
+        teams_connector = await TeamsInfo.get_teams_connector_client(turn_context)
+        return teams_connector.teams.get_team_details(team_id)
+
+    @staticmethod
+    async def get_team_channels(
+        turn_context: TurnContext, team_id: str = ""
+    ) -> List[ChannelInfo]:
+        if not team_id:
+            team_id = TeamsInfo.get_team_id(turn_context)
+
+        if not team_id:
+            raise TypeError(
+                "TeamsInfo.get_team_channels: method is only valid within the scope of MS Teams Team."
+            )
+
+        teams_connector = await TeamsInfo.get_teams_connector_client(turn_context)
+        return teams_connector.teams.get_teams_channels(team_id).conversations
+
+    @staticmethod
+    async def get_team_members(
+        turn_context: TurnContext, team_id: str = ""
+    ) -> List[TeamsChannelAccount]:
+        if not team_id:
+            team_id = TeamsInfo.get_team_id(turn_context)
+
+        if not team_id:
+            raise TypeError(
+                "TeamsInfo.get_team_members: method is only valid within the scope of MS Teams Team."
+            )
+
+        connector_client = await TeamsInfo._get_connector_client(turn_context)
+        return await TeamsInfo._get_members(
+            connector_client, turn_context.activity.conversation.id,
+        )
+
+    @staticmethod
+    async def get_members(turn_context: TurnContext) -> List[TeamsChannelAccount]:
+        team_id = TeamsInfo.get_team_id(turn_context)
+        if not team_id:
+            conversation_id = turn_context.activity.conversation.id
+            connector_client = await TeamsInfo._get_connector_client(turn_context)
+            return await TeamsInfo._get_members(connector_client, conversation_id)
+
+        return await TeamsInfo.get_team_members(turn_context, team_id)
+
+    @staticmethod
+    async def get_paged_team_members(
+        turn_context: TurnContext,
+        team_id: str = "",
+        continuation_token: str = None,
+        page_size: int = None,
+    ) -> List[TeamsPagedMembersResult]:
+        if not team_id:
+            team_id = TeamsInfo.get_team_id(turn_context)
+
+        if not team_id:
+            raise TypeError(
+                "TeamsInfo.get_team_members: method is only valid within the scope of MS Teams Team."
+            )
+
+        connector_client = await TeamsInfo._get_connector_client(turn_context)
+        return await TeamsInfo._get_paged_members(
+            connector_client, team_id, continuation_token, page_size,
+        )
+
+    @staticmethod
+    async def get_paged_members(
+        turn_context: TurnContext, continuation_token: str = None, page_size: int = None
+    ) -> List[TeamsPagedMembersResult]:
+
+        team_id = TeamsInfo.get_team_id(turn_context)
+        if not team_id:
+            conversation_id = turn_context.activity.conversation.id
+            connector_client = await TeamsInfo._get_connector_client(turn_context)
+            return await TeamsInfo._get_paged_members(
+                connector_client, conversation_id, continuation_token, page_size
+            )
+
+        return await TeamsInfo.get_paged_team_members(
+            turn_context, team_id, continuation_token, page_size
+        )
+
+    @staticmethod
+    async def get_team_member(
+        turn_context: TurnContext, team_id: str = "", member_id: str = None
+    ) -> TeamsChannelAccount:
+        if not team_id:
+            team_id = TeamsInfo.get_team_id(turn_context)
+
+        if not team_id:
+            raise TypeError(
+                "TeamsInfo.get_team_member: method is only valid within the scope of MS Teams Team."
+            )
+
+        if not member_id:
+            raise TypeError("TeamsInfo.get_team_member: method requires a member_id")
+
+        connector_client = await TeamsInfo._get_connector_client(turn_context)
+        return await TeamsInfo._get_member(
+            connector_client, turn_context.activity.conversation.id, member_id
+        )
+
+    @staticmethod
+    async def get_member(
+        turn_context: TurnContext, member_id: str
+    ) -> TeamsChannelAccount:
+        team_id = TeamsInfo.get_team_id(turn_context)
+        if not team_id:
+            conversation_id = turn_context.activity.conversation.id
+            connector_client = await TeamsInfo._get_connector_client(turn_context)
+            return await TeamsInfo._get_member(
+                connector_client, conversation_id, member_id
+            )
+
+        return await TeamsInfo.get_team_member(turn_context, team_id, member_id)
+
+    @staticmethod
+    async def get_meeting_participant(
+        turn_context: TurnContext,
+        meeting_id: str = None,
+        participant_id: str = None,
+        tenant_id: str = None,
+    ) -> TeamsMeetingParticipant:
+        meeting_id = (
+            meeting_id
+            if meeting_id
+            else teams_get_meeting_info(turn_context.activity).id
+        )
+        if meeting_id is None:
+            raise TypeError(
+                "TeamsInfo._get_meeting_participant: method requires a meeting_id"
+            )
+
+        participant_id = (
+            participant_id
+            if participant_id
+            else turn_context.activity.from_property.aad_object_id
+        )
+        if participant_id is None:
+            raise TypeError(
+                "TeamsInfo._get_meeting_participant: method requires a participant_id"
+            )
+
+        tenant_id = (
+            tenant_id
+            if tenant_id
+            else teams_get_channel_data(turn_context.activity).tenant.id
+        )
+        if tenant_id is None:
+            raise TypeError(
+                "TeamsInfo._get_meeting_participant: method requires a tenant_id"
+            )
+
+        connector_client = await TeamsInfo.get_teams_connector_client(turn_context)
+        return connector_client.teams.fetch_participant(
+            meeting_id, participant_id, tenant_id
+        )
+
+    @staticmethod
+    async def get_teams_connector_client(
+        turn_context: TurnContext,
+    ) -> TeamsConnectorClient:
+        # A normal connector client is retrieved in order to use the credentials
+        # while creating a TeamsConnectorClient below
+        connector_client = await TeamsInfo._get_connector_client(turn_context)
+
+        return TeamsConnectorClient(
+            connector_client.config.credentials, turn_context.activity.service_url,
+        )
+
+    @staticmethod
+    def get_team_id(turn_context: TurnContext):
+        channel_data = TeamsChannelData(**turn_context.activity.channel_data)
+        if channel_data.team:
+            return channel_data.team["id"]
+        return ""
+
+    @staticmethod
+    async def _get_connector_client(turn_context: TurnContext) -> ConnectorClient:
+        return await turn_context.adapter.create_connector_client(
+            turn_context.activity.service_url
+        )
+
+    @staticmethod
+    async def _get_members(
+        connector_client: ConnectorClient, conversation_id: str
+    ) -> List[TeamsChannelAccount]:
+        if connector_client is None:
+            raise TypeError("TeamsInfo._get_members.connector_client: cannot be None.")
+
+        if not conversation_id:
+            raise TypeError("TeamsInfo._get_members.conversation_id: cannot be empty.")
+
+        teams_members = []
+        members = await connector_client.conversations.get_conversation_members(
+            conversation_id
+        )
+
+        for member in members:
+            teams_members.append(
+                TeamsChannelAccount().deserialize(
+                    dict(member.serialize(), **member.additional_properties)
+                )
+            )
+
+        return teams_members
+
+    @staticmethod
+    async def _get_paged_members(
+        connector_client: ConnectorClient,
+        conversation_id: str,
+        continuation_token: str = None,
+        page_size: int = None,
+    ) -> List[TeamsPagedMembersResult]:
+        if connector_client is None:
+            raise TypeError(
+                "TeamsInfo._get_paged_members.connector_client: cannot be None."
+            )
+
+        if not conversation_id:
+            raise TypeError(
+                "TeamsInfo._get_paged_members.conversation_id: cannot be empty."
+            )
+
+        return await connector_client.conversations.get_teams_conversation_paged_members(
+            conversation_id, page_size, continuation_token
+        )
+
+    @staticmethod
+    async def _get_member(
+        connector_client: ConnectorClient, conversation_id: str, member_id: str
+    ) -> TeamsChannelAccount:
+        if connector_client is None:
+            raise TypeError("TeamsInfo._get_member.connector_client: cannot be None.")
+
+        if not conversation_id:
+            raise TypeError("TeamsInfo._get_member.conversation_id: cannot be empty.")
+
+        if not member_id:
+            raise TypeError("TeamsInfo._get_member.member_id: cannot be empty.")
+
+        member: TeamsChannelAccount = await connector_client.conversations.get_conversation_member(
+            conversation_id, member_id
+        )
+
+        return TeamsChannelAccount().deserialize(
+            dict(member.serialize(), **member.additional_properties)
+        )
diff --git a/libraries/botbuilder-core/botbuilder/core/telemetry_constants.py b/libraries/botbuilder-core/botbuilder/core/telemetry_constants.py
index 1ae0f1816..a67a56fbd 100644
--- a/libraries/botbuilder-core/botbuilder/core/telemetry_constants.py
+++ b/libraries/botbuilder-core/botbuilder/core/telemetry_constants.py
@@ -5,6 +5,7 @@
 class TelemetryConstants:
     """Telemetry logger property names."""
 
+    ATTACHMENTS_PROPERTY: str = "attachments"
     CHANNEL_ID_PROPERTY: str = "channelId"
     CONVERSATION_ID_PROPERTY: str = "conversationId"
     CONVERSATION_NAME_PROPERTY: str = "conversationName"
diff --git a/libraries/botbuilder-core/botbuilder/core/telemetry_logger_middleware.py b/libraries/botbuilder-core/botbuilder/core/telemetry_logger_middleware.py
index 251bf7fb7..33fcd6681 100644
--- a/libraries/botbuilder-core/botbuilder/core/telemetry_logger_middleware.py
+++ b/libraries/botbuilder-core/botbuilder/core/telemetry_logger_middleware.py
@@ -1,9 +1,11 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License.
 """Middleware Component for logging Activity messages."""
-
 from typing import Awaitable, Callable, List, Dict
 from botbuilder.schema import Activity, ConversationReference, ActivityTypes
+from botbuilder.schema.teams import TeamsChannelData, TeamInfo
+from botframework.connector import Channels
+
 from .bot_telemetry_client import BotTelemetryClient
 from .bot_assert import BotAssert
 from .middleware_set import Middleware
@@ -160,15 +162,21 @@ async def fill_receive_event_properties(
         BotTelemetryClient.track_event method for the BotMessageReceived event.
         """
         properties = {
-            TelemetryConstants.FROM_ID_PROPERTY: activity.from_property.id,
+            TelemetryConstants.FROM_ID_PROPERTY: activity.from_property.id
+            if activity.from_property
+            else None,
             TelemetryConstants.CONVERSATION_NAME_PROPERTY: activity.conversation.name,
             TelemetryConstants.LOCALE_PROPERTY: activity.locale,
             TelemetryConstants.RECIPIENT_ID_PROPERTY: activity.recipient.id,
-            TelemetryConstants.RECIPIENT_NAME_PROPERTY: activity.from_property.name,
+            TelemetryConstants.RECIPIENT_NAME_PROPERTY: activity.recipient.name,
         }
 
         if self.log_personal_information:
-            if activity.from_property.name and activity.from_property.name.strip():
+            if (
+                activity.from_property
+                and activity.from_property.name
+                and activity.from_property.name.strip()
+            ):
                 properties[
                     TelemetryConstants.FROM_NAME_PROPERTY
                 ] = activity.from_property.name
@@ -177,6 +185,10 @@ async def fill_receive_event_properties(
             if activity.speak and activity.speak.strip():
                 properties[TelemetryConstants.SPEAK_PROPERTY] = activity.speak
 
+        TelemetryLoggerMiddleware.__populate_additional_channel_properties(
+            activity, properties
+        )
+
         # Additional properties can override "stock" properties
         if additional_properties:
             for prop in additional_properties:
@@ -205,6 +217,10 @@ async def fill_send_event_properties(
 
         # Use the LogPersonalInformation flag to toggle logging PII data, text and user name are common examples
         if self.log_personal_information:
+            if activity.attachments and activity.attachments.strip():
+                properties[
+                    TelemetryConstants.ATTACHMENTS_PROPERTY
+                ] = activity.attachments
             if activity.from_property.name and activity.from_property.name.strip():
                 properties[
                     TelemetryConstants.FROM_NAME_PROPERTY
@@ -278,3 +294,25 @@ async def fill_delete_event_properties(
                 properties[prop.key] = prop.value
 
         return properties
+
+    @staticmethod
+    def __populate_additional_channel_properties(
+        activity: Activity, properties: dict,
+    ):
+        if activity.channel_id == Channels.ms_teams:
+            teams_channel_data: TeamsChannelData = activity.channel_data
+
+            properties["TeamsTenantId"] = (
+                teams_channel_data.tenant
+                if teams_channel_data and teams_channel_data.tenant
+                else ""
+            )
+
+            properties["TeamsUserAadObjectId"] = (
+                activity.from_property.aad_object_id if activity.from_property else ""
+            )
+
+            if teams_channel_data and teams_channel_data.team:
+                properties["TeamsTeamInfo"] = TeamInfo.serialize(
+                    teams_channel_data.team
+                )
diff --git a/libraries/botbuilder-core/botbuilder/core/transcript_logger.py b/libraries/botbuilder-core/botbuilder/core/transcript_logger.py
index ef3918145..bfd838f24 100644
--- a/libraries/botbuilder-core/botbuilder/core/transcript_logger.py
+++ b/libraries/botbuilder-core/botbuilder/core/transcript_logger.py
@@ -4,10 +4,18 @@
 
 import datetime
 import copy
+import random
+import string
 from queue import Queue
 from abc import ABC, abstractmethod
 from typing import Awaitable, Callable, List
-from botbuilder.schema import Activity, ActivityTypes, ConversationReference
+from botbuilder.schema import (
+    Activity,
+    ActivityEventNames,
+    ActivityTypes,
+    ChannelAccount,
+    ConversationReference,
+)
 from .middleware_set import Middleware
 from .turn_context import TurnContext
 
@@ -44,9 +52,17 @@ async def on_turn(
         activity = context.activity
         # Log incoming activity at beginning of turn
         if activity:
+            if not activity.from_property:
+                activity.from_property = ChannelAccount()
             if not activity.from_property.role:
                 activity.from_property.role = "user"
-            self.log_activity(transcript, copy.copy(activity))
+
+            # We should not log ContinueConversation events used by skills to initialize the middleware.
+            if not (
+                context.activity.type == ActivityTypes.event
+                and context.activity.name == ActivityEventNames.continue_conversation
+            ):
+                await self.log_activity(transcript, copy.copy(activity))
 
         # hook up onSend pipeline
         # pylint: disable=unused-argument
@@ -57,8 +73,27 @@ async def send_activities_handler(
         ):
             # Run full pipeline
             responses = await next_send()
-            for activity in activities:
-                self.log_activity(transcript, copy.copy(activity))
+            for index, activity in enumerate(activities):
+                cloned_activity = copy.copy(activity)
+                if responses and index < len(responses):
+                    cloned_activity.id = responses[index].id
+
+                # For certain channels, a ResourceResponse with an id is not always sent to the bot.
+                # This fix uses the timestamp on the activity to populate its id for logging the transcript
+                # If there is no outgoing timestamp, the current time for the bot is used for the activity.id
+                if not cloned_activity.id:
+                    alphanumeric = string.ascii_lowercase + string.digits
+                    prefix = "g_" + "".join(
+                        random.choice(alphanumeric) for i in range(5)
+                    )
+                    epoch = datetime.datetime.utcfromtimestamp(0)
+                    if cloned_activity.timestamp:
+                        reference = cloned_activity.timestamp
+                    else:
+                        reference = datetime.datetime.today()
+                    delta = (reference - epoch).total_seconds() * 1000
+                    cloned_activity.id = f"{prefix}{delta}"
+                await self.log_activity(transcript, cloned_activity)
             return responses
 
         context.on_send_activities(send_activities_handler)
@@ -71,7 +106,7 @@ async def update_activity_handler(
             response = await next_update()
             update_activity = copy.copy(activity)
             update_activity.type = ActivityTypes.message_update
-            self.log_activity(transcript, update_activity)
+            await self.log_activity(transcript, update_activity)
             return response
 
         context.on_update_activity(update_activity_handler)
@@ -91,7 +126,7 @@ async def delete_activity_handler(
             deleted_activity: Activity = TurnContext.apply_conversation_reference(
                 delete_msg, reference, False
             )
-            self.log_activity(transcript, deleted_activity)
+            await self.log_activity(transcript, deleted_activity)
 
         context.on_delete_activity(delete_activity_handler)
 
@@ -106,7 +141,7 @@ async def delete_activity_handler(
             await self.logger.log_activity(activity)
             transcript.task_done()
 
-    def log_activity(self, transcript: Queue, activity: Activity) -> None:
+    async def log_activity(self, transcript: Queue, activity: Activity) -> None:
         """Logs the activity.
         :param transcript: transcript.
         :param activity: Activity to log.
diff --git a/libraries/botbuilder-core/botbuilder/core/turn_context.py b/libraries/botbuilder-core/botbuilder/core/turn_context.py
index 99d53996a..b8799a02b 100644
--- a/libraries/botbuilder-core/botbuilder/core/turn_context.py
+++ b/libraries/botbuilder-core/botbuilder/core/turn_context.py
@@ -1,334 +1,413 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import re
-from copy import copy
-from typing import List, Callable, Union, Dict
-from botbuilder.schema import Activity, ConversationReference, Mention, ResourceResponse
-
-
-class TurnContext:
-    def __init__(self, adapter_or_context, request: Activity = None):
-        """
-        Creates a new TurnContext instance.
-        :param adapter_or_context:
-        :param request:
-        """
-        if isinstance(adapter_or_context, TurnContext):
-            adapter_or_context.copy_to(self)
-        else:
-            self.adapter = adapter_or_context
-            self._activity = request
-            self.responses: List[Activity] = []
-            self._services: dict = {}
-            self._on_send_activities: Callable[
-                ["TurnContext", List[Activity], Callable], List[ResourceResponse]
-            ] = []
-            self._on_update_activity: Callable[
-                ["TurnContext", Activity, Callable], ResourceResponse
-            ] = []
-            self._on_delete_activity: Callable[
-                ["TurnContext", ConversationReference, Callable], None
-            ] = []
-            self._responded: bool = False
-
-        if self.adapter is None:
-            raise TypeError("TurnContext must be instantiated with an adapter.")
-        if self.activity is None:
-            raise TypeError(
-                "TurnContext must be instantiated with a request parameter of type Activity."
-            )
-
-        self._turn_state = {}
-
-    @property
-    def turn_state(self) -> Dict[str, object]:
-        return self._turn_state
-
-    def copy_to(self, context: "TurnContext") -> None:
-        """
-        Called when this TurnContext instance is passed into the constructor of a new TurnContext
-        instance. Can be overridden in derived classes.
-        :param context:
-        :return:
-        """
-        for attribute in [
-            "adapter",
-            "activity",
-            "_responded",
-            "_services",
-            "_on_send_activities",
-            "_on_update_activity",
-            "_on_delete_activity",
-        ]:
-            setattr(context, attribute, getattr(self, attribute))
-
-    @property
-    def activity(self):
-        """
-        The received activity.
-        :return:
-        """
-        return self._activity
-
-    @activity.setter
-    def activity(self, value):
-        """
-        Used to set TurnContext._activity when a context object is created. Only takes instances of Activities.
-        :param value:
-        :return:
-        """
-        if not isinstance(value, Activity):
-            raise TypeError(
-                "TurnContext: cannot set `activity` to a type other than Activity."
-            )
-        self._activity = value
-
-    @property
-    def responded(self) -> bool:
-        """
-        If `true` at least one response has been sent for the current turn of conversation.
-        :return:
-        """
-        return self._responded
-
-    @responded.setter
-    def responded(self, value: bool):
-        if not value:
-            raise ValueError("TurnContext: cannot set TurnContext.responded to False.")
-        self._responded = True
-
-    @property
-    def services(self):
-        """
-        Map of services and other values cached for the lifetime of the turn.
-        :return:
-        """
-        return self._services
-
-    def get(self, key: str) -> object:
-        if not key or not isinstance(key, str):
-            raise TypeError('"key" must be a valid string.')
-        try:
-            return self._services[key]
-        except KeyError:
-            raise KeyError("%s not found in TurnContext._services." % key)
-
-    def has(self, key: str) -> bool:
-        """
-        Returns True is set() has been called for a key. The cached value may be of type 'None'.
-        :param key:
-        :return:
-        """
-        if key in self._services:
-            return True
-        return False
-
-    def set(self, key: str, value: object) -> None:
-        """
-        Caches a value for the lifetime of the current turn.
-        :param key:
-        :param value:
-        :return:
-        """
-        if not key or not isinstance(key, str):
-            raise KeyError('"key" must be a valid string.')
-
-        self._services[key] = value
-
-    async def send_activity(
-        self, *activity_or_text: Union[Activity, str]
-    ) -> ResourceResponse:
-        """
-        Sends a single activity or message to the user.
-        :param activity_or_text:
-        :return:
-        """
-        reference = TurnContext.get_conversation_reference(self.activity)
-
-        output = [
-            TurnContext.apply_conversation_reference(
-                Activity(text=a, type="message") if isinstance(a, str) else a, reference
-            )
-            for a in activity_or_text
-        ]
-        for activity in output:
-            if not activity.input_hint:
-                activity.input_hint = "acceptingInput"
-
-        async def callback(context: "TurnContext", output):
-            responses = await context.adapter.send_activities(context, output)
-            context._responded = True  # pylint: disable=protected-access
-            return responses
-
-        result = await self._emit(
-            self._on_send_activities, output, callback(self, output)
-        )
-
-        return result[0] if result else ResourceResponse()
-
-    async def update_activity(self, activity: Activity):
-        """
-        Replaces an existing activity.
-        :param activity:
-        :return:
-        """
-        return await self._emit(
-            self._on_update_activity,
-            activity,
-            self.adapter.update_activity(self, activity),
-        )
-
-    async def delete_activity(self, id_or_reference: Union[str, ConversationReference]):
-        """
-        Deletes an existing activity.
-        :param id_or_reference:
-        :return:
-        """
-        if isinstance(id_or_reference, str):
-            reference = TurnContext.get_conversation_reference(self.activity)
-            reference.activity_id = id_or_reference
-        else:
-            reference = id_or_reference
-        return await self._emit(
-            self._on_delete_activity,
-            reference,
-            self.adapter.delete_activity(self, reference),
-        )
-
-    def on_send_activities(self, handler) -> "TurnContext":
-        """
-        Registers a handler to be notified of and potentially intercept the sending of activities.
-        :param handler:
-        :return:
-        """
-        self._on_send_activities.append(handler)
-        return self
-
-    def on_update_activity(self, handler) -> "TurnContext":
-        """
-        Registers a handler to be notified of and potentially intercept an activity being updated.
-        :param handler:
-        :return:
-        """
-        self._on_update_activity.append(handler)
-        return self
-
-    def on_delete_activity(self, handler) -> "TurnContext":
-        """
-        Registers a handler to be notified of and potentially intercept an activity being deleted.
-        :param handler:
-        :return:
-        """
-        self._on_delete_activity.append(handler)
-        return self
-
-    async def _emit(self, plugins, arg, logic):
-        handlers = copy(plugins)
-
-        async def emit_next(i: int):
-            context = self
-            try:
-                if i < len(handlers):
-
-                    async def next_handler():
-                        await emit_next(i + 1)
-
-                    await handlers[i](context, arg, next_handler)
-
-            except Exception as error:
-                raise error
-
-        await emit_next(0)
-        # This should be changed to `return await logic()`
-        return await logic
-
-    @staticmethod
-    def get_conversation_reference(activity: Activity) -> ConversationReference:
-        """
-        Returns the conversation reference for an activity. This can be saved as a plain old JSON
-        object and then later used to message the user proactively.
-
-        Usage Example:
-        reference = TurnContext.get_conversation_reference(context.request)
-        :param activity:
-        :return:
-        """
-        return ConversationReference(
-            activity_id=activity.id,
-            user=copy(activity.from_property),
-            bot=copy(activity.recipient),
-            conversation=copy(activity.conversation),
-            channel_id=activity.channel_id,
-            service_url=activity.service_url,
-        )
-
-    @staticmethod
-    def apply_conversation_reference(
-        activity: Activity, reference: ConversationReference, is_incoming: bool = False
-    ) -> Activity:
-        """
-        Updates an activity with the delivery information from a conversation reference. Calling
-        this after get_conversation_reference on an incoming activity
-        will properly address the reply to a received activity.
-        :param activity:
-        :param reference:
-        :param is_incoming:
-        :return:
-        """
-        activity.channel_id = reference.channel_id
-        activity.service_url = reference.service_url
-        activity.conversation = reference.conversation
-        if is_incoming:
-            activity.from_property = reference.user
-            activity.recipient = reference.bot
-            if reference.activity_id:
-                activity.id = reference.activity_id
-        else:
-            activity.from_property = reference.bot
-            activity.recipient = reference.user
-            if reference.activity_id:
-                activity.reply_to_id = reference.activity_id
-
-        return activity
-
-    @staticmethod
-    def get_reply_conversation_reference(
-        activity: Activity, reply: ResourceResponse
-    ) -> ConversationReference:
-        reference: ConversationReference = TurnContext.get_conversation_reference(
-            activity
-        )
-
-        # Update the reference with the new outgoing Activity's id.
-        reference.activity_id = reply.id
-
-        return reference
-
-    @staticmethod
-    def remove_recipient_mention(activity: Activity) -> str:
-        return TurnContext.remove_mention_text(activity, activity.recipient.id)
-
-    @staticmethod
-    def remove_mention_text(activity: Activity, identifier: str) -> str:
-        mentions = TurnContext.get_mentions(activity)
-        for mention in mentions:
-            if mention.mentioned.id == identifier:
-                mention_name_match = re.match(
-                    r"(.*?)<\/at>", mention.text, re.IGNORECASE
-                )
-                if mention_name_match:
-                    activity.text = re.sub(
-                        mention_name_match.groups()[1], "", activity.text
-                    )
-                    activity.text = re.sub(r"<\/at>", "", activity.text)
-        return activity.text
-
-    @staticmethod
-    def get_mentions(activity: Activity) -> List[Mention]:
-        result: List[Mention] = []
-        if activity.entities is not None:
-            for entity in activity.entities:
-                if entity.type.lower() == "mention":
-                    result.append(entity)
-        return result
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import re
+from copy import copy, deepcopy
+from datetime import datetime
+from typing import List, Callable, Union, Dict
+from botbuilder.schema import (
+    Activity,
+    ActivityTypes,
+    ConversationReference,
+    InputHints,
+    Mention,
+    ResourceResponse,
+    DeliveryModes,
+)
+from .re_escape import escape
+
+
+class TurnContext:
+
+    # Same constant as in the BF Adapter, duplicating here to avoid circular dependency
+    _INVOKE_RESPONSE_KEY = "BotFrameworkAdapter.InvokeResponse"
+
+    def __init__(self, adapter_or_context, request: Activity = None):
+        """
+        Creates a new TurnContext instance.
+        :param adapter_or_context:
+        :param request:
+        """
+        if isinstance(adapter_or_context, TurnContext):
+            adapter_or_context.copy_to(self)
+        else:
+            self.adapter = adapter_or_context
+            self._activity = request
+            self.responses: List[Activity] = []
+            self._services: dict = {}
+            self._on_send_activities: Callable[
+                ["TurnContext", List[Activity], Callable], List[ResourceResponse]
+            ] = []
+            self._on_update_activity: Callable[
+                ["TurnContext", Activity, Callable], ResourceResponse
+            ] = []
+            self._on_delete_activity: Callable[
+                ["TurnContext", ConversationReference, Callable], None
+            ] = []
+            self._responded: bool = False
+
+        if self.adapter is None:
+            raise TypeError("TurnContext must be instantiated with an adapter.")
+        if self.activity is None:
+            raise TypeError(
+                "TurnContext must be instantiated with a request parameter of type Activity."
+            )
+
+        self._turn_state = {}
+
+        # A list of activities to send when `context.Activity.DeliveryMode == 'expectReplies'`
+        self.buffered_reply_activities = []
+
+    @property
+    def turn_state(self) -> Dict[str, object]:
+        return self._turn_state
+
+    def copy_to(self, context: "TurnContext") -> None:
+        """
+        Called when this TurnContext instance is passed into the constructor of a new TurnContext
+        instance. Can be overridden in derived classes.
+        :param context:
+        :return:
+        """
+        for attribute in [
+            "adapter",
+            "activity",
+            "_responded",
+            "_services",
+            "_on_send_activities",
+            "_on_update_activity",
+            "_on_delete_activity",
+        ]:
+            setattr(context, attribute, getattr(self, attribute))
+
+    @property
+    def activity(self):
+        """
+        The received activity.
+        :return:
+        """
+        return self._activity
+
+    @activity.setter
+    def activity(self, value):
+        """
+        Used to set TurnContext._activity when a context object is created. Only takes instances of Activities.
+        :param value:
+        :return:
+        """
+        if not isinstance(value, Activity):
+            raise TypeError(
+                "TurnContext: cannot set `activity` to a type other than Activity."
+            )
+        self._activity = value
+
+    @property
+    def responded(self) -> bool:
+        """
+        If `true` at least one response has been sent for the current turn of conversation.
+        :return:
+        """
+        return self._responded
+
+    @responded.setter
+    def responded(self, value: bool):
+        if not value:
+            raise ValueError("TurnContext: cannot set TurnContext.responded to False.")
+        self._responded = True
+
+    @property
+    def services(self):
+        """
+        Map of services and other values cached for the lifetime of the turn.
+        :return:
+        """
+        return self._services
+
+    def get(self, key: str) -> object:
+        if not key or not isinstance(key, str):
+            raise TypeError('"key" must be a valid string.')
+        try:
+            return self._services[key]
+        except KeyError:
+            raise KeyError("%s not found in TurnContext._services." % key)
+
+    def has(self, key: str) -> bool:
+        """
+        Returns True is set() has been called for a key. The cached value may be of type 'None'.
+        :param key:
+        :return:
+        """
+        if key in self._services:
+            return True
+        return False
+
+    def set(self, key: str, value: object) -> None:
+        """
+        Caches a value for the lifetime of the current turn.
+        :param key:
+        :param value:
+        :return:
+        """
+        if not key or not isinstance(key, str):
+            raise KeyError('"key" must be a valid string.')
+
+        self._services[key] = value
+
+    async def send_activity(
+        self,
+        activity_or_text: Union[Activity, str],
+        speak: str = None,
+        input_hint: str = None,
+    ) -> ResourceResponse:
+        """
+        Sends a single activity or message to the user.
+        :param activity_or_text:
+        :return:
+        """
+        if isinstance(activity_or_text, str):
+            activity_or_text = Activity(
+                text=activity_or_text,
+                input_hint=input_hint or InputHints.accepting_input,
+                speak=speak,
+            )
+
+        result = await self.send_activities([activity_or_text])
+        return result[0] if result else None
+
+    async def send_activities(
+        self, activities: List[Activity]
+    ) -> List[ResourceResponse]:
+        sent_non_trace_activity = False
+        ref = TurnContext.get_conversation_reference(self.activity)
+
+        def activity_validator(activity: Activity) -> Activity:
+            if not getattr(activity, "type", None):
+                activity.type = ActivityTypes.message
+            if activity.type != ActivityTypes.trace:
+                nonlocal sent_non_trace_activity
+                sent_non_trace_activity = True
+            if not activity.input_hint:
+                activity.input_hint = "acceptingInput"
+            activity.id = None
+            return activity
+
+        output = [
+            activity_validator(
+                TurnContext.apply_conversation_reference(deepcopy(act), ref)
+            )
+            for act in activities
+        ]
+
+        # send activities through adapter
+        async def logic():
+            nonlocal sent_non_trace_activity
+
+            if self.activity.delivery_mode == DeliveryModes.expect_replies:
+                responses = []
+                for activity in output:
+                    self.buffered_reply_activities.append(activity)
+                    # Ensure the TurnState has the InvokeResponseKey, since this activity
+                    # is not being sent through the adapter, where it would be added to TurnState.
+                    if activity.type == ActivityTypes.invoke_response:
+                        self.turn_state[TurnContext._INVOKE_RESPONSE_KEY] = activity
+
+                    responses.append(ResourceResponse())
+
+                if sent_non_trace_activity:
+                    self.responded = True
+
+                return responses
+
+            responses = await self.adapter.send_activities(self, output)
+            if sent_non_trace_activity:
+                self.responded = True
+            return responses
+
+        return await self._emit(self._on_send_activities, output, logic())
+
+    async def update_activity(self, activity: Activity):
+        """
+        Replaces an existing activity.
+        :param activity:
+        :return:
+        """
+        reference = TurnContext.get_conversation_reference(self.activity)
+
+        return await self._emit(
+            self._on_update_activity,
+            TurnContext.apply_conversation_reference(activity, reference),
+            self.adapter.update_activity(self, activity),
+        )
+
+    async def delete_activity(self, id_or_reference: Union[str, ConversationReference]):
+        """
+        Deletes an existing activity.
+        :param id_or_reference:
+        :return:
+        """
+        if isinstance(id_or_reference, str):
+            reference = TurnContext.get_conversation_reference(self.activity)
+            reference.activity_id = id_or_reference
+        else:
+            reference = id_or_reference
+        return await self._emit(
+            self._on_delete_activity,
+            reference,
+            self.adapter.delete_activity(self, reference),
+        )
+
+    def on_send_activities(self, handler) -> "TurnContext":
+        """
+        Registers a handler to be notified of and potentially intercept the sending of activities.
+        :param handler:
+        :return:
+        """
+        self._on_send_activities.append(handler)
+        return self
+
+    def on_update_activity(self, handler) -> "TurnContext":
+        """
+        Registers a handler to be notified of and potentially intercept an activity being updated.
+        :param handler:
+        :return:
+        """
+        self._on_update_activity.append(handler)
+        return self
+
+    def on_delete_activity(self, handler) -> "TurnContext":
+        """
+        Registers a handler to be notified of and potentially intercept an activity being deleted.
+        :param handler:
+        :return:
+        """
+        self._on_delete_activity.append(handler)
+        return self
+
+    async def _emit(self, plugins, arg, logic):
+        handlers = copy(plugins)
+
+        async def emit_next(i: int):
+            context = self
+            try:
+                if i < len(handlers):
+
+                    async def next_handler():
+                        await emit_next(i + 1)
+
+                    await handlers[i](context, arg, next_handler)
+
+            except Exception as error:
+                raise error
+
+        await emit_next(0)
+        # logic does not use parentheses because it's a coroutine
+        return await logic
+
+    async def send_trace_activity(
+        self, name: str, value: object = None, value_type: str = None, label: str = None
+    ) -> ResourceResponse:
+        trace_activity = Activity(
+            type=ActivityTypes.trace,
+            timestamp=datetime.utcnow(),
+            name=name,
+            value=value,
+            value_type=value_type,
+            label=label,
+        )
+
+        return await self.send_activity(trace_activity)
+
+    @staticmethod
+    def get_conversation_reference(activity: Activity) -> ConversationReference:
+        """
+        Returns the conversation reference for an activity. This can be saved as a plain old JSON
+        object and then later used to message the user proactively.
+
+        Usage Example:
+        reference = TurnContext.get_conversation_reference(context.request)
+        :param activity:
+        :return:
+        """
+        return ConversationReference(
+            activity_id=activity.id,
+            user=copy(activity.from_property),
+            bot=copy(activity.recipient),
+            conversation=copy(activity.conversation),
+            channel_id=activity.channel_id,
+            locale=activity.locale,
+            service_url=activity.service_url,
+        )
+
+    @staticmethod
+    def apply_conversation_reference(
+        activity: Activity, reference: ConversationReference, is_incoming: bool = False
+    ) -> Activity:
+        """
+        Updates an activity with the delivery information from a conversation reference. Calling
+        this after get_conversation_reference on an incoming activity
+        will properly address the reply to a received activity.
+        :param activity:
+        :param reference:
+        :param is_incoming:
+        :return:
+        """
+        activity.channel_id = reference.channel_id
+        activity.locale = reference.locale
+        activity.service_url = reference.service_url
+        activity.conversation = reference.conversation
+        if is_incoming:
+            activity.from_property = reference.user
+            activity.recipient = reference.bot
+            if reference.activity_id:
+                activity.id = reference.activity_id
+        else:
+            activity.from_property = reference.bot
+            activity.recipient = reference.user
+            if reference.activity_id:
+                activity.reply_to_id = reference.activity_id
+
+        return activity
+
+    @staticmethod
+    def get_reply_conversation_reference(
+        activity: Activity, reply: ResourceResponse
+    ) -> ConversationReference:
+        reference: ConversationReference = TurnContext.get_conversation_reference(
+            activity
+        )
+
+        # Update the reference with the new outgoing Activity's id.
+        reference.activity_id = reply.id
+
+        return reference
+
+    @staticmethod
+    def remove_recipient_mention(activity: Activity) -> str:
+        return TurnContext.remove_mention_text(activity, activity.recipient.id)
+
+    @staticmethod
+    def remove_mention_text(activity: Activity, identifier: str) -> str:
+        mentions = TurnContext.get_mentions(activity)
+        for mention in mentions:
+            if mention.additional_properties["mentioned"]["id"] == identifier:
+                mention_name_match = re.match(
+                    r"(.*?)<\/at>",
+                    escape(mention.additional_properties["text"]),
+                    re.IGNORECASE,
+                )
+                if mention_name_match:
+                    activity.text = re.sub(
+                        mention_name_match.groups()[1], "", activity.text
+                    )
+                    activity.text = re.sub(r"<\/at>", "", activity.text)
+        return activity.text
+
+    @staticmethod
+    def get_mentions(activity: Activity) -> List[Mention]:
+        result: List[Mention] = []
+        if activity.entities is not None:
+            for entity in activity.entities:
+                if entity.type.lower() == "mention":
+                    result.append(entity)
+
+        return result
diff --git a/libraries/botbuilder-core/botbuilder/core/user_state.py b/libraries/botbuilder-core/botbuilder/core/user_state.py
index ab4b3f676..7cd23f8b1 100644
--- a/libraries/botbuilder-core/botbuilder/core/user_state.py
+++ b/libraries/botbuilder-core/botbuilder/core/user_state.py
@@ -23,7 +23,7 @@ def __init__(self, storage: Storage, namespace=""):
         """
         self.namespace = namespace
 
-        super(UserState, self).__init__(storage, "UserState")
+        super(UserState, self).__init__(storage, "Internal.UserState")
 
     def get_storage_key(self, turn_context: TurnContext) -> str:
         """
diff --git a/libraries/botbuilder-core/botbuilder/core/user_token_provider.py b/libraries/botbuilder-core/botbuilder/core/user_token_provider.py
deleted file mode 100644
index 4316a2f88..000000000
--- a/libraries/botbuilder-core/botbuilder/core/user_token_provider.py
+++ /dev/null
@@ -1,62 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from abc import ABC, abstractmethod
-from typing import Dict, List
-
-from botbuilder.schema import TokenResponse
-
-from .turn_context import TurnContext
-
-
-class UserTokenProvider(ABC):
-    @abstractmethod
-    async def get_user_token(
-        self, context: TurnContext, connection_name: str, magic_code: str = None
-    ) -> TokenResponse:
-        """
-        Retrieves the OAuth token for a user that is in a sign-in flow.
-        :param context:
-        :param connection_name:
-        :param magic_code:
-        :return:
-        """
-        raise NotImplementedError()
-
-    @abstractmethod
-    async def sign_out_user(
-        self, context: TurnContext, connection_name: str, user_id: str = None
-    ):
-        """
-        Signs the user out with the token server.
-        :param context:
-        :param connection_name:
-        :param user_id:
-        :return:
-        """
-        raise NotImplementedError()
-
-    @abstractmethod
-    async def get_oauth_sign_in_link(
-        self, context: TurnContext, connection_name: str
-    ) -> str:
-        """
-        Get the raw signin link to be sent to the user for signin for a connection name.
-        :param context:
-        :param connection_name:
-        :return:
-        """
-        raise NotImplementedError()
-
-    @abstractmethod
-    async def get_aad_tokens(
-        self, context: TurnContext, connection_name: str, resource_urls: List[str]
-    ) -> Dict[str, TokenResponse]:
-        """
-        Retrieves Azure Active Directory tokens for particular resources on a configured connection.
-        :param context:
-        :param connection_name:
-        :param resource_urls:
-        :return:
-        """
-        raise NotImplementedError()
diff --git a/libraries/botbuilder-core/requirements.txt b/libraries/botbuilder-core/requirements.txt
index 76ed88e0a..04934948c 100644
--- a/libraries/botbuilder-core/requirements.txt
+++ b/libraries/botbuilder-core/requirements.txt
@@ -1,7 +1,7 @@
-msrest>=0.6.6
-botframework-connector>=4.4.0b1
-botbuilder-schema>=4.4.0b1
-requests>=2.18.1
+msrest==0.6.10
+botframework-connector==4.12.0
+botbuilder-schema==4.12.0
+requests==2.23.0
 PyJWT==1.5.3
-cryptography>=2.3.0
-aiounittest>=1.2.1
\ No newline at end of file
+cryptography==3.2
+aiounittest==1.3.0
\ No newline at end of file
diff --git a/libraries/botbuilder-core/setup.py b/libraries/botbuilder-core/setup.py
index b3dff50fa..5a144f90b 100644
--- a/libraries/botbuilder-core/setup.py
+++ b/libraries/botbuilder-core/setup.py
@@ -4,11 +4,11 @@
 import os
 from setuptools import setup
 
-VERSION = os.environ["packageVersion"] if "packageVersion" in os.environ else "4.4.0b1"
+VERSION = os.environ["packageVersion"] if "packageVersion" in os.environ else "4.12.0"
 REQUIRES = [
-    "botbuilder-schema>=4.4.0b1",
-    "botframework-connector>=4.4.0b1",
-    "jsonpickle>=1.2",
+    "botbuilder-schema==4.12.0",
+    "botframework-connector==4.12.0",
+    "jsonpickle==1.2",
 ]
 
 root = os.path.abspath(os.path.dirname(__file__))
@@ -35,6 +35,10 @@
         "botbuilder.core",
         "botbuilder.core.adapters",
         "botbuilder.core.inspection",
+        "botbuilder.core.integration",
+        "botbuilder.core.skills",
+        "botbuilder.core.teams",
+        "botbuilder.core.oauth",
     ],
     install_requires=REQUIRES,
     classifiers=[
@@ -42,7 +46,7 @@
         "Intended Audience :: Developers",
         "License :: OSI Approved :: MIT License",
         "Operating System :: OS Independent",
-        "Development Status :: 3 - Alpha",
+        "Development Status :: 5 - Production/Stable",
         "Topic :: Scientific/Engineering :: Artificial Intelligence",
     ],
 )
diff --git a/libraries/botbuilder-core/tests/simple_adapter.py b/libraries/botbuilder-core/tests/simple_adapter.py
index 63f575a82..b8dd3c404 100644
--- a/libraries/botbuilder-core/tests/simple_adapter.py
+++ b/libraries/botbuilder-core/tests/simple_adapter.py
@@ -1,56 +1,97 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import unittest
-from typing import List
-from botbuilder.core import BotAdapter, TurnContext
-from botbuilder.schema import Activity, ConversationReference, ResourceResponse
-
-
-class SimpleAdapter(BotAdapter):
-    def __init__(self, call_on_send=None, call_on_update=None, call_on_delete=None):
-        super(SimpleAdapter, self).__init__()
-        self.test_aux = unittest.TestCase("__init__")
-        self._call_on_send = call_on_send
-        self._call_on_update = call_on_update
-        self._call_on_delete = call_on_delete
-
-    async def delete_activity(
-        self, context: TurnContext, reference: ConversationReference
-    ):
-        self.test_aux.assertIsNotNone(
-            reference, "SimpleAdapter.delete_activity: missing reference"
-        )
-        if self._call_on_delete is not None:
-            self._call_on_delete(reference)
-
-    async def send_activities(self, context: TurnContext, activities: List[Activity]):
-        self.test_aux.assertIsNotNone(
-            activities, "SimpleAdapter.delete_activity: missing reference"
-        )
-        self.test_aux.assertTrue(
-            len(activities) > 0,
-            "SimpleAdapter.send_activities: empty activities array.",
-        )
-
-        if self._call_on_send is not None:
-            self._call_on_send(activities)
-        responses = []
-
-        for activity in activities:
-            responses.append(ResourceResponse(id=activity.id))
-
-        return responses
-
-    async def update_activity(self, context: TurnContext, activity: Activity):
-        self.test_aux.assertIsNotNone(
-            activity, "SimpleAdapter.update_activity: missing activity"
-        )
-        if self._call_on_update is not None:
-            self._call_on_update(activity)
-
-        return ResourceResponse(activity.id)
-
-    async def process_request(self, activity, handler):
-        context = TurnContext(self, activity)
-        return self.run_pipeline(context, handler)
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import unittest
+from typing import List, Tuple, Awaitable, Callable
+from botbuilder.core import BotAdapter, TurnContext
+from botbuilder.schema import (
+    Activity,
+    ConversationReference,
+    ResourceResponse,
+    ConversationParameters,
+)
+from botbuilder.schema.teams import TeamsChannelAccount
+
+
+class SimpleAdapter(BotAdapter):
+    # pylint: disable=unused-argument
+
+    def __init__(
+        self,
+        call_on_send=None,
+        call_on_update=None,
+        call_on_delete=None,
+        call_create_conversation=None,
+    ):
+        super(SimpleAdapter, self).__init__()
+        self.test_aux = unittest.TestCase("__init__")
+        self._call_on_send = call_on_send
+        self._call_on_update = call_on_update
+        self._call_on_delete = call_on_delete
+        self._call_create_conversation = call_create_conversation
+
+    async def delete_activity(
+        self, context: TurnContext, reference: ConversationReference
+    ):
+        self.test_aux.assertIsNotNone(
+            reference, "SimpleAdapter.delete_activity: missing reference"
+        )
+        if self._call_on_delete is not None:
+            self._call_on_delete(reference)
+
+    async def send_activities(
+        self, context: TurnContext, activities: List[Activity]
+    ) -> List[ResourceResponse]:
+        self.test_aux.assertIsNotNone(
+            activities, "SimpleAdapter.delete_activity: missing reference"
+        )
+        self.test_aux.assertTrue(
+            len(activities) > 0,
+            "SimpleAdapter.send_activities: empty activities array.",
+        )
+
+        if self._call_on_send is not None:
+            self._call_on_send(activities)
+        responses = []
+
+        for activity in activities:
+            responses.append(ResourceResponse(id=activity.id))
+
+        return responses
+
+    async def create_conversation(
+        self,
+        reference: ConversationReference,
+        logic: Callable[[TurnContext], Awaitable] = None,
+        conversation_parameters: ConversationParameters = None,
+    ) -> Tuple[ConversationReference, str]:
+        if self._call_create_conversation is not None:
+            self._call_create_conversation()
+
+    async def update_activity(self, context: TurnContext, activity: Activity):
+        self.test_aux.assertIsNotNone(
+            activity, "SimpleAdapter.update_activity: missing activity"
+        )
+        if self._call_on_update is not None:
+            self._call_on_update(activity)
+
+        return ResourceResponse(activity.id)
+
+    async def process_request(self, activity, handler):
+        context = TurnContext(self, activity)
+        return await self.run_pipeline(context, handler)
+
+    async def create_connector_client(self, service_url: str):
+        return TestConnectorClient()
+
+
+class TestConnectorClient:
+    def __init__(self) -> None:
+        self.conversations = TestConversations()
+
+
+class TestConversations:
+    async def get_conversation_member(  # pylint: disable=unused-argument
+        self, conversation_id, member_id
+    ):
+        return TeamsChannelAccount(id=member_id)
diff --git a/libraries/botbuilder-core/tests/skills/test_skill_handler.py b/libraries/botbuilder-core/tests/skills/test_skill_handler.py
new file mode 100644
index 000000000..73997cdff
--- /dev/null
+++ b/libraries/botbuilder-core/tests/skills/test_skill_handler.py
@@ -0,0 +1,629 @@
+import hashlib
+import json
+from datetime import datetime
+from uuid import uuid4
+from asyncio import Future
+from typing import Dict, List, Callable
+
+from unittest.mock import Mock, MagicMock
+import aiounittest
+
+from botframework.connector.auth import (
+    AuthenticationConfiguration,
+    AuthenticationConstants,
+    ClaimsIdentity,
+)
+from botbuilder.core import (
+    TurnContext,
+    BotActionNotImplementedError,
+    conversation_reference_extension,
+)
+from botbuilder.core.skills import ConversationIdFactoryBase, SkillHandler
+from botbuilder.schema import (
+    Activity,
+    ActivityTypes,
+    AttachmentData,
+    ChannelAccount,
+    ConversationAccount,
+    ConversationParameters,
+    ConversationsResult,
+    ConversationResourceResponse,
+    ConversationReference,
+    PagedMembersResult,
+    ResourceResponse,
+    Transcript,
+    CallerIdConstants,
+)
+
+
+class ConversationIdFactoryForTest(ConversationIdFactoryBase):
+    def __init__(self):
+        self._conversation_refs: Dict[str, str] = {}
+
+    async def create_skill_conversation_id(  # pylint: disable=W0221
+        self, conversation_reference: ConversationReference
+    ) -> str:
+        cr_json = json.dumps(conversation_reference.serialize())
+
+        key = hashlib.md5(
+            f"{conversation_reference.conversation.id}{conversation_reference.service_url}".encode()
+        ).hexdigest()
+
+        if key not in self._conversation_refs:
+            self._conversation_refs[key] = cr_json
+
+        return key
+
+    async def get_conversation_reference(
+        self, skill_conversation_id: str
+    ) -> ConversationReference:
+        conversation_reference = ConversationReference().deserialize(
+            json.loads(self._conversation_refs[skill_conversation_id])
+        )
+        return conversation_reference
+
+    async def delete_conversation_reference(self, skill_conversation_id: str):
+        pass
+
+
+class SkillHandlerInstanceForTests(SkillHandler):
+    async def test_on_get_conversations(
+        self, claims_identity: ClaimsIdentity, continuation_token: str = "",
+    ) -> ConversationsResult:
+        return await self.on_get_conversations(claims_identity, continuation_token)
+
+    async def test_on_create_conversation(
+        self, claims_identity: ClaimsIdentity, parameters: ConversationParameters,
+    ) -> ConversationResourceResponse:
+        return await self.on_create_conversation(claims_identity, parameters)
+
+    async def test_on_send_to_conversation(
+        self, claims_identity: ClaimsIdentity, conversation_id: str, activity: Activity,
+    ) -> ResourceResponse:
+        return await self.on_send_to_conversation(
+            claims_identity, conversation_id, activity
+        )
+
+    async def test_on_send_conversation_history(
+        self,
+        claims_identity: ClaimsIdentity,
+        conversation_id: str,
+        transcript: Transcript,
+    ) -> ResourceResponse:
+        return await self.on_send_conversation_history(
+            claims_identity, conversation_id, transcript
+        )
+
+    async def test_on_update_activity(
+        self,
+        claims_identity: ClaimsIdentity,
+        conversation_id: str,
+        activity_id: str,
+        activity: Activity,
+    ) -> ResourceResponse:
+        return await self.on_update_activity(
+            claims_identity, conversation_id, activity_id, activity
+        )
+
+    async def test_on_reply_to_activity(
+        self,
+        claims_identity: ClaimsIdentity,
+        conversation_id: str,
+        activity_id: str,
+        activity: Activity,
+    ) -> ResourceResponse:
+        return await self.on_reply_to_activity(
+            claims_identity, conversation_id, activity_id, activity
+        )
+
+    async def test_on_delete_activity(
+        self, claims_identity: ClaimsIdentity, conversation_id: str, activity_id: str,
+    ):
+        return await self.on_delete_activity(
+            claims_identity, conversation_id, activity_id
+        )
+
+    async def test_on_get_conversation_members(
+        self, claims_identity: ClaimsIdentity, conversation_id: str,
+    ) -> List[ChannelAccount]:
+        return await self.on_get_conversation_members(claims_identity, conversation_id)
+
+    async def test_on_get_conversation_paged_members(
+        self,
+        claims_identity: ClaimsIdentity,
+        conversation_id: str,
+        page_size: int = None,
+        continuation_token: str = "",
+    ) -> PagedMembersResult:
+        return await self.on_get_conversation_paged_members(
+            claims_identity, conversation_id, page_size, continuation_token
+        )
+
+    async def test_on_delete_conversation_member(
+        self, claims_identity: ClaimsIdentity, conversation_id: str, member_id: str,
+    ):
+        return await self.on_delete_conversation_member(
+            claims_identity, conversation_id, member_id
+        )
+
+    async def test_on_get_activity_members(
+        self, claims_identity: ClaimsIdentity, conversation_id: str, activity_id: str,
+    ) -> List[ChannelAccount]:
+        return await self.on_get_activity_members(
+            claims_identity, conversation_id, activity_id
+        )
+
+    async def test_on_upload_attachment(
+        self,
+        claims_identity: ClaimsIdentity,
+        conversation_id: str,
+        attachment_upload: AttachmentData,
+    ) -> ResourceResponse:
+        return await self.on_upload_attachment(
+            claims_identity, conversation_id, attachment_upload
+        )
+
+
+# pylint: disable=invalid-name
+# pylint: disable=attribute-defined-outside-init
+
+
+class TestSkillHandler(aiounittest.AsyncTestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.bot_id = str(uuid4())
+        cls.skill_id = str(uuid4())
+
+        cls._test_id_factory = ConversationIdFactoryForTest()
+
+        cls._claims_identity = ClaimsIdentity({}, False)
+
+        cls._claims_identity.claims[AuthenticationConstants.AUDIENCE_CLAIM] = cls.bot_id
+        cls._claims_identity.claims[AuthenticationConstants.APP_ID_CLAIM] = cls.skill_id
+        cls._claims_identity.claims[
+            AuthenticationConstants.SERVICE_URL_CLAIM
+        ] = "http://testbot.com/api/messages"
+        cls._conversation_reference = ConversationReference(
+            conversation=ConversationAccount(id=str(uuid4())),
+            service_url="http://testbot.com/api/messages",
+        )
+
+    def create_skill_handler_for_testing(self, adapter) -> SkillHandlerInstanceForTests:
+        mock_bot = Mock()
+        mock_bot.on_turn = MagicMock(return_value=Future())
+        mock_bot.on_turn.return_value.set_result(Mock())
+
+        return SkillHandlerInstanceForTests(
+            adapter,
+            mock_bot,
+            self._test_id_factory,
+            Mock(),
+            AuthenticationConfiguration(),
+        )
+
+    async def test_on_send_to_conversation(self):
+        self._conversation_id = await self._test_id_factory.create_skill_conversation_id(
+            self._conversation_reference
+        )
+        # python 3.7 doesn't support AsyncMock, change this when min ver is 3.8
+        send_activities_called = False
+
+        mock_adapter = Mock()
+
+        async def continue_conversation(
+            reference: ConversationReference,
+            callback: Callable,
+            bot_id: str = None,
+            claims_identity: ClaimsIdentity = None,
+            audience: str = None,
+        ):  # pylint: disable=unused-argument
+            # Invoke the callback created by the handler so we can assert the rest of the execution.
+            turn_context = TurnContext(
+                mock_adapter,
+                conversation_reference_extension.get_continuation_activity(
+                    self._conversation_reference
+                ),
+            )
+            await callback(turn_context)
+
+            # Assert the callback set the right properties.
+            assert (
+                f"{CallerIdConstants.bot_to_bot_prefix}{self.skill_id}"
+            ), turn_context.activity.caller_id
+
+        async def send_activities(
+            context: TurnContext, activities: List[Activity]
+        ):  # pylint: disable=unused-argument
+            # Messages should not have a caller id set when sent back to the caller.
+            nonlocal send_activities_called
+            assert activities[0].caller_id is None
+            assert activities[0].reply_to_id is None
+            send_activities_called = True
+            return [ResourceResponse(id="resourceId")]
+
+        mock_adapter.continue_conversation = continue_conversation
+        mock_adapter.send_activities = send_activities
+
+        types_to_test = [
+            ActivityTypes.end_of_conversation,
+            ActivityTypes.event,
+            ActivityTypes.message,
+        ]
+
+        for activity_type in types_to_test:
+            with self.subTest(act_type=activity_type):
+                send_activities_called = False
+                activity = Activity(type=activity_type, attachments=[], entities=[])
+                TurnContext.apply_conversation_reference(
+                    activity, self._conversation_reference
+                )
+                sut = self.create_skill_handler_for_testing(mock_adapter)
+
+                resource_response = await sut.test_on_send_to_conversation(
+                    self._claims_identity, self._conversation_id, activity
+                )
+
+                if activity_type == ActivityTypes.message:
+                    assert send_activities_called
+                    assert resource_response.id == "resourceId"
+
+    async def test_forwarding_on_send_to_conversation(self):
+        self._conversation_id = await self._test_id_factory.create_skill_conversation_id(
+            self._conversation_reference
+        )
+
+        resource_response_id = "rId"
+
+        async def side_effect(
+            *arg_list, **args_dict
+        ):  # pylint: disable=unused-argument
+            fake_context = Mock()
+            fake_context.turn_state = {}
+            fake_context.send_activity = MagicMock(return_value=Future())
+            fake_context.send_activity.return_value.set_result(
+                ResourceResponse(id=resource_response_id)
+            )
+            await arg_list[1](fake_context)
+
+        mock_adapter = Mock()
+        mock_adapter.continue_conversation = side_effect
+        mock_adapter.send_activities = MagicMock(return_value=Future())
+        mock_adapter.send_activities.return_value.set_result([])
+
+        sut = self.create_skill_handler_for_testing(mock_adapter)
+
+        activity = Activity(type=ActivityTypes.message, attachments=[], entities=[])
+        TurnContext.apply_conversation_reference(activity, self._conversation_reference)
+
+        assert not activity.caller_id
+
+        response = await sut.test_on_send_to_conversation(
+            self._claims_identity, self._conversation_id, activity
+        )
+
+        assert response.id is resource_response_id
+
+    async def test_on_reply_to_activity(self):
+        resource_response_id = "resourceId"
+        self._conversation_id = await self._test_id_factory.create_skill_conversation_id(
+            self._conversation_reference
+        )
+
+        types_to_test = [
+            ActivityTypes.end_of_conversation,
+            ActivityTypes.event,
+            ActivityTypes.message,
+        ]
+
+        for activity_type in types_to_test:
+            with self.subTest(act_type=activity_type):
+                mock_adapter = Mock()
+                mock_adapter.continue_conversation = MagicMock(return_value=Future())
+                mock_adapter.continue_conversation.return_value.set_result(Mock())
+                mock_adapter.send_activities = MagicMock(return_value=Future())
+                mock_adapter.send_activities.return_value.set_result(
+                    [ResourceResponse(id=resource_response_id)]
+                )
+
+                sut = self.create_skill_handler_for_testing(mock_adapter)
+
+                activity = Activity(type=activity_type, attachments=[], entities=[])
+                activity_id = str(uuid4())
+                TurnContext.apply_conversation_reference(
+                    activity, self._conversation_reference
+                )
+
+                resource_response = await sut.test_on_reply_to_activity(
+                    self._claims_identity, self._conversation_id, activity_id, activity
+                )
+
+                # continue_conversation validation
+                (
+                    args_continue,
+                    kwargs_continue,
+                ) = mock_adapter.continue_conversation.call_args_list[0]
+                mock_adapter.continue_conversation.assert_called_once()
+
+                assert isinstance(args_continue[0], ConversationReference)
+                assert callable(args_continue[1])
+                assert isinstance(kwargs_continue["claims_identity"], ClaimsIdentity)
+
+                turn_context = TurnContext(
+                    mock_adapter,
+                    conversation_reference_extension.get_continuation_activity(
+                        self._conversation_reference
+                    ),
+                )
+                await args_continue[1](turn_context)
+                # assert the callback set the right properties.
+                assert (
+                    f"{CallerIdConstants.bot_to_bot_prefix}{self.skill_id}"
+                ), turn_context.activity.caller_id
+
+                if activity_type == ActivityTypes.message:
+                    # send_activities validation
+                    (args_send, _,) = mock_adapter.send_activities.call_args_list[0]
+                    activity_from_send = args_send[1][0]
+                    assert activity_from_send.caller_id is None
+                    assert activity_from_send.reply_to_id, activity_id
+                    assert resource_response.id, resource_response_id
+                else:
+                    # Assert mock SendActivitiesAsync wasn't called
+                    mock_adapter.send_activities.assert_not_called()
+
+    async def test_on_update_activity(self):
+        self._conversation_id = await self._test_id_factory.create_skill_conversation_id(
+            self._conversation_reference
+        )
+        resource_response_id = "resourceId"
+        called_continue = False
+        called_update = False
+
+        mock_adapter = Mock()
+        activity = Activity(type=ActivityTypes.message, attachments=[], entities=[])
+        activity_id = str(uuid4())
+        message = activity.text = f"TestUpdate {datetime.now()}."
+
+        async def continue_conversation(
+            reference: ConversationReference,
+            callback: Callable,
+            bot_id: str = None,
+            claims_identity: ClaimsIdentity = None,
+            audience: str = None,
+        ):  # pylint: disable=unused-argument
+            # Invoke the callback created by the handler so we can assert the rest of the execution.
+            nonlocal called_continue
+            turn_context = TurnContext(
+                mock_adapter,
+                conversation_reference_extension.get_continuation_activity(
+                    self._conversation_reference
+                ),
+            )
+            await callback(turn_context)
+
+            # Assert the callback set the right properties.
+            assert (
+                f"{CallerIdConstants.bot_to_bot_prefix}{self.skill_id}"
+            ), turn_context.activity.caller_id
+            called_continue = True
+
+        async def update_activity(
+            context: TurnContext,  # pylint: disable=unused-argument
+            new_activity: Activity,
+        ) -> ResourceResponse:
+            # Assert the activity being sent.
+            nonlocal called_update
+            assert activity_id, new_activity.reply_to_id
+            assert message, new_activity.text
+            called_update = True
+
+            return ResourceResponse(id=resource_response_id)
+
+        mock_adapter.continue_conversation = continue_conversation
+        mock_adapter.update_activity = update_activity
+
+        sut = self.create_skill_handler_for_testing(mock_adapter)
+        resource_response = await sut.test_on_update_activity(
+            self._claims_identity, self._conversation_id, activity_id, activity
+        )
+
+        assert called_continue
+        assert called_update
+        assert resource_response, resource_response_id
+
+    async def test_on_delete_activity(self):
+        self._conversation_id = await self._test_id_factory.create_skill_conversation_id(
+            self._conversation_reference
+        )
+
+        resource_response_id = "resourceId"
+        called_continue = False
+        called_delete = False
+
+        mock_adapter = Mock()
+        activity_id = str(uuid4())
+
+        async def continue_conversation(
+            reference: ConversationReference,
+            callback: Callable,
+            bot_id: str = None,
+            claims_identity: ClaimsIdentity = None,
+            audience: str = None,
+        ):  # pylint: disable=unused-argument
+            # Invoke the callback created by the handler so we can assert the rest of the execution.
+            nonlocal called_continue
+            turn_context = TurnContext(
+                mock_adapter,
+                conversation_reference_extension.get_continuation_activity(
+                    self._conversation_reference
+                ),
+            )
+            await callback(turn_context)
+            called_continue = True
+
+        async def delete_activity(
+            context: TurnContext,  # pylint: disable=unused-argument
+            conversation_reference: ConversationReference,
+        ) -> ResourceResponse:
+            # Assert the activity being sent.
+            nonlocal called_delete
+            # Assert the activity_id being deleted.
+            assert activity_id, conversation_reference.activity_id
+            called_delete = True
+
+            return ResourceResponse(id=resource_response_id)
+
+        mock_adapter.continue_conversation = continue_conversation
+        mock_adapter.delete_activity = delete_activity
+
+        sut = self.create_skill_handler_for_testing(mock_adapter)
+
+        await sut.test_on_delete_activity(
+            self._claims_identity, self._conversation_id, activity_id
+        )
+
+        assert called_continue
+        assert called_delete
+
+    async def test_on_get_activity_members(self):
+        self._conversation_id = ""
+
+        mock_adapter = Mock()
+
+        sut = self.create_skill_handler_for_testing(mock_adapter)
+        activity_id = str(uuid4())
+
+        with self.assertRaises(BotActionNotImplementedError):
+            await sut.test_on_get_activity_members(
+                self._claims_identity, self._conversation_id, activity_id
+            )
+
+    async def test_on_create_conversation(self):
+        mock_adapter = Mock()
+
+        sut = self.create_skill_handler_for_testing(mock_adapter)
+        conversation_parameters = ConversationParameters()
+
+        with self.assertRaises(BotActionNotImplementedError):
+            await sut.test_on_create_conversation(
+                self._claims_identity, conversation_parameters
+            )
+
+    async def test_on_get_conversations(self):
+        self._conversation_id = ""
+
+        mock_adapter = Mock()
+
+        sut = self.create_skill_handler_for_testing(mock_adapter)
+
+        with self.assertRaises(BotActionNotImplementedError):
+            await sut.test_on_get_conversations(
+                self._claims_identity, self._conversation_id
+            )
+
+    async def test_on_get_conversation_members(self):
+        self._conversation_id = ""
+
+        mock_adapter = Mock()
+
+        sut = self.create_skill_handler_for_testing(mock_adapter)
+
+        with self.assertRaises(BotActionNotImplementedError):
+            await sut.test_on_get_conversation_members(
+                self._claims_identity, self._conversation_id
+            )
+
+    async def test_on_get_conversation_paged_members(self):
+        self._conversation_id = ""
+
+        mock_adapter = Mock()
+
+        sut = self.create_skill_handler_for_testing(mock_adapter)
+
+        with self.assertRaises(BotActionNotImplementedError):
+            await sut.test_on_get_conversation_paged_members(
+                self._claims_identity, self._conversation_id
+            )
+
+    async def test_on_delete_conversation_member(self):
+        self._conversation_id = ""
+
+        mock_adapter = Mock()
+
+        sut = self.create_skill_handler_for_testing(mock_adapter)
+        member_id = str(uuid4())
+
+        with self.assertRaises(BotActionNotImplementedError):
+            await sut.test_on_delete_conversation_member(
+                self._claims_identity, self._conversation_id, member_id
+            )
+
+    async def test_on_send_conversation_history(self):
+        self._conversation_id = ""
+
+        mock_adapter = Mock()
+
+        sut = self.create_skill_handler_for_testing(mock_adapter)
+        transcript = Transcript()
+
+        with self.assertRaises(BotActionNotImplementedError):
+            await sut.test_on_send_conversation_history(
+                self._claims_identity, self._conversation_id, transcript
+            )
+
+    async def test_on_upload_attachment(self):
+        self._conversation_id = ""
+
+        mock_adapter = Mock()
+
+        sut = self.create_skill_handler_for_testing(mock_adapter)
+        attachment_data = AttachmentData()
+
+        with self.assertRaises(BotActionNotImplementedError):
+            await sut.test_on_upload_attachment(
+                self._claims_identity, self._conversation_id, attachment_data
+            )
+
+    async def test_event_activity(self):
+        activity = Activity(type=ActivityTypes.event)
+        await self.__activity_callback_test(activity)
+        assert (
+            activity.caller_id
+            == f"{CallerIdConstants.bot_to_bot_prefix}{self.skill_id}"
+        )
+
+    async def test_eoc_activity(self):
+        activity = Activity(type=ActivityTypes.end_of_conversation)
+        await self.__activity_callback_test(activity)
+        assert (
+            activity.caller_id
+            == f"{CallerIdConstants.bot_to_bot_prefix}{self.skill_id}"
+        )
+
+    async def __activity_callback_test(self, activity: Activity):
+        self._conversation_id = await self._test_id_factory.create_skill_conversation_id(
+            self._conversation_reference
+        )
+
+        mock_adapter = Mock()
+        mock_adapter.continue_conversation = MagicMock(return_value=Future())
+        mock_adapter.continue_conversation.return_value.set_result(Mock())
+        mock_adapter.send_activities = MagicMock(return_value=Future())
+        mock_adapter.send_activities.return_value.set_result([])
+
+        sut = self.create_skill_handler_for_testing(mock_adapter)
+
+        activity_id = str(uuid4())
+        TurnContext.apply_conversation_reference(activity, self._conversation_reference)
+
+        await sut.test_on_reply_to_activity(
+            self._claims_identity, self._conversation_id, activity_id, activity
+        )
+
+        args, kwargs = mock_adapter.continue_conversation.call_args_list[0]
+
+        assert isinstance(args[0], ConversationReference)
+        assert callable(args[1])
+        assert isinstance(kwargs["claims_identity"], ClaimsIdentity)
+
+        await args[1](TurnContext(mock_adapter, activity))
diff --git a/libraries/botbuilder-core/tests/teams/simple_adapter_with_create_conversation.py b/libraries/botbuilder-core/tests/teams/simple_adapter_with_create_conversation.py
new file mode 100644
index 000000000..477aa3b28
--- /dev/null
+++ b/libraries/botbuilder-core/tests/teams/simple_adapter_with_create_conversation.py
@@ -0,0 +1,83 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import unittest
+from typing import List, Tuple, Awaitable, Callable
+from botbuilder.core import BotAdapter, TurnContext
+from botbuilder.schema import (
+    Activity,
+    ConversationReference,
+    ResourceResponse,
+    ConversationParameters,
+)
+
+
+class SimpleAdapterWithCreateConversation(BotAdapter):
+    # pylint: disable=unused-argument
+
+    def __init__(
+        self,
+        call_on_send=None,
+        call_on_update=None,
+        call_on_delete=None,
+        call_create_conversation=None,
+    ):
+        super(SimpleAdapterWithCreateConversation, self).__init__()
+        self.test_aux = unittest.TestCase("__init__")
+        self._call_on_send = call_on_send
+        self._call_on_update = call_on_update
+        self._call_on_delete = call_on_delete
+        self._call_create_conversation = call_create_conversation
+
+    async def delete_activity(
+        self, context: TurnContext, reference: ConversationReference
+    ):
+        self.test_aux.assertIsNotNone(
+            reference, "SimpleAdapter.delete_activity: missing reference"
+        )
+        if self._call_on_delete is not None:
+            self._call_on_delete(reference)
+
+    async def send_activities(
+        self, context: TurnContext, activities: List[Activity]
+    ) -> List[ResourceResponse]:
+        self.test_aux.assertIsNotNone(
+            activities, "SimpleAdapter.delete_activity: missing reference"
+        )
+        self.test_aux.assertTrue(
+            len(activities) > 0,
+            "SimpleAdapter.send_activities: empty activities array.",
+        )
+
+        if self._call_on_send is not None:
+            self._call_on_send(activities)
+        responses = []
+
+        for activity in activities:
+            responses.append(ResourceResponse(id=activity.id))
+
+        return responses
+
+    async def create_conversation(
+        self,
+        reference: ConversationReference,
+        logic: Callable[[TurnContext], Awaitable] = None,
+        conversation_parameters: ConversationParameters = None,
+    ) -> Tuple[ConversationReference, str]:
+        if self._call_create_conversation is not None:
+            self._call_create_conversation()
+        ref = ConversationReference(activity_id="new_conversation_id")
+        return (ref, "reference123")
+
+    async def update_activity(self, context: TurnContext, activity: Activity):
+        self.test_aux.assertIsNotNone(
+            activity, "SimpleAdapter.update_activity: missing activity"
+        )
+        if self._call_on_update is not None:
+            self._call_on_update(activity)
+
+        return ResourceResponse(activity.id)
+
+    async def process_request(self, activity, handler):
+        context = TurnContext(self, activity)
+        return await self.run_pipeline(context, handler)
diff --git a/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py b/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py
new file mode 100644
index 000000000..3a2f2318c
--- /dev/null
+++ b/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py
@@ -0,0 +1,1013 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+# pylint: disable=too-many-lines
+
+from typing import List
+import aiounittest
+from botbuilder.core import BotAdapter, TurnContext
+from botbuilder.core.teams import TeamsActivityHandler
+from botbuilder.schema import (
+    Activity,
+    ActivityTypes,
+    ChannelAccount,
+    ConversationAccount,
+    ConversationReference,
+    ResourceResponse,
+)
+from botbuilder.schema.teams import (
+    AppBasedLinkQuery,
+    ChannelInfo,
+    FileConsentCardResponse,
+    MessageActionsPayload,
+    MessagingExtensionAction,
+    MessagingExtensionQuery,
+    O365ConnectorCardActionQuery,
+    TaskModuleRequest,
+    TaskModuleRequestContext,
+    TeamInfo,
+    TeamsChannelAccount,
+)
+from botframework.connector import Channels
+from simple_adapter import SimpleAdapter
+
+
+class TestingTeamsActivityHandler(TeamsActivityHandler):
+    __test__ = False
+
+    def __init__(self):
+        self.record: List[str] = []
+
+    async def on_conversation_update_activity(self, turn_context: TurnContext):
+        self.record.append("on_conversation_update_activity")
+        return await super().on_conversation_update_activity(turn_context)
+
+    async def on_teams_members_added(  # pylint: disable=unused-argument
+        self,
+        teams_members_added: [TeamsChannelAccount],
+        team_info: TeamInfo,
+        turn_context: TurnContext,
+    ):
+        self.record.append("on_teams_members_added")
+        return await super().on_teams_members_added(
+            teams_members_added, team_info, turn_context
+        )
+
+    async def on_teams_members_removed(
+        self,
+        teams_members_removed: [TeamsChannelAccount],
+        team_info: TeamInfo,
+        turn_context: TurnContext,
+    ):
+        self.record.append("on_teams_members_removed")
+        return await super().on_teams_members_removed(
+            teams_members_removed, team_info, turn_context
+        )
+
+    async def on_message_activity(self, turn_context: TurnContext):
+        self.record.append("on_message_activity")
+        return await super().on_message_activity(turn_context)
+
+    async def on_token_response_event(self, turn_context: TurnContext):
+        self.record.append("on_token_response_event")
+        return await super().on_token_response_event(turn_context)
+
+    async def on_event(self, turn_context: TurnContext):
+        self.record.append("on_event")
+        return await super().on_event(turn_context)
+
+    async def on_end_of_conversation_activity(self, turn_context: TurnContext):
+        self.record.append("on_end_of_conversation_activity")
+        return await super().on_end_of_conversation_activity(turn_context)
+
+    async def on_typing_activity(self, turn_context: TurnContext):
+        self.record.append("on_typing_activity")
+        return await super().on_typing_activity(turn_context)
+
+    async def on_unrecognized_activity_type(self, turn_context: TurnContext):
+        self.record.append("on_unrecognized_activity_type")
+        return await super().on_unrecognized_activity_type(turn_context)
+
+    async def on_teams_channel_created(
+        self, channel_info: ChannelInfo, team_info: TeamInfo, turn_context: TurnContext
+    ):
+        self.record.append("on_teams_channel_created")
+        return await super().on_teams_channel_created(
+            channel_info, team_info, turn_context
+        )
+
+    async def on_teams_channel_renamed(
+        self, channel_info: ChannelInfo, team_info: TeamInfo, turn_context: TurnContext
+    ):
+        self.record.append("on_teams_channel_renamed")
+        return await super().on_teams_channel_renamed(
+            channel_info, team_info, turn_context
+        )
+
+    async def on_teams_channel_restored(
+        self, channel_info: ChannelInfo, team_info: TeamInfo, turn_context: TurnContext
+    ):
+        self.record.append("on_teams_channel_restored")
+        return await super().on_teams_channel_restored(
+            channel_info, team_info, turn_context
+        )
+
+    async def on_teams_channel_deleted(
+        self, channel_info: ChannelInfo, team_info: TeamInfo, turn_context: TurnContext
+    ):
+        self.record.append("on_teams_channel_deleted")
+        return await super().on_teams_channel_renamed(
+            channel_info, team_info, turn_context
+        )
+
+    async def on_teams_team_archived(
+        self, team_info: TeamInfo, turn_context: TurnContext
+    ):
+        self.record.append("on_teams_team_archived")
+        return await super().on_teams_team_archived(team_info, turn_context)
+
+    async def on_teams_team_deleted(
+        self, team_info: TeamInfo, turn_context: TurnContext
+    ):
+        self.record.append("on_teams_team_deleted")
+        return await super().on_teams_team_deleted(team_info, turn_context)
+
+    async def on_teams_team_hard_deleted(
+        self, team_info: TeamInfo, turn_context: TurnContext
+    ):
+        self.record.append("on_teams_team_hard_deleted")
+        return await super().on_teams_team_hard_deleted(team_info, turn_context)
+
+    async def on_teams_team_renamed_activity(
+        self, team_info: TeamInfo, turn_context: TurnContext
+    ):
+        self.record.append("on_teams_team_renamed_activity")
+        return await super().on_teams_team_renamed_activity(team_info, turn_context)
+
+    async def on_teams_team_restored(
+        self, team_info: TeamInfo, turn_context: TurnContext
+    ):
+        self.record.append("on_teams_team_restored")
+        return await super().on_teams_team_restored(team_info, turn_context)
+
+    async def on_teams_team_unarchived(
+        self, team_info: TeamInfo, turn_context: TurnContext
+    ):
+        self.record.append("on_teams_team_unarchived")
+        return await super().on_teams_team_unarchived(team_info, turn_context)
+
+    async def on_invoke_activity(self, turn_context: TurnContext):
+        self.record.append("on_invoke_activity")
+        return await super().on_invoke_activity(turn_context)
+
+    async def on_teams_signin_verify_state(self, turn_context: TurnContext):
+        self.record.append("on_teams_signin_verify_state")
+        return await super().on_teams_signin_verify_state(turn_context)
+
+    async def on_teams_file_consent(
+        self,
+        turn_context: TurnContext,
+        file_consent_card_response: FileConsentCardResponse,
+    ):
+        self.record.append("on_teams_file_consent")
+        return await super().on_teams_file_consent(
+            turn_context, file_consent_card_response
+        )
+
+    async def on_teams_file_consent_accept(
+        self,
+        turn_context: TurnContext,
+        file_consent_card_response: FileConsentCardResponse,
+    ):
+        self.record.append("on_teams_file_consent_accept")
+        return await super().on_teams_file_consent_accept(
+            turn_context, file_consent_card_response
+        )
+
+    async def on_teams_file_consent_decline(
+        self,
+        turn_context: TurnContext,
+        file_consent_card_response: FileConsentCardResponse,
+    ):
+        self.record.append("on_teams_file_consent_decline")
+        return await super().on_teams_file_consent_decline(
+            turn_context, file_consent_card_response
+        )
+
+    async def on_teams_o365_connector_card_action(
+        self, turn_context: TurnContext, query: O365ConnectorCardActionQuery
+    ):
+        self.record.append("on_teams_o365_connector_card_action")
+        return await super().on_teams_o365_connector_card_action(turn_context, query)
+
+    async def on_teams_app_based_link_query(
+        self, turn_context: TurnContext, query: AppBasedLinkQuery
+    ):
+        self.record.append("on_teams_app_based_link_query")
+        return await super().on_teams_app_based_link_query(turn_context, query)
+
+    async def on_teams_messaging_extension_query(
+        self, turn_context: TurnContext, query: MessagingExtensionQuery
+    ):
+        self.record.append("on_teams_messaging_extension_query")
+        return await super().on_teams_messaging_extension_query(turn_context, query)
+
+    async def on_teams_messaging_extension_submit_action_dispatch(
+        self, turn_context: TurnContext, action: MessagingExtensionAction
+    ):
+        self.record.append("on_teams_messaging_extension_submit_action_dispatch")
+        return await super().on_teams_messaging_extension_submit_action_dispatch(
+            turn_context, action
+        )
+
+    async def on_teams_messaging_extension_submit_action(
+        self, turn_context: TurnContext, action: MessagingExtensionAction
+    ):
+        self.record.append("on_teams_messaging_extension_submit_action")
+        return await super().on_teams_messaging_extension_submit_action(
+            turn_context, action
+        )
+
+    async def on_teams_messaging_extension_bot_message_preview_edit(
+        self, turn_context: TurnContext, action: MessagingExtensionAction
+    ):
+        self.record.append("on_teams_messaging_extension_bot_message_preview_edit")
+        return await super().on_teams_messaging_extension_bot_message_preview_edit(
+            turn_context, action
+        )
+
+    async def on_teams_messaging_extension_bot_message_preview_send(
+        self, turn_context: TurnContext, action: MessagingExtensionAction
+    ):
+        self.record.append("on_teams_messaging_extension_bot_message_preview_send")
+        return await super().on_teams_messaging_extension_bot_message_preview_send(
+            turn_context, action
+        )
+
+    async def on_teams_messaging_extension_fetch_task(
+        self, turn_context: TurnContext, action: MessagingExtensionAction
+    ):
+        self.record.append("on_teams_messaging_extension_fetch_task")
+        return await super().on_teams_messaging_extension_fetch_task(
+            turn_context, action
+        )
+
+    async def on_teams_messaging_extension_configuration_query_settings_url(
+        self, turn_context: TurnContext, query: MessagingExtensionQuery
+    ):
+        self.record.append(
+            "on_teams_messaging_extension_configuration_query_settings_url"
+        )
+        return await super().on_teams_messaging_extension_configuration_query_settings_url(
+            turn_context, query
+        )
+
+    async def on_teams_messaging_extension_configuration_setting(
+        self, turn_context: TurnContext, settings
+    ):
+        self.record.append("on_teams_messaging_extension_configuration_setting")
+        return await super().on_teams_messaging_extension_configuration_setting(
+            turn_context, settings
+        )
+
+    async def on_teams_messaging_extension_card_button_clicked(
+        self, turn_context: TurnContext, card_data
+    ):
+        self.record.append("on_teams_messaging_extension_card_button_clicked")
+        return await super().on_teams_messaging_extension_card_button_clicked(
+            turn_context, card_data
+        )
+
+    async def on_teams_task_module_fetch(
+        self, turn_context: TurnContext, task_module_request
+    ):
+        self.record.append("on_teams_task_module_fetch")
+        return await super().on_teams_task_module_fetch(
+            turn_context, task_module_request
+        )
+
+    async def on_teams_task_module_submit(  # pylint: disable=unused-argument
+        self, turn_context: TurnContext, task_module_request: TaskModuleRequest
+    ):
+        self.record.append("on_teams_task_module_submit")
+        return await super().on_teams_task_module_submit(
+            turn_context, task_module_request
+        )
+
+
+class NotImplementedAdapter(BotAdapter):
+    async def delete_activity(
+        self, context: TurnContext, reference: ConversationReference
+    ):
+        raise NotImplementedError()
+
+    async def send_activities(
+        self, context: TurnContext, activities: List[Activity]
+    ) -> List[ResourceResponse]:
+        raise NotImplementedError()
+
+    async def update_activity(self, context: TurnContext, activity: Activity):
+        raise NotImplementedError()
+
+
+class TestTeamsActivityHandler(aiounittest.AsyncTestCase):
+    async def test_on_teams_channel_created_activity(self):
+        # arrange
+        activity = Activity(
+            type=ActivityTypes.conversation_update,
+            channel_data={
+                "eventType": "channelCreated",
+                "channel": {"id": "asdfqwerty", "name": "new_channel"},
+            },
+            channel_id=Channels.ms_teams,
+        )
+
+        turn_context = TurnContext(NotImplementedAdapter(), activity)
+
+        # Act
+        bot = TestingTeamsActivityHandler()
+        await bot.on_turn(turn_context)
+
+        # Assert
+        assert len(bot.record) == 2
+        assert bot.record[0] == "on_conversation_update_activity"
+        assert bot.record[1] == "on_teams_channel_created"
+
+    async def test_on_teams_channel_renamed_activity(self):
+        # arrange
+        activity = Activity(
+            type=ActivityTypes.conversation_update,
+            channel_data={
+                "eventType": "channelRenamed",
+                "channel": {"id": "asdfqwerty", "name": "new_channel"},
+            },
+            channel_id=Channels.ms_teams,
+        )
+
+        turn_context = TurnContext(NotImplementedAdapter(), activity)
+
+        # Act
+        bot = TestingTeamsActivityHandler()
+        await bot.on_turn(turn_context)
+
+        # Assert
+        assert len(bot.record) == 2
+        assert bot.record[0] == "on_conversation_update_activity"
+        assert bot.record[1] == "on_teams_channel_renamed"
+
+    async def test_on_teams_channel_restored_activity(self):
+        # arrange
+        activity = Activity(
+            type=ActivityTypes.conversation_update,
+            channel_data={
+                "eventType": "channelRestored",
+                "channel": {"id": "asdfqwerty", "name": "channel_restored"},
+            },
+            channel_id=Channels.ms_teams,
+        )
+
+        turn_context = TurnContext(NotImplementedAdapter(), activity)
+
+        # Act
+        bot = TestingTeamsActivityHandler()
+        await bot.on_turn(turn_context)
+
+        # Assert
+        assert len(bot.record) == 2
+        assert bot.record[0] == "on_conversation_update_activity"
+        assert bot.record[1] == "on_teams_channel_restored"
+
+    async def test_on_teams_channel_deleted_activity(self):
+        # arrange
+        activity = Activity(
+            type=ActivityTypes.conversation_update,
+            channel_data={
+                "eventType": "channelDeleted",
+                "channel": {"id": "asdfqwerty", "name": "new_channel"},
+            },
+            channel_id=Channels.ms_teams,
+        )
+
+        turn_context = TurnContext(NotImplementedAdapter(), activity)
+
+        # Act
+        bot = TestingTeamsActivityHandler()
+        await bot.on_turn(turn_context)
+
+        # Assert
+        assert len(bot.record) == 2
+        assert bot.record[0] == "on_conversation_update_activity"
+        assert bot.record[1] == "on_teams_channel_deleted"
+
+    async def test_on_teams_team_archived(self):
+        # arrange
+        activity = Activity(
+            type=ActivityTypes.conversation_update,
+            channel_data={
+                "eventType": "teamArchived",
+                "team": {"id": "team_id_1", "name": "archived_team_name"},
+            },
+            channel_id=Channels.ms_teams,
+        )
+
+        turn_context = TurnContext(NotImplementedAdapter(), activity)
+
+        # Act
+        bot = TestingTeamsActivityHandler()
+        await bot.on_turn(turn_context)
+
+        # Assert
+        assert len(bot.record) == 2
+        assert bot.record[0] == "on_conversation_update_activity"
+        assert bot.record[1] == "on_teams_team_archived"
+
+    async def test_on_teams_team_deleted(self):
+        # arrange
+        activity = Activity(
+            type=ActivityTypes.conversation_update,
+            channel_data={
+                "eventType": "teamDeleted",
+                "team": {"id": "team_id_1", "name": "deleted_team_name"},
+            },
+            channel_id=Channels.ms_teams,
+        )
+
+        turn_context = TurnContext(NotImplementedAdapter(), activity)
+
+        # Act
+        bot = TestingTeamsActivityHandler()
+        await bot.on_turn(turn_context)
+
+        # Assert
+        assert len(bot.record) == 2
+        assert bot.record[0] == "on_conversation_update_activity"
+        assert bot.record[1] == "on_teams_team_deleted"
+
+    async def test_on_teams_team_hard_deleted(self):
+        # arrange
+        activity = Activity(
+            type=ActivityTypes.conversation_update,
+            channel_data={
+                "eventType": "teamHardDeleted",
+                "team": {"id": "team_id_1", "name": "hard_deleted_team_name"},
+            },
+            channel_id=Channels.ms_teams,
+        )
+
+        turn_context = TurnContext(NotImplementedAdapter(), activity)
+
+        # Act
+        bot = TestingTeamsActivityHandler()
+        await bot.on_turn(turn_context)
+
+        # Assert
+        assert len(bot.record) == 2
+        assert bot.record[0] == "on_conversation_update_activity"
+        assert bot.record[1] == "on_teams_team_hard_deleted"
+
+    async def test_on_teams_team_renamed_activity(self):
+        # arrange
+        activity = Activity(
+            type=ActivityTypes.conversation_update,
+            channel_data={
+                "eventType": "teamRenamed",
+                "team": {"id": "team_id_1", "name": "new_team_name"},
+            },
+            channel_id=Channels.ms_teams,
+        )
+
+        turn_context = TurnContext(NotImplementedAdapter(), activity)
+
+        # Act
+        bot = TestingTeamsActivityHandler()
+        await bot.on_turn(turn_context)
+
+        # Assert
+        assert len(bot.record) == 2
+        assert bot.record[0] == "on_conversation_update_activity"
+        assert bot.record[1] == "on_teams_team_renamed_activity"
+
+    async def test_on_teams_team_restored(self):
+        # arrange
+        activity = Activity(
+            type=ActivityTypes.conversation_update,
+            channel_data={
+                "eventType": "teamRestored",
+                "team": {"id": "team_id_1", "name": "restored_team_name"},
+            },
+            channel_id=Channels.ms_teams,
+        )
+
+        turn_context = TurnContext(NotImplementedAdapter(), activity)
+
+        # Act
+        bot = TestingTeamsActivityHandler()
+        await bot.on_turn(turn_context)
+
+        # Assert
+        assert len(bot.record) == 2
+        assert bot.record[0] == "on_conversation_update_activity"
+        assert bot.record[1] == "on_teams_team_restored"
+
+    async def test_on_teams_team_unarchived(self):
+        # arrange
+        activity = Activity(
+            type=ActivityTypes.conversation_update,
+            channel_data={
+                "eventType": "teamUnarchived",
+                "team": {"id": "team_id_1", "name": "unarchived_team_name"},
+            },
+            channel_id=Channels.ms_teams,
+        )
+
+        turn_context = TurnContext(NotImplementedAdapter(), activity)
+
+        # Act
+        bot = TestingTeamsActivityHandler()
+        await bot.on_turn(turn_context)
+
+        # Assert
+        assert len(bot.record) == 2
+        assert bot.record[0] == "on_conversation_update_activity"
+        assert bot.record[1] == "on_teams_team_unarchived"
+
+    async def test_on_teams_members_added_activity(self):
+        # arrange
+        activity = Activity(
+            type=ActivityTypes.conversation_update,
+            channel_data={
+                "eventType": "teamMemberAdded",
+                "team": {"id": "team_id_1", "name": "new_team_name"},
+            },
+            members_added=[
+                ChannelAccount(
+                    id="123",
+                    name="test_user",
+                    aad_object_id="asdfqwerty",
+                    role="tester",
+                )
+            ],
+            channel_id=Channels.ms_teams,
+            conversation=ConversationAccount(id="456"),
+        )
+
+        turn_context = TurnContext(SimpleAdapter(), activity)
+
+        # Act
+        bot = TestingTeamsActivityHandler()
+        await bot.on_turn(turn_context)
+
+        # Assert
+        assert len(bot.record) == 2
+        assert bot.record[0] == "on_conversation_update_activity"
+        assert bot.record[1] == "on_teams_members_added"
+
+    async def test_bot_on_teams_members_added_activity(self):
+        # arrange
+        activity = Activity(
+            recipient=ChannelAccount(id="botid"),
+            type=ActivityTypes.conversation_update,
+            channel_data={
+                "eventType": "teamMemberAdded",
+                "team": {"id": "team_id_1", "name": "new_team_name"},
+            },
+            members_added=[
+                ChannelAccount(
+                    id="botid",
+                    name="test_user",
+                    aad_object_id="asdfqwerty",
+                    role="tester",
+                )
+            ],
+            channel_id=Channels.ms_teams,
+            conversation=ConversationAccount(id="456"),
+        )
+
+        turn_context = TurnContext(SimpleAdapter(), activity)
+
+        # Act
+        bot = TestingTeamsActivityHandler()
+        await bot.on_turn(turn_context)
+
+        # Assert
+        assert len(bot.record) == 2
+        assert bot.record[0] == "on_conversation_update_activity"
+        assert bot.record[1] == "on_teams_members_added"
+
+    async def test_on_teams_members_removed_activity(self):
+        # arrange
+        activity = Activity(
+            type=ActivityTypes.conversation_update,
+            channel_data={
+                "eventType": "teamMemberRemoved",
+                "team": {"id": "team_id_1", "name": "new_team_name"},
+            },
+            members_removed=[
+                ChannelAccount(
+                    id="123",
+                    name="test_user",
+                    aad_object_id="asdfqwerty",
+                    role="tester",
+                )
+            ],
+            channel_id=Channels.ms_teams,
+        )
+
+        turn_context = TurnContext(SimpleAdapter(), activity)
+
+        # Act
+        bot = TestingTeamsActivityHandler()
+        await bot.on_turn(turn_context)
+
+        # Assert
+        assert len(bot.record) == 2
+        assert bot.record[0] == "on_conversation_update_activity"
+        assert bot.record[1] == "on_teams_members_removed"
+
+    async def test_on_signin_verify_state(self):
+        # arrange
+        activity = Activity(type=ActivityTypes.invoke, name="signin/verifyState")
+
+        turn_context = TurnContext(SimpleAdapter(), activity)
+
+        # Act
+        bot = TestingTeamsActivityHandler()
+        await bot.on_turn(turn_context)
+
+        # Assert
+        assert len(bot.record) == 2
+        assert bot.record[0] == "on_invoke_activity"
+        assert bot.record[1] == "on_teams_signin_verify_state"
+
+    async def test_on_file_consent_accept_activity(self):
+        # arrange
+        activity = Activity(
+            type=ActivityTypes.invoke,
+            name="fileConsent/invoke",
+            value={"action": "accept"},
+        )
+
+        turn_context = TurnContext(SimpleAdapter(), activity)
+
+        # Act
+        bot = TestingTeamsActivityHandler()
+        await bot.on_turn(turn_context)
+
+        # Assert
+        assert len(bot.record) == 3
+        assert bot.record[0] == "on_invoke_activity"
+        assert bot.record[1] == "on_teams_file_consent"
+        assert bot.record[2] == "on_teams_file_consent_accept"
+
+    async def test_on_file_consent_decline_activity(self):
+        # Arrange
+        activity = Activity(
+            type=ActivityTypes.invoke,
+            name="fileConsent/invoke",
+            value={"action": "decline"},
+        )
+
+        turn_context = TurnContext(SimpleAdapter(), activity)
+
+        # Act
+        bot = TestingTeamsActivityHandler()
+        await bot.on_turn(turn_context)
+
+        # Assert
+        assert len(bot.record) == 3
+        assert bot.record[0] == "on_invoke_activity"
+        assert bot.record[1] == "on_teams_file_consent"
+        assert bot.record[2] == "on_teams_file_consent_decline"
+
+    async def test_on_file_consent_bad_action_activity(self):
+        # Arrange
+        activity = Activity(
+            type=ActivityTypes.invoke,
+            name="fileConsent/invoke",
+            value={"action": "bad_action"},
+        )
+
+        turn_context = TurnContext(SimpleAdapter(), activity)
+
+        # Act
+        bot = TestingTeamsActivityHandler()
+        await bot.on_turn(turn_context)
+
+        # Assert
+        assert len(bot.record) == 2
+        assert bot.record[0] == "on_invoke_activity"
+        assert bot.record[1] == "on_teams_file_consent"
+
+    async def test_on_teams_o365_connector_card_action(self):
+        # arrange
+        activity = Activity(
+            type=ActivityTypes.invoke,
+            name="actionableMessage/executeAction",
+            value={"body": "body_here", "actionId": "action_id_here"},
+        )
+
+        turn_context = TurnContext(SimpleAdapter(), activity)
+
+        # Act
+        bot = TestingTeamsActivityHandler()
+        await bot.on_turn(turn_context)
+
+        # Assert
+        assert len(bot.record) == 2
+        assert bot.record[0] == "on_invoke_activity"
+        assert bot.record[1] == "on_teams_o365_connector_card_action"
+
+    async def test_on_app_based_link_query(self):
+        # arrange
+        activity = Activity(
+            type=ActivityTypes.invoke,
+            name="composeExtension/query",
+            value={"url": "http://www.test.com"},
+        )
+
+        turn_context = TurnContext(SimpleAdapter(), activity)
+
+        # Act
+        bot = TestingTeamsActivityHandler()
+        await bot.on_turn(turn_context)
+
+        # Assert
+        assert len(bot.record) == 2
+        assert bot.record[0] == "on_invoke_activity"
+        assert bot.record[1] == "on_teams_messaging_extension_query"
+
+    async def test_on_teams_messaging_extension_bot_message_preview_edit_activity(self):
+        # Arrange
+
+        activity = Activity(
+            type=ActivityTypes.invoke,
+            name="composeExtension/submitAction",
+            value={
+                "data": {"key": "value"},
+                "context": {"theme": "dark"},
+                "commandId": "test_command",
+                "commandContext": "command_context_test",
+                "botMessagePreviewAction": "edit",
+                "botActivityPreview": [{"id": "activity123"}],
+                "messagePayload": {"id": "payloadid"},
+            },
+        )
+
+        turn_context = TurnContext(SimpleAdapter(), activity)
+
+        # Act
+        bot = TestingTeamsActivityHandler()
+        await bot.on_turn(turn_context)
+
+        # Assert
+        assert len(bot.record) == 3
+        assert bot.record[0] == "on_invoke_activity"
+        assert bot.record[1] == "on_teams_messaging_extension_submit_action_dispatch"
+        assert bot.record[2] == "on_teams_messaging_extension_bot_message_preview_edit"
+
+    async def test_on_teams_messaging_extension_bot_message_send_activity(self):
+        # Arrange
+        activity = Activity(
+            type=ActivityTypes.invoke,
+            name="composeExtension/submitAction",
+            value={
+                "data": {"key": "value"},
+                "context": {"theme": "dark"},
+                "commandId": "test_command",
+                "commandContext": "command_context_test",
+                "botMessagePreviewAction": "send",
+                "botActivityPreview": [{"id": "123"}],
+                "messagePayload": {"id": "abc"},
+            },
+        )
+
+        turn_context = TurnContext(SimpleAdapter(), activity)
+
+        # Act
+        bot = TestingTeamsActivityHandler()
+        await bot.on_turn(turn_context)
+
+        # Assert
+        assert len(bot.record) == 3
+        assert bot.record[0] == "on_invoke_activity"
+        assert bot.record[1] == "on_teams_messaging_extension_submit_action_dispatch"
+        assert bot.record[2] == "on_teams_messaging_extension_bot_message_preview_send"
+
+    async def test_on_teams_messaging_extension_bot_message_send_activity_with_none(
+        self,
+    ):
+        # Arrange
+        activity = Activity(
+            type=ActivityTypes.invoke,
+            name="composeExtension/submitAction",
+            value={
+                "data": {"key": "value"},
+                "context": {"theme": "dark"},
+                "commandId": "test_command",
+                "commandContext": "command_context_test",
+                "botMessagePreviewAction": None,
+                "botActivityPreview": [{"id": "test123"}],
+                "messagePayload": {"id": "payloadid123"},
+            },
+        )
+
+        turn_context = TurnContext(SimpleAdapter(), activity)
+
+        # Act
+        bot = TestingTeamsActivityHandler()
+        await bot.on_turn(turn_context)
+
+        # Assert
+        assert len(bot.record) == 3
+        assert bot.record[0] == "on_invoke_activity"
+        assert bot.record[1] == "on_teams_messaging_extension_submit_action_dispatch"
+        assert bot.record[2] == "on_teams_messaging_extension_submit_action"
+
+    async def test_on_teams_messaging_extension_bot_message_send_activity_with_empty_string(
+        self,
+    ):
+        # Arrange
+        activity = Activity(
+            type=ActivityTypes.invoke,
+            name="composeExtension/submitAction",
+            value={
+                "data": {"key": "value"},
+                "context": {"theme": "dark"},
+                "commandId": "test_command",
+                "commandContext": "command_context_test",
+                "botMessagePreviewAction": "",
+                "botActivityPreview": [Activity().serialize()],
+                "messagePayload": MessageActionsPayload().serialize(),
+            },
+        )
+
+        turn_context = TurnContext(SimpleAdapter(), activity)
+
+        # Act
+        bot = TestingTeamsActivityHandler()
+        await bot.on_turn(turn_context)
+
+        # Assert
+        assert len(bot.record) == 3
+        assert bot.record[0] == "on_invoke_activity"
+        assert bot.record[1] == "on_teams_messaging_extension_submit_action_dispatch"
+        assert bot.record[2] == "on_teams_messaging_extension_submit_action"
+
+    async def test_on_teams_messaging_extension_fetch_task(self):
+        # Arrange
+        activity = Activity(
+            type=ActivityTypes.invoke,
+            name="composeExtension/fetchTask",
+            value={
+                "data": {"key": "value"},
+                "context": {"theme": "dark"},
+                "commandId": "test_command",
+                "commandContext": "command_context_test",
+                "botMessagePreviewAction": "message_action",
+                "botActivityPreview": [{"id": "123"}],
+                "messagePayload": {"id": "abc123"},
+            },
+        )
+        turn_context = TurnContext(SimpleAdapter(), activity)
+
+        # Act
+        bot = TestingTeamsActivityHandler()
+        await bot.on_turn(turn_context)
+
+        # Assert
+        assert len(bot.record) == 2
+        assert bot.record[0] == "on_invoke_activity"
+        assert bot.record[1] == "on_teams_messaging_extension_fetch_task"
+
+    async def test_on_teams_messaging_extension_configuration_query_settings_url(self):
+        # Arrange
+        activity = Activity(
+            type=ActivityTypes.invoke,
+            name="composeExtension/querySettingUrl",
+            value={
+                "commandId": "test_command",
+                "parameters": [],
+                "messagingExtensionQueryOptions": {"skip": 1, "count": 1},
+                "state": "state_string",
+            },
+        )
+
+        turn_context = TurnContext(SimpleAdapter(), activity)
+
+        # Act
+        bot = TestingTeamsActivityHandler()
+        await bot.on_turn(turn_context)
+
+        # Assert
+        assert len(bot.record) == 2
+        assert bot.record[0] == "on_invoke_activity"
+        assert (
+            bot.record[1]
+            == "on_teams_messaging_extension_configuration_query_settings_url"
+        )
+
+    async def test_on_teams_messaging_extension_configuration_setting(self):
+        # Arrange
+        activity = Activity(
+            type=ActivityTypes.invoke,
+            name="composeExtension/setting",
+            value={"key": "value"},
+        )
+
+        turn_context = TurnContext(SimpleAdapter(), activity)
+
+        # Act
+        bot = TestingTeamsActivityHandler()
+        await bot.on_turn(turn_context)
+
+        # Assert
+        assert len(bot.record) == 2
+        assert bot.record[0] == "on_invoke_activity"
+        assert bot.record[1] == "on_teams_messaging_extension_configuration_setting"
+
+    async def test_on_teams_messaging_extension_card_button_clicked(self):
+        # Arrange
+        activity = Activity(
+            type=ActivityTypes.invoke,
+            name="composeExtension/onCardButtonClicked",
+            value={"key": "value"},
+        )
+
+        turn_context = TurnContext(SimpleAdapter(), activity)
+
+        # Act
+        bot = TestingTeamsActivityHandler()
+        await bot.on_turn(turn_context)
+
+        # Assert
+        assert len(bot.record) == 2
+        assert bot.record[0] == "on_invoke_activity"
+        assert bot.record[1] == "on_teams_messaging_extension_card_button_clicked"
+
+    async def test_on_teams_task_module_fetch(self):
+        # Arrange
+        activity = Activity(
+            type=ActivityTypes.invoke,
+            name="task/fetch",
+            value={
+                "data": {"key": "value"},
+                "context": TaskModuleRequestContext().serialize(),
+            },
+        )
+
+        turn_context = TurnContext(SimpleAdapter(), activity)
+
+        # Act
+        bot = TestingTeamsActivityHandler()
+        await bot.on_turn(turn_context)
+
+        # Assert
+        assert len(bot.record) == 2
+        assert bot.record[0] == "on_invoke_activity"
+        assert bot.record[1] == "on_teams_task_module_fetch"
+
+    async def test_on_teams_task_module_submit(self):
+        # Arrange
+        activity = Activity(
+            type=ActivityTypes.invoke,
+            name="task/submit",
+            value={
+                "data": {"key": "value"},
+                "context": TaskModuleRequestContext().serialize(),
+            },
+        )
+
+        turn_context = TurnContext(SimpleAdapter(), activity)
+
+        # Act
+        bot = TestingTeamsActivityHandler()
+        await bot.on_turn(turn_context)
+
+        # Assert
+        assert len(bot.record) == 2
+        assert bot.record[0] == "on_invoke_activity"
+        assert bot.record[1] == "on_teams_task_module_submit"
+
+    async def test_on_end_of_conversation_activity(self):
+        activity = Activity(type=ActivityTypes.end_of_conversation)
+
+        turn_context = TurnContext(SimpleAdapter(), activity)
+
+        # Act
+        bot = TestingTeamsActivityHandler()
+        await bot.on_turn(turn_context)
+
+        assert len(bot.record) == 1
+        assert bot.record[0] == "on_end_of_conversation_activity"
+
+    async def test_typing_activity(self):
+        activity = Activity(type=ActivityTypes.typing)
+
+        turn_context = TurnContext(SimpleAdapter(), activity)
+
+        # Act
+        bot = TestingTeamsActivityHandler()
+        await bot.on_turn(turn_context)
+
+        assert len(bot.record) == 1
+        assert bot.record[0] == "on_typing_activity"
diff --git a/libraries/botbuilder-core/tests/teams/test_teams_channel_data.py b/libraries/botbuilder-core/tests/teams/test_teams_channel_data.py
new file mode 100644
index 000000000..e468526bc
--- /dev/null
+++ b/libraries/botbuilder-core/tests/teams/test_teams_channel_data.py
@@ -0,0 +1,30 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import aiounittest
+
+from botbuilder.schema import Activity
+from botbuilder.schema.teams import TeamsChannelData
+from botbuilder.core.teams import teams_get_team_info
+
+
+class TestTeamsChannelData(aiounittest.AsyncTestCase):
+    def test_teams_aad_group_id_deserialize(self):
+        # Arrange
+        raw_channel_data = {"team": {"aadGroupId": "teamGroup123"}}
+
+        # Act
+        channel_data = TeamsChannelData().deserialize(raw_channel_data)
+
+        # Assert
+        assert channel_data.team.aad_group_id == "teamGroup123"
+
+    def test_teams_get_team_info(self):
+        # Arrange
+        activity = Activity(channel_data={"team": {"aadGroupId": "teamGroup123"}})
+
+        # Act
+        team_info = teams_get_team_info(activity)
+
+        # Assert
+        assert team_info.aad_group_id == "teamGroup123"
diff --git a/libraries/botbuilder-core/tests/teams/test_teams_extension.py b/libraries/botbuilder-core/tests/teams/test_teams_extension.py
new file mode 100644
index 000000000..98c1ee829
--- /dev/null
+++ b/libraries/botbuilder-core/tests/teams/test_teams_extension.py
@@ -0,0 +1,162 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import aiounittest
+
+from botbuilder.schema import Activity
+from botbuilder.schema.teams import TeamInfo
+from botbuilder.core.teams import (
+    teams_get_channel_id,
+    teams_get_team_info,
+    teams_notify_user,
+)
+from botbuilder.core.teams.teams_activity_extensions import teams_get_meeting_info
+
+
+class TestTeamsActivityHandler(aiounittest.AsyncTestCase):
+    def test_teams_get_channel_id(self):
+        # Arrange
+        activity = Activity(
+            channel_data={"channel": {"id": "id123", "name": "channel_name"}}
+        )
+
+        # Act
+        result = teams_get_channel_id(activity)
+
+        # Assert
+        assert result == "id123"
+
+    def test_teams_get_channel_id_with_no_channel(self):
+        # Arrange
+        activity = Activity(
+            channel_data={"team": {"id": "id123", "name": "channel_name"}}
+        )
+
+        # Act
+        result = teams_get_channel_id(activity)
+
+        # Assert
+        assert result is None
+
+    def test_teams_get_channel_id_with_no_channel_id(self):
+        # Arrange
+        activity = Activity(channel_data={"team": {"name": "channel_name"}})
+
+        # Act
+        result = teams_get_channel_id(activity)
+
+        # Assert
+        assert result is None
+
+    def test_teams_get_channel_id_with_no_channel_data(self):
+        # Arrange
+        activity = Activity(type="type")
+
+        # Act
+        result = teams_get_channel_id(activity)
+
+        # Assert
+        assert result is None
+
+    def test_teams_get_channel_id_with_none_activity(self):
+        # Arrange
+        activity = None
+
+        # Act
+        result = teams_get_channel_id(activity)
+
+        # Assert
+        assert result is None
+
+    def test_teams_get_team_info(self):
+        # Arrange
+        activity = Activity(
+            channel_data={"team": {"id": "id123", "name": "channel_name"}}
+        )
+
+        # Act
+        result = teams_get_team_info(activity)
+
+        # Assert
+        assert result == TeamInfo(id="id123", name="channel_name")
+
+    def test_teams_get_team_info_with_no_channel_data(self):
+        # Arrange
+        activity = Activity(type="type")
+
+        # Act
+        result = teams_get_team_info(activity)
+
+        # Assert
+        assert result is None
+
+    def test_teams_get_team_info_with_no_team_info(self):
+        # Arrange
+        activity = Activity(channel_data={"eventType": "eventType"})
+
+        # Act
+        result = teams_get_team_info(activity)
+
+        # Assert
+        assert result is None
+
+    def test_teams_get_team_info_with_none_activity(self):
+        # Arrange
+        activity = None
+
+        # Act
+        result = teams_get_team_info(activity)
+
+        # Assert
+        assert result is None
+
+    def test_teams_notify_user(self):
+        # Arrange
+        activity = Activity(channel_data={"eventType": "eventType"})
+
+        # Act
+        teams_notify_user(activity)
+
+        # Assert
+        assert activity.channel_data.notification.alert
+
+    def test_teams_notify_user_with_no_activity(self):
+        # Arrange
+        activity = None
+
+        # Act
+        teams_notify_user(activity)
+
+        # Assert
+        assert activity is None
+
+    def test_teams_notify_user_with_preexisting_notification(self):
+        # Arrange
+        activity = Activity(channel_data={"notification": {"alert": False}})
+
+        # Act
+        teams_notify_user(activity)
+
+        # Assert
+        assert activity.channel_data.notification.alert
+
+    def test_teams_notify_user_with_no_channel_data(self):
+        # Arrange
+        activity = Activity(id="id123")
+
+        # Act
+        teams_notify_user(activity)
+
+        # Assert
+        assert activity.channel_data.notification.alert
+        assert activity.id == "id123"
+
+    def test_teams_meeting_info(self):
+        # Arrange
+        activity = Activity(channel_data={"meeting": {"id": "meeting123"}})
+
+        # Act
+        meeting_id = teams_get_meeting_info(activity).id
+
+        # Assert
+        assert meeting_id == "meeting123"
diff --git a/libraries/botbuilder-core/tests/teams/test_teams_helper.py b/libraries/botbuilder-core/tests/teams/test_teams_helper.py
new file mode 100644
index 000000000..782973f0a
--- /dev/null
+++ b/libraries/botbuilder-core/tests/teams/test_teams_helper.py
@@ -0,0 +1,54 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import aiounittest
+
+from botbuilder.core.teams.teams_helper import deserializer_helper
+from botbuilder.schema import Activity, ChannelAccount, Mention
+from botbuilder.schema.teams import (
+    MessageActionsPayload,
+    MessagingExtensionAction,
+    TaskModuleRequestContext,
+)
+
+
+class TestTeamsActivityHandler(aiounittest.AsyncTestCase):
+    def test_teams_helper_teams_schema(self):
+        # Arrange
+        data = {
+            "data": {"key": "value"},
+            "context": {"theme": "dark"},
+            "commandId": "test_command",
+            "commandContext": "command_context_test",
+            "botMessagePreviewAction": "edit",
+            "botActivityPreview": [{"id": "activity123"}],
+            "messagePayload": {"id": "payloadid"},
+        }
+
+        # Act
+        result = deserializer_helper(MessagingExtensionAction, data)
+
+        # Assert
+        assert result.data == {"key": "value"}
+        assert result.context == TaskModuleRequestContext(theme="dark")
+        assert result.command_id == "test_command"
+        assert result.bot_message_preview_action == "edit"
+        assert len(result.bot_activity_preview) == 1
+        assert result.bot_activity_preview[0] == Activity(id="activity123")
+        assert result.message_payload == MessageActionsPayload(id="payloadid")
+
+    def test_teams_helper_schema(self):
+        # Arrange
+        data = {
+            "mentioned": {"id": "123", "name": "testName"},
+            "text": "Hello testName",
+            "type": "mention",
+        }
+
+        # Act
+        result = deserializer_helper(Mention, data)
+
+        # Assert
+        assert result.mentioned == ChannelAccount(id="123", name="testName")
+        assert result.text == "Hello testName"
+        assert result.type == "mention"
diff --git a/libraries/botbuilder-core/tests/teams/test_teams_info.py b/libraries/botbuilder-core/tests/teams/test_teams_info.py
new file mode 100644
index 000000000..5c044e6ca
--- /dev/null
+++ b/libraries/botbuilder-core/tests/teams/test_teams_info.py
@@ -0,0 +1,237 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import aiounittest
+from botframework.connector import Channels
+
+from botbuilder.core import TurnContext, MessageFactory
+from botbuilder.core.teams import TeamsInfo, TeamsActivityHandler
+from botbuilder.schema import (
+    Activity,
+    ChannelAccount,
+    ConversationAccount,
+)
+from simple_adapter_with_create_conversation import SimpleAdapterWithCreateConversation
+
+ACTIVITY = Activity(
+    id="1234",
+    type="message",
+    text="test",
+    from_property=ChannelAccount(id="user", name="User Name"),
+    recipient=ChannelAccount(id="bot", name="Bot Name"),
+    conversation=ConversationAccount(id="convo", name="Convo Name"),
+    channel_data={"channelData": {}},
+    channel_id="UnitTest",
+    locale="en-us",
+    service_url="https://example.org",
+)
+
+
+class TestTeamsInfo(aiounittest.AsyncTestCase):
+    async def test_send_message_to_teams_channels_without_activity(self):
+        def create_conversation():
+            pass
+
+        adapter = SimpleAdapterWithCreateConversation(
+            call_create_conversation=create_conversation
+        )
+
+        activity = Activity()
+        turn_context = TurnContext(adapter, activity)
+
+        try:
+            await TeamsInfo.send_message_to_teams_channel(
+                turn_context, None, "channelId123"
+            )
+        except ValueError:
+            pass
+        else:
+            assert False, "should have raise ValueError"
+
+    async def test_send_message_to_teams(self):
+        def create_conversation():
+            pass
+
+        adapter = SimpleAdapterWithCreateConversation(
+            call_create_conversation=create_conversation
+        )
+
+        turn_context = TurnContext(adapter, ACTIVITY)
+        handler = TestTeamsActivityHandler()
+        await handler.on_turn(turn_context)
+
+    async def test_send_message_to_teams_channels_without_turn_context(self):
+        try:
+            await TeamsInfo.send_message_to_teams_channel(
+                None, ACTIVITY, "channelId123"
+            )
+        except ValueError:
+            pass
+        else:
+            assert False, "should have raise ValueError"
+
+    async def test_send_message_to_teams_channels_without_teams_channel_id(self):
+        def create_conversation():
+            pass
+
+        adapter = SimpleAdapterWithCreateConversation(
+            call_create_conversation=create_conversation
+        )
+
+        turn_context = TurnContext(adapter, ACTIVITY)
+
+        try:
+            await TeamsInfo.send_message_to_teams_channel(turn_context, ACTIVITY, "")
+        except ValueError:
+            pass
+        else:
+            assert False, "should have raise ValueError"
+
+    async def test_send_message_to_teams_channel_works(self):
+        adapter = SimpleAdapterWithCreateConversation()
+
+        turn_context = TurnContext(adapter, ACTIVITY)
+        result = await TeamsInfo.send_message_to_teams_channel(
+            turn_context, ACTIVITY, "teamId123"
+        )
+        assert result[0].activity_id == "new_conversation_id"
+        assert result[1] == "reference123"
+
+    async def test_get_team_details_works_without_team_id(self):
+        adapter = SimpleAdapterWithCreateConversation()
+        ACTIVITY.channel_data = {}
+        turn_context = TurnContext(adapter, ACTIVITY)
+        result = TeamsInfo.get_team_id(turn_context)
+
+        assert result == ""
+
+    async def test_get_team_details_works_with_team_id(self):
+        adapter = SimpleAdapterWithCreateConversation()
+        team_id = "teamId123"
+        ACTIVITY.channel_data = {"team": {"id": team_id}}
+        turn_context = TurnContext(adapter, ACTIVITY)
+        result = TeamsInfo.get_team_id(turn_context)
+
+        assert result == team_id
+
+    async def test_get_team_details_without_team_id(self):
+        def create_conversation():
+            pass
+
+        adapter = SimpleAdapterWithCreateConversation(
+            call_create_conversation=create_conversation
+        )
+
+        turn_context = TurnContext(adapter, ACTIVITY)
+
+        try:
+            await TeamsInfo.get_team_details(turn_context)
+        except TypeError:
+            pass
+        else:
+            assert False, "should have raise TypeError"
+
+    async def test_get_team_channels_without_team_id(self):
+        def create_conversation():
+            pass
+
+        adapter = SimpleAdapterWithCreateConversation(
+            call_create_conversation=create_conversation
+        )
+
+        turn_context = TurnContext(adapter, ACTIVITY)
+
+        try:
+            await TeamsInfo.get_team_channels(turn_context)
+        except TypeError:
+            pass
+        else:
+            assert False, "should have raise TypeError"
+
+    async def test_get_paged_team_members_without_team_id(self):
+        def create_conversation():
+            pass
+
+        adapter = SimpleAdapterWithCreateConversation(
+            call_create_conversation=create_conversation
+        )
+
+        turn_context = TurnContext(adapter, ACTIVITY)
+
+        try:
+            await TeamsInfo.get_paged_team_members(turn_context)
+        except TypeError:
+            pass
+        else:
+            assert False, "should have raise TypeError"
+
+    async def test_get_team_members_without_team_id(self):
+        def create_conversation():
+            pass
+
+        adapter = SimpleAdapterWithCreateConversation(
+            call_create_conversation=create_conversation
+        )
+
+        turn_context = TurnContext(adapter, ACTIVITY)
+
+        try:
+            await TeamsInfo.get_team_member(turn_context)
+        except TypeError:
+            pass
+        else:
+            assert False, "should have raise TypeError"
+
+    async def test_get_team_members_without_member_id(self):
+        def create_conversation():
+            pass
+
+        adapter = SimpleAdapterWithCreateConversation(
+            call_create_conversation=create_conversation
+        )
+
+        turn_context = TurnContext(adapter, ACTIVITY)
+
+        try:
+            await TeamsInfo.get_team_member(turn_context, "teamId123")
+        except TypeError:
+            pass
+        else:
+            assert False, "should have raise TypeError"
+
+    async def test_get_participant(self):
+        adapter = SimpleAdapterWithCreateConversation()
+
+        activity = Activity(
+            type="message",
+            text="Test-get_participant",
+            channel_id=Channels.ms_teams,
+            from_property=ChannelAccount(aad_object_id="participantId-1"),
+            channel_data={
+                "meeting": {"id": "meetingId-1"},
+                "tenant": {"id": "tenantId-1"},
+            },
+            service_url="https://test.coffee",
+        )
+
+        turn_context = TurnContext(adapter, activity)
+        handler = TeamsActivityHandler()
+        await handler.on_turn(turn_context)
+
+
+class TestTeamsActivityHandler(TeamsActivityHandler):
+    async def on_turn(self, turn_context: TurnContext):
+        await super().on_turn(turn_context)
+
+        if turn_context.activity.text == "test_send_message_to_teams_channel":
+            await self.call_send_message_to_teams(turn_context)
+
+    async def call_send_message_to_teams(self, turn_context: TurnContext):
+        msg = MessageFactory.text("call_send_message_to_teams")
+        channel_id = "teams_channel_123"
+        reference = await TeamsInfo.send_message_to_teams_channel(
+            turn_context, msg, channel_id
+        )
+
+        assert reference[0].activity_id == "new_conversation_id"
+        assert reference[1] == "reference123"
diff --git a/libraries/botbuilder-core/tests/test_activity_handler.py b/libraries/botbuilder-core/tests/test_activity_handler.py
index 1e068e295..69ccfa830 100644
--- a/libraries/botbuilder-core/tests/test_activity_handler.py
+++ b/libraries/botbuilder-core/tests/test_activity_handler.py
@@ -1,98 +1,358 @@
-from typing import List
-
-import aiounittest
-from botbuilder.core import ActivityHandler, BotAdapter, TurnContext
-from botbuilder.schema import (
-    Activity,
-    ActivityTypes,
-    ChannelAccount,
-    ConversationReference,
-    MessageReaction,
-)
-
-
-class TestingActivityHandler(ActivityHandler):
-    def __init__(self):
-        self.record: List[str] = []
-
-    async def on_message_activity(self, turn_context: TurnContext):
-        self.record.append("on_message_activity")
-        return await super().on_message_activity(turn_context)
-
-    async def on_members_added_activity(
-        self, members_added: ChannelAccount, turn_context: TurnContext
-    ):
-        self.record.append("on_members_added_activity")
-        return await super().on_members_added_activity(members_added, turn_context)
-
-    async def on_members_removed_activity(
-        self, members_removed: ChannelAccount, turn_context: TurnContext
-    ):
-        self.record.append("on_members_removed_activity")
-        return await super().on_members_removed_activity(members_removed, turn_context)
-
-    async def on_message_reaction_activity(self, turn_context: TurnContext):
-        self.record.append("on_message_reaction_activity")
-        return await super().on_message_reaction_activity(turn_context)
-
-    async def on_reactions_added(
-        self, message_reactions: List[MessageReaction], turn_context: TurnContext
-    ):
-        self.record.append("on_reactions_added")
-        return await super().on_reactions_added(message_reactions, turn_context)
-
-    async def on_reactions_removed(
-        self, message_reactions: List[MessageReaction], turn_context: TurnContext
-    ):
-        self.record.append("on_reactions_removed")
-        return await super().on_reactions_removed(message_reactions, turn_context)
-
-    async def on_token_response_event(self, turn_context: TurnContext):
-        self.record.append("on_token_response_event")
-        return await super().on_token_response_event(turn_context)
-
-    async def on_event(self, turn_context: TurnContext):
-        self.record.append("on_event")
-        return await super().on_event(turn_context)
-
-    async def on_unrecognized_activity_type(self, turn_context: TurnContext):
-        self.record.append("on_unrecognized_activity_type")
-        return await super().on_unrecognized_activity_type(turn_context)
-
-
-class NotImplementedAdapter(BotAdapter):
-    async def delete_activity(
-        self, context: TurnContext, reference: ConversationReference
-    ):
-        raise NotImplementedError()
-
-    async def send_activities(self, context: TurnContext, activities: List[Activity]):
-        raise NotImplementedError()
-
-    async def update_activity(self, context: TurnContext, activity: Activity):
-        raise NotImplementedError()
-
-
-class TestActivityHandler(aiounittest.AsyncTestCase):
-    async def test_message_reaction(self):
-        # Note the code supports multiple adds and removes in the same activity though
-        # a channel may decide to send separate activities for each. For example, Teams
-        # sends separate activities each with a single add and a single remove.
-
-        # Arrange
-        activity = Activity(
-            type=ActivityTypes.message_reaction,
-            reactions_added=[MessageReaction(type="sad")],
-            reactions_removed=[MessageReaction(type="angry")],
-        )
-        turn_context = TurnContext(NotImplementedAdapter(), activity)
-
-        # Act
-        bot = TestingActivityHandler()
-        await bot.on_turn(turn_context)
-
-        # Assert
-        assert len(bot.record) == 3
-        assert bot.record[0] == "on_message_reaction_activity"
-        assert bot.record[1] == "on_reactions_added"
-        assert bot.record[2] == "on_reactions_removed"
+from http import HTTPStatus
+from typing import List
+
+import aiounittest
+from botframework.connector import ConnectorClient
+from botframework.connector.auth import AppCredentials
+
+from botbuilder.core import ActivityHandler, BotAdapter, TurnContext, InvokeResponse
+from botbuilder.schema import (
+    Activity,
+    ActivityTypes,
+    ChannelAccount,
+    ConversationReference,
+    MessageReaction,
+    ResourceResponse,
+    HealthCheckResponse,
+)
+
+from botbuilder.core.bot_framework_adapter import USER_AGENT
+
+
+class TestingActivityHandler(ActivityHandler):
+    __test__ = False
+
+    def __init__(self):
+        self.record: List[str] = []
+
+    async def on_message_activity(self, turn_context: TurnContext):
+        self.record.append("on_message_activity")
+        return await super().on_message_activity(turn_context)
+
+    async def on_members_added_activity(
+        self, members_added: ChannelAccount, turn_context: TurnContext
+    ):
+        self.record.append("on_members_added_activity")
+        return await super().on_members_added_activity(members_added, turn_context)
+
+    async def on_members_removed_activity(
+        self, members_removed: ChannelAccount, turn_context: TurnContext
+    ):
+        self.record.append("on_members_removed_activity")
+        return await super().on_members_removed_activity(members_removed, turn_context)
+
+    async def on_message_reaction_activity(self, turn_context: TurnContext):
+        self.record.append("on_message_reaction_activity")
+        return await super().on_message_reaction_activity(turn_context)
+
+    async def on_reactions_added(
+        self, message_reactions: List[MessageReaction], turn_context: TurnContext
+    ):
+        self.record.append("on_reactions_added")
+        return await super().on_reactions_added(message_reactions, turn_context)
+
+    async def on_reactions_removed(
+        self, message_reactions: List[MessageReaction], turn_context: TurnContext
+    ):
+        self.record.append("on_reactions_removed")
+        return await super().on_reactions_removed(message_reactions, turn_context)
+
+    async def on_token_response_event(self, turn_context: TurnContext):
+        self.record.append("on_token_response_event")
+        return await super().on_token_response_event(turn_context)
+
+    async def on_event(self, turn_context: TurnContext):
+        self.record.append("on_event")
+        return await super().on_event(turn_context)
+
+    async def on_end_of_conversation_activity(self, turn_context: TurnContext):
+        self.record.append("on_end_of_conversation_activity")
+        return await super().on_end_of_conversation_activity(turn_context)
+
+    async def on_typing_activity(self, turn_context: TurnContext):
+        self.record.append("on_typing_activity")
+        return await super().on_typing_activity(turn_context)
+
+    async def on_installation_update(self, turn_context: TurnContext):
+        self.record.append("on_installation_update")
+        return await super().on_installation_update(turn_context)
+
+    async def on_installation_update_add(self, turn_context: TurnContext):
+        self.record.append("on_installation_update_add")
+        return await super().on_installation_update_add(turn_context)
+
+    async def on_installation_update_remove(self, turn_context: TurnContext):
+        self.record.append("on_installation_update_remove")
+        return await super().on_installation_update_remove(turn_context)
+
+    async def on_unrecognized_activity_type(self, turn_context: TurnContext):
+        self.record.append("on_unrecognized_activity_type")
+        return await super().on_unrecognized_activity_type(turn_context)
+
+    async def on_invoke_activity(self, turn_context: TurnContext):
+        self.record.append("on_invoke_activity")
+        if turn_context.activity.name == "some.random.invoke":
+            return self._create_invoke_response()
+
+        return await super().on_invoke_activity(turn_context)
+
+    async def on_sign_in_invoke(  # pylint: disable=unused-argument
+        self, turn_context: TurnContext
+    ):
+        self.record.append("on_sign_in_invoke")
+        return
+
+    async def on_healthcheck(self, turn_context: TurnContext) -> HealthCheckResponse:
+        self.record.append("on_healthcheck")
+        return HealthCheckResponse()
+
+
+class NotImplementedAdapter(BotAdapter):
+    async def delete_activity(
+        self, context: TurnContext, reference: ConversationReference
+    ):
+        raise NotImplementedError()
+
+    async def send_activities(
+        self, context: TurnContext, activities: List[Activity]
+    ) -> List[ResourceResponse]:
+        raise NotImplementedError()
+
+    async def update_activity(self, context: TurnContext, activity: Activity):
+        raise NotImplementedError()
+
+
+class TestInvokeAdapter(NotImplementedAdapter):
+    def __init__(self, on_turn_error=None, activity: Activity = None):
+        super().__init__(on_turn_error)
+
+        self.activity = activity
+
+    async def delete_activity(
+        self, context: TurnContext, reference: ConversationReference
+    ):
+        raise NotImplementedError()
+
+    async def send_activities(
+        self, context: TurnContext, activities: List[Activity]
+    ) -> List[ResourceResponse]:
+        self.activity = next(
+            (
+                activity
+                for activity in activities
+                if activity.type == ActivityTypes.invoke_response
+            ),
+            None,
+        )
+
+        return []
+
+    async def update_activity(self, context: TurnContext, activity: Activity):
+        raise NotImplementedError()
+
+
+class MockConnectorClient(ConnectorClient):
+    def __init__(self):
+        super().__init__(
+            credentials=MockCredentials(), base_url="http://tempuri.org/whatever"
+        )
+
+
+class MockCredentials(AppCredentials):
+    def get_access_token(self, force_refresh: bool = False) -> str:
+        return "awesome"
+
+
+class TestActivityHandler(aiounittest.AsyncTestCase):
+    async def test_message_reaction(self):
+        # Note the code supports multiple adds and removes in the same activity though
+        # a channel may decide to send separate activities for each. For example, Teams
+        # sends separate activities each with a single add and a single remove.
+
+        # Arrange
+        activity = Activity(
+            type=ActivityTypes.message_reaction,
+            reactions_added=[MessageReaction(type="sad")],
+            reactions_removed=[MessageReaction(type="angry")],
+        )
+        turn_context = TurnContext(NotImplementedAdapter(), activity)
+
+        # Act
+        bot = TestingActivityHandler()
+        await bot.on_turn(turn_context)
+
+        # Assert
+        assert len(bot.record) == 3
+        assert bot.record[0] == "on_message_reaction_activity"
+        assert bot.record[1] == "on_reactions_added"
+        assert bot.record[2] == "on_reactions_removed"
+
+    async def test_invoke(self):
+        activity = Activity(type=ActivityTypes.invoke, name="some.random.invoke")
+
+        adapter = TestInvokeAdapter()
+        turn_context = TurnContext(adapter, activity)
+
+        # Act
+        bot = TestingActivityHandler()
+        await bot.on_turn(turn_context)
+
+        assert len(bot.record) == 1
+        assert bot.record[0] == "on_invoke_activity"
+        assert adapter.activity.value.status == int(HTTPStatus.OK)
+
+    async def test_invoke_should_not_match(self):
+        activity = Activity(type=ActivityTypes.invoke, name="should.not.match")
+
+        adapter = TestInvokeAdapter()
+        turn_context = TurnContext(adapter, activity)
+
+        # Act
+        bot = TestingActivityHandler()
+        await bot.on_turn(turn_context)
+
+        assert len(bot.record) == 1
+        assert bot.record[0] == "on_invoke_activity"
+        assert adapter.activity.value.status == int(HTTPStatus.NOT_IMPLEMENTED)
+
+    async def test_on_end_of_conversation_activity(self):
+        activity = Activity(type=ActivityTypes.end_of_conversation)
+
+        adapter = TestInvokeAdapter()
+        turn_context = TurnContext(adapter, activity)
+
+        # Act
+        bot = TestingActivityHandler()
+        await bot.on_turn(turn_context)
+
+        assert len(bot.record) == 1
+        assert bot.record[0] == "on_end_of_conversation_activity"
+
+    async def test_typing_activity(self):
+        activity = Activity(type=ActivityTypes.typing)
+
+        adapter = TestInvokeAdapter()
+        turn_context = TurnContext(adapter, activity)
+
+        # Act
+        bot = TestingActivityHandler()
+        await bot.on_turn(turn_context)
+
+        assert len(bot.record) == 1
+        assert bot.record[0] == "on_typing_activity"
+
+    async def test_on_installation_update(self):
+        activity = Activity(type=ActivityTypes.installation_update)
+
+        adapter = TestInvokeAdapter()
+        turn_context = TurnContext(adapter, activity)
+
+        # Act
+        bot = TestingActivityHandler()
+        await bot.on_turn(turn_context)
+
+        assert len(bot.record) == 1
+        assert bot.record[0] == "on_installation_update"
+
+    async def test_on_installation_update_add(self):
+        activity = Activity(type=ActivityTypes.installation_update, action="add")
+
+        adapter = TestInvokeAdapter()
+        turn_context = TurnContext(adapter, activity)
+
+        # Act
+        bot = TestingActivityHandler()
+        await bot.on_turn(turn_context)
+
+        assert len(bot.record) == 2
+        assert bot.record[0] == "on_installation_update"
+        assert bot.record[1] == "on_installation_update_add"
+
+    async def test_on_installation_update_add_upgrade(self):
+        activity = Activity(
+            type=ActivityTypes.installation_update, action="add-upgrade"
+        )
+
+        adapter = TestInvokeAdapter()
+        turn_context = TurnContext(adapter, activity)
+
+        # Act
+        bot = TestingActivityHandler()
+        await bot.on_turn(turn_context)
+
+        assert len(bot.record) == 2
+        assert bot.record[0] == "on_installation_update"
+        assert bot.record[1] == "on_installation_update_add"
+
+    async def test_on_installation_update_remove(self):
+        activity = Activity(type=ActivityTypes.installation_update, action="remove")
+
+        adapter = TestInvokeAdapter()
+        turn_context = TurnContext(adapter, activity)
+
+        # Act
+        bot = TestingActivityHandler()
+        await bot.on_turn(turn_context)
+
+        assert len(bot.record) == 2
+        assert bot.record[0] == "on_installation_update"
+        assert bot.record[1] == "on_installation_update_remove"
+
+    async def test_on_installation_update_remove_upgrade(self):
+        activity = Activity(
+            type=ActivityTypes.installation_update, action="remove-upgrade"
+        )
+
+        adapter = TestInvokeAdapter()
+        turn_context = TurnContext(adapter, activity)
+
+        # Act
+        bot = TestingActivityHandler()
+        await bot.on_turn(turn_context)
+
+        assert len(bot.record) == 2
+        assert bot.record[0] == "on_installation_update"
+        assert bot.record[1] == "on_installation_update_remove"
+
+    async def test_healthcheck(self):
+        activity = Activity(type=ActivityTypes.invoke, name="healthcheck",)
+
+        adapter = TestInvokeAdapter()
+        turn_context = TurnContext(adapter, activity)
+
+        bot = ActivityHandler()
+        await bot.on_turn(turn_context)
+
+        self.assertIsNotNone(adapter.activity)
+        self.assertIsInstance(adapter.activity.value, InvokeResponse)
+        self.assertEqual(adapter.activity.value.status, 200)
+
+        response = HealthCheckResponse.deserialize(adapter.activity.value.body)
+        self.assertTrue(response.health_results.success)
+        self.assertTrue(response.health_results.messages)
+        self.assertEqual(response.health_results.messages[0], "Health check succeeded.")
+
+    async def test_healthcheck_with_connector(self):
+        activity = Activity(type=ActivityTypes.invoke, name="healthcheck",)
+
+        adapter = TestInvokeAdapter()
+        turn_context = TurnContext(adapter, activity)
+
+        mock_connector_client = MockConnectorClient()
+        turn_context.turn_state[
+            BotAdapter.BOT_CONNECTOR_CLIENT_KEY
+        ] = mock_connector_client
+
+        bot = ActivityHandler()
+        await bot.on_turn(turn_context)
+
+        self.assertIsNotNone(adapter.activity)
+        self.assertIsInstance(adapter.activity.value, InvokeResponse)
+        self.assertEqual(adapter.activity.value.status, 200)
+
+        response = HealthCheckResponse.deserialize(adapter.activity.value.body)
+        self.assertTrue(response.health_results.success)
+        self.assertEqual(response.health_results.authorization, "Bearer awesome")
+        self.assertEqual(response.health_results.user_agent, USER_AGENT)
+        self.assertTrue(response.health_results.messages)
+        self.assertEqual(response.health_results.messages[0], "Health check succeeded.")
diff --git a/libraries/botbuilder-core/tests/test_auto_save_middleware.py b/libraries/botbuilder-core/tests/test_auto_save_middleware.py
index 4e28c68be..275bd6d91 100644
--- a/libraries/botbuilder-core/tests/test_auto_save_middleware.py
+++ b/libraries/botbuilder-core/tests/test_auto_save_middleware.py
@@ -107,7 +107,7 @@ async def test_should_support_plugins_passed_to_constructor(self):
         assert foo_state.write_called, "save_all_changes() not called."
 
     async def test_should_not_add_any_bot_state_on_construction_if_none_are_passed_in(
-        self
+        self,
     ):
         middleware = AutoSaveStateMiddleware()
         assert (
diff --git a/libraries/botbuilder-core/tests/test_bot_adapter.py b/libraries/botbuilder-core/tests/test_bot_adapter.py
index 86752482b..5f524dca2 100644
--- a/libraries/botbuilder-core/tests/test_bot_adapter.py
+++ b/libraries/botbuilder-core/tests/test_bot_adapter.py
@@ -1,86 +1,86 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import uuid
-from typing import List
-import aiounittest
-
-from botbuilder.core import TurnContext
-from botbuilder.core.adapters import TestAdapter
-from botbuilder.schema import (
-    Activity,
-    ConversationAccount,
-    ConversationReference,
-    ChannelAccount,
-)
-
-from simple_adapter import SimpleAdapter
-from call_counting_middleware import CallCountingMiddleware
-from test_message import TestMessage
-
-
-class TestBotAdapter(aiounittest.AsyncTestCase):
-    def test_adapter_single_use(self):
-        adapter = SimpleAdapter()
-        adapter.use(CallCountingMiddleware())
-
-    def test_adapter_use_chaining(self):
-        adapter = SimpleAdapter()
-        adapter.use(CallCountingMiddleware()).use(CallCountingMiddleware())
-
-    async def test_pass_resource_responses_through(self):
-        def validate_responses(  # pylint: disable=unused-argument
-            activities: List[Activity]
-        ):
-            pass  # no need to do anything.
-
-        adapter = SimpleAdapter(call_on_send=validate_responses)
-        context = TurnContext(adapter, Activity())
-
-        activity_id = str(uuid.uuid1())
-        activity = TestMessage.message(activity_id)
-
-        resource_response = await context.send_activity(activity)
-        self.assertTrue(
-            resource_response.id == activity_id, "Incorrect response Id returned"
-        )
-
-    async def test_continue_conversation_direct_msg(self):
-        callback_invoked = False
-        adapter = TestAdapter()
-        reference = ConversationReference(
-            activity_id="activityId",
-            bot=ChannelAccount(id="channelId", name="testChannelAccount", role="bot"),
-            channel_id="testChannel",
-            service_url="testUrl",
-            conversation=ConversationAccount(
-                conversation_type="",
-                id="testConversationId",
-                is_group=False,
-                name="testConversationName",
-                role="user",
-            ),
-            user=ChannelAccount(id="channelId", name="testChannelAccount", role="bot"),
-        )
-
-        async def continue_callback(turn_context):  # pylint: disable=unused-argument
-            nonlocal callback_invoked
-            callback_invoked = True
-
-        await adapter.continue_conversation("MyBot", reference, continue_callback)
-        self.assertTrue(callback_invoked)
-
-    async def test_turn_error(self):
-        async def on_error(turn_context: TurnContext, err: Exception):
-            nonlocal self
-            self.assertIsNotNone(turn_context, "turn_context not found.")
-            self.assertIsNotNone(err, "error not found.")
-            self.assertEqual(err.__class__, Exception, "unexpected error thrown.")
-
-        adapter = SimpleAdapter()
-        adapter.on_turn_error = on_error
-
-        def handler(context: TurnContext):  # pylint: disable=unused-argument
-            raise Exception
-
-        await adapter.process_request(TestMessage.message(), handler)
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import uuid
+from typing import List
+import aiounittest
+
+from botbuilder.core import TurnContext
+from botbuilder.core.adapters import TestAdapter
+from botbuilder.schema import (
+    Activity,
+    ConversationAccount,
+    ConversationReference,
+    ChannelAccount,
+)
+
+from simple_adapter import SimpleAdapter
+from call_counting_middleware import CallCountingMiddleware
+from test_message import TestMessage
+
+
+class TestBotAdapter(aiounittest.AsyncTestCase):
+    def test_adapter_single_use(self):
+        adapter = SimpleAdapter()
+        adapter.use(CallCountingMiddleware())
+
+    def test_adapter_use_chaining(self):
+        adapter = SimpleAdapter()
+        adapter.use(CallCountingMiddleware()).use(CallCountingMiddleware())
+
+    async def test_pass_resource_responses_through(self):
+        def validate_responses(  # pylint: disable=unused-argument
+            activities: List[Activity],
+        ):
+            pass  # no need to do anything.
+
+        adapter = SimpleAdapter(call_on_send=validate_responses)
+        context = TurnContext(adapter, Activity())
+
+        activity_id = str(uuid.uuid1())
+        activity = TestMessage.message(activity_id)
+
+        resource_response = await context.send_activity(activity)
+        self.assertTrue(
+            resource_response.id != activity_id, "Incorrect response Id returned"
+        )
+
+    async def test_continue_conversation_direct_msg(self):
+        callback_invoked = False
+        adapter = TestAdapter()
+        reference = ConversationReference(
+            activity_id="activityId",
+            bot=ChannelAccount(id="channelId", name="testChannelAccount", role="bot"),
+            channel_id="testChannel",
+            service_url="testUrl",
+            conversation=ConversationAccount(
+                conversation_type="",
+                id="testConversationId",
+                is_group=False,
+                name="testConversationName",
+                role="user",
+            ),
+            user=ChannelAccount(id="channelId", name="testChannelAccount", role="bot"),
+        )
+
+        async def continue_callback(turn_context):  # pylint: disable=unused-argument
+            nonlocal callback_invoked
+            callback_invoked = True
+
+        await adapter.continue_conversation(reference, continue_callback, "MyBot")
+        self.assertTrue(callback_invoked)
+
+    async def test_turn_error(self):
+        async def on_error(turn_context: TurnContext, err: Exception):
+            nonlocal self
+            self.assertIsNotNone(turn_context, "turn_context not found.")
+            self.assertIsNotNone(err, "error not found.")
+            self.assertEqual(err.__class__, Exception, "unexpected error thrown.")
+
+        adapter = SimpleAdapter()
+        adapter.on_turn_error = on_error
+
+        def handler(context: TurnContext):  # pylint: disable=unused-argument
+            raise Exception
+
+        await adapter.process_request(TestMessage.message(), handler)
diff --git a/libraries/botbuilder-core/tests/test_bot_framework_adapter.py b/libraries/botbuilder-core/tests/test_bot_framework_adapter.py
index 6532b1e52..fe4f55e3f 100644
--- a/libraries/botbuilder-core/tests/test_bot_framework_adapter.py
+++ b/libraries/botbuilder-core/tests/test_bot_framework_adapter.py
@@ -1,289 +1,733 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from copy import copy, deepcopy
-from unittest.mock import Mock
-import unittest
-import uuid
-import aiounittest
-
-from botbuilder.core import (
-    BotFrameworkAdapter,
-    BotFrameworkAdapterSettings,
-    TurnContext,
-)
-from botbuilder.schema import (
-    Activity,
-    ActivityTypes,
-    ConversationAccount,
-    ConversationReference,
-    ConversationResourceResponse,
-    ChannelAccount,
-)
-from botframework.connector.aio import ConnectorClient
-from botframework.connector.auth import ClaimsIdentity
-
-REFERENCE = ConversationReference(
-    activity_id="1234",
-    channel_id="test",
-    service_url="https://example.org/channel",
-    user=ChannelAccount(id="user", name="User Name"),
-    bot=ChannelAccount(id="bot", name="Bot Name"),
-    conversation=ConversationAccount(id="convo1"),
-)
-
-TEST_ACTIVITY = Activity(text="test", type=ActivityTypes.message)
-
-INCOMING_MESSAGE = TurnContext.apply_conversation_reference(
-    copy(TEST_ACTIVITY), REFERENCE, True
-)
-OUTGOING_MESSAGE = TurnContext.apply_conversation_reference(
-    copy(TEST_ACTIVITY), REFERENCE
-)
-INCOMING_INVOKE = TurnContext.apply_conversation_reference(
-    Activity(type=ActivityTypes.invoke), REFERENCE, True
-)
-
-
-class AdapterUnderTest(BotFrameworkAdapter):
-    def __init__(self, settings=None):
-        super().__init__(settings)
-        self.tester = aiounittest.AsyncTestCase()
-        self.fail_auth = False
-        self.fail_operation = False
-        self.expect_auth_header = ""
-        self.new_service_url = None
-
-    def aux_test_authenticate_request(self, request: Activity, auth_header: str):
-        return super().authenticate_request(request, auth_header)
-
-    def aux_test_create_connector_client(self, service_url: str):
-        return super().create_connector_client(service_url)
-
-    async def authenticate_request(self, request: Activity, auth_header: str):
-        self.tester.assertIsNotNone(
-            request, "authenticate_request() not passed request."
-        )
-        self.tester.assertEqual(
-            auth_header,
-            self.expect_auth_header,
-            "authenticateRequest() not passed expected authHeader.",
-        )
-        return not self.fail_auth
-
-    def create_connector_client(self, service_url: str) -> ConnectorClient:
-        self.tester.assertIsNotNone(
-            service_url, "create_connector_client() not passed service_url."
-        )
-        connector_client_mock = Mock()
-
-        async def mock_reply_to_activity(conversation_id, activity_id, activity):
-            nonlocal self
-            self.tester.assertIsNotNone(
-                conversation_id, "reply_to_activity not passed conversation_id"
-            )
-            self.tester.assertIsNotNone(
-                activity_id, "reply_to_activity not passed activity_id"
-            )
-            self.tester.assertIsNotNone(
-                activity, "reply_to_activity not passed activity"
-            )
-            return not self.fail_auth
-
-        async def mock_send_to_conversation(conversation_id, activity):
-            nonlocal self
-            self.tester.assertIsNotNone(
-                conversation_id, "send_to_conversation not passed conversation_id"
-            )
-            self.tester.assertIsNotNone(
-                activity, "send_to_conversation not passed activity"
-            )
-            return not self.fail_auth
-
-        async def mock_update_activity(conversation_id, activity_id, activity):
-            nonlocal self
-            self.tester.assertIsNotNone(
-                conversation_id, "update_activity not passed conversation_id"
-            )
-            self.tester.assertIsNotNone(
-                activity_id, "update_activity not passed activity_id"
-            )
-            self.tester.assertIsNotNone(activity, "update_activity not passed activity")
-            return not self.fail_auth
-
-        async def mock_delete_activity(conversation_id, activity_id):
-            nonlocal self
-            self.tester.assertIsNotNone(
-                conversation_id, "delete_activity not passed conversation_id"
-            )
-            self.tester.assertIsNotNone(
-                activity_id, "delete_activity not passed activity_id"
-            )
-            return not self.fail_auth
-
-        async def mock_create_conversation(parameters):
-            nonlocal self
-            self.tester.assertIsNotNone(
-                parameters, "create_conversation not passed parameters"
-            )
-            response = ConversationResourceResponse(
-                activity_id=REFERENCE.activity_id,
-                service_url=REFERENCE.service_url,
-                id=uuid.uuid4(),
-            )
-            return response
-
-        connector_client_mock.conversations.reply_to_activity.side_effect = (
-            mock_reply_to_activity
-        )
-        connector_client_mock.conversations.send_to_conversation.side_effect = (
-            mock_send_to_conversation
-        )
-        connector_client_mock.conversations.update_activity.side_effect = (
-            mock_update_activity
-        )
-        connector_client_mock.conversations.delete_activity.side_effect = (
-            mock_delete_activity
-        )
-        connector_client_mock.conversations.create_conversation.side_effect = (
-            mock_create_conversation
-        )
-
-        return connector_client_mock
-
-
-async def process_activity(
-    channel_id: str, channel_data_tenant_id: str, conversation_tenant_id: str
-):
-    activity = None
-    mock_claims = unittest.mock.create_autospec(ClaimsIdentity)
-    mock_credential_provider = unittest.mock.create_autospec(
-        BotFrameworkAdapterSettings
-    )
-
-    sut = BotFrameworkAdapter(mock_credential_provider)
-
-    async def aux_func(context):
-        nonlocal activity
-        activity = context.Activity
-
-    await sut.process_activity(
-        Activity(
-            channel_id=channel_id,
-            service_url="https://smba.trafficmanager.net/amer/",
-            channel_data={"tenant": {"id": channel_data_tenant_id}},
-            conversation=ConversationAccount(tenant_id=conversation_tenant_id),
-        ),
-        mock_claims,
-        aux_func,
-    )
-    return activity
-
-
-class TestBotFrameworkAdapter(aiounittest.AsyncTestCase):
-    def test_should_create_connector_client(self):
-        adapter = AdapterUnderTest()
-        client = adapter.aux_test_create_connector_client(REFERENCE.service_url)
-        self.assertIsNotNone(client, "client not returned.")
-        self.assertIsNotNone(client.conversations, "invalid client returned.")
-
-    async def test_should_process_activity(self):
-        called = False
-        adapter = AdapterUnderTest()
-
-        async def aux_func_assert_context(context):
-            self.assertIsNotNone(context, "context not passed.")
-            nonlocal called
-            called = True
-
-        await adapter.process_activity(INCOMING_MESSAGE, "", aux_func_assert_context)
-        self.assertTrue(called, "bot logic not called.")
-
-    async def test_should_update_activity(self):
-        adapter = AdapterUnderTest()
-        context = TurnContext(adapter, INCOMING_MESSAGE)
-        self.assertTrue(
-            await adapter.update_activity(context, INCOMING_MESSAGE),
-            "Activity not updated.",
-        )
-
-    async def test_should_fail_to_update_activity_if_service_url_missing(self):
-        adapter = AdapterUnderTest()
-        context = TurnContext(adapter, INCOMING_MESSAGE)
-        cpy = deepcopy(INCOMING_MESSAGE)
-        cpy.service_url = None
-        with self.assertRaises(Exception) as _:
-            await adapter.update_activity(context, cpy)
-
-    async def test_should_fail_to_update_activity_if_conversation_missing(self):
-        adapter = AdapterUnderTest()
-        context = TurnContext(adapter, INCOMING_MESSAGE)
-        cpy = deepcopy(INCOMING_MESSAGE)
-        cpy.conversation = None
-        with self.assertRaises(Exception) as _:
-            await adapter.update_activity(context, cpy)
-
-    async def test_should_fail_to_update_activity_if_activity_id_missing(self):
-        adapter = AdapterUnderTest()
-        context = TurnContext(adapter, INCOMING_MESSAGE)
-        cpy = deepcopy(INCOMING_MESSAGE)
-        cpy.id = None
-        with self.assertRaises(Exception) as _:
-            await adapter.update_activity(context, cpy)
-
-    async def test_should_migrate_tenant_id_for_msteams(self):
-        incoming = TurnContext.apply_conversation_reference(
-            activity=Activity(
-                type=ActivityTypes.message,
-                text="foo",
-                channel_data={"tenant": {"id": "1234"}},
-            ),
-            reference=REFERENCE,
-            is_incoming=True,
-        )
-
-        incoming.channel_id = "msteams"
-        adapter = AdapterUnderTest()
-
-        async def aux_func_assert_tenant_id_copied(context):
-            self.assertEqual(
-                context.activity.conversation.tenant_id,
-                "1234",
-                "should have copied tenant id from "
-                "channel_data to conversation address",
-            )
-
-        await adapter.process_activity(incoming, "", aux_func_assert_tenant_id_copied)
-
-    async def test_should_create_valid_conversation_for_msteams(self):
-
-        tenant_id = "testTenant"
-
-        reference = deepcopy(REFERENCE)
-        reference.conversation.tenant_id = tenant_id
-        reference.channel_data = {"tenant": {"id": tenant_id}}
-        adapter = AdapterUnderTest()
-
-        called = False
-
-        async def aux_func_assert_valid_conversation(context):
-            self.assertIsNotNone(context, "context not passed")
-            self.assertIsNotNone(context.activity, "context has no request")
-            self.assertIsNotNone(
-                context.activity.conversation, "request has invalid conversation"
-            )
-            self.assertEqual(
-                context.activity.conversation.tenant_id,
-                tenant_id,
-                "request has invalid tenant_id on conversation",
-            )
-            self.assertEqual(
-                context.activity.channel_data["tenant"]["id"],
-                tenant_id,
-                "request has invalid tenant_id in channel_data",
-            )
-            nonlocal called
-            called = True
-
-        await adapter.create_conversation(reference, aux_func_assert_valid_conversation)
-        self.assertTrue(called, "bot logic not called.")
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from copy import copy, deepcopy
+from unittest.mock import Mock
+import unittest
+import uuid
+import aiounittest
+
+from botbuilder.core import (
+    BotFrameworkAdapter,
+    BotFrameworkAdapterSettings,
+    TurnContext,
+)
+from botbuilder.schema import (
+    Activity,
+    ActivityTypes,
+    ConversationAccount,
+    ConversationReference,
+    ConversationResourceResponse,
+    ChannelAccount,
+    DeliveryModes,
+    ExpectedReplies,
+    CallerIdConstants,
+)
+from botframework.connector.aio import ConnectorClient
+from botframework.connector.auth import (
+    ClaimsIdentity,
+    AuthenticationConstants,
+    AppCredentials,
+    CredentialProvider,
+    SimpleChannelProvider,
+    GovernmentConstants,
+    SimpleCredentialProvider,
+)
+
+REFERENCE = ConversationReference(
+    activity_id="1234",
+    channel_id="test",
+    locale="en-uS",  # Intentionally oddly-cased to check that it isn't defaulted somewhere, but tests stay in English
+    service_url="https://example.org/channel",
+    user=ChannelAccount(id="user", name="User Name"),
+    bot=ChannelAccount(id="bot", name="Bot Name"),
+    conversation=ConversationAccount(id="convo1"),
+)
+
+TEST_ACTIVITY = Activity(text="test", type=ActivityTypes.message)
+
+INCOMING_MESSAGE = TurnContext.apply_conversation_reference(
+    copy(TEST_ACTIVITY), REFERENCE, True
+)
+OUTGOING_MESSAGE = TurnContext.apply_conversation_reference(
+    copy(TEST_ACTIVITY), REFERENCE
+)
+INCOMING_INVOKE = TurnContext.apply_conversation_reference(
+    Activity(type=ActivityTypes.invoke), REFERENCE, True
+)
+
+
+class AdapterUnderTest(BotFrameworkAdapter):
+    def __init__(self, settings=None):
+        super().__init__(settings)
+        self.tester = aiounittest.AsyncTestCase()
+        self.fail_auth = False
+        self.fail_operation = False
+        self.expect_auth_header = ""
+        self.new_service_url = None
+        self.connector_client_mock = None
+
+    def aux_test_authenticate_request(self, request: Activity, auth_header: str):
+        return super()._authenticate_request(request, auth_header)
+
+    async def aux_test_create_connector_client(self, service_url: str):
+        return await super().create_connector_client(service_url)
+
+    async def _authenticate_request(
+        self, request: Activity, auth_header: str
+    ) -> ClaimsIdentity:
+        self.tester.assertIsNotNone(
+            request, "authenticate_request() not passed request."
+        )
+        self.tester.assertEqual(
+            auth_header,
+            self.expect_auth_header,
+            "authenticateRequest() not passed expected authHeader.",
+        )
+
+        if self.fail_auth:
+            raise PermissionError("Unauthorized Access. Request is not authorized")
+
+        return ClaimsIdentity(
+            claims={
+                AuthenticationConstants.AUDIENCE_CLAIM: self.settings.app_id,
+                AuthenticationConstants.APP_ID_CLAIM: self.settings.app_id,
+            },
+            is_authenticated=True,
+        )
+
+    async def create_connector_client(
+        self,
+        service_url: str,
+        identity: ClaimsIdentity = None,  # pylint: disable=unused-argument
+        audience: str = None,  # pylint: disable=unused-argument
+    ) -> ConnectorClient:
+        return self._get_or_create_connector_client(service_url, None)
+
+    def _get_or_create_connector_client(
+        self, service_url: str, credentials: AppCredentials
+    ) -> ConnectorClient:
+        self.tester.assertIsNotNone(
+            service_url, "create_connector_client() not passed service_url."
+        )
+
+        if self.connector_client_mock:
+            return self.connector_client_mock
+        self.connector_client_mock = Mock()
+
+        async def mock_reply_to_activity(conversation_id, activity_id, activity):
+            nonlocal self
+            self.tester.assertIsNotNone(
+                conversation_id, "reply_to_activity not passed conversation_id"
+            )
+            self.tester.assertIsNotNone(
+                activity_id, "reply_to_activity not passed activity_id"
+            )
+            self.tester.assertIsNotNone(
+                activity, "reply_to_activity not passed activity"
+            )
+            return not self.fail_auth
+
+        async def mock_send_to_conversation(conversation_id, activity):
+            nonlocal self
+            self.tester.assertIsNotNone(
+                conversation_id, "send_to_conversation not passed conversation_id"
+            )
+            self.tester.assertIsNotNone(
+                activity, "send_to_conversation not passed activity"
+            )
+            return not self.fail_auth
+
+        async def mock_update_activity(conversation_id, activity_id, activity):
+            nonlocal self
+            self.tester.assertIsNotNone(
+                conversation_id, "update_activity not passed conversation_id"
+            )
+            self.tester.assertIsNotNone(
+                activity_id, "update_activity not passed activity_id"
+            )
+            self.tester.assertIsNotNone(activity, "update_activity not passed activity")
+            return not self.fail_auth
+
+        async def mock_delete_activity(conversation_id, activity_id):
+            nonlocal self
+            self.tester.assertIsNotNone(
+                conversation_id, "delete_activity not passed conversation_id"
+            )
+            self.tester.assertIsNotNone(
+                activity_id, "delete_activity not passed activity_id"
+            )
+            return not self.fail_auth
+
+        async def mock_create_conversation(parameters):
+            nonlocal self
+            self.tester.assertIsNotNone(
+                parameters, "create_conversation not passed parameters"
+            )
+            response = ConversationResourceResponse(
+                activity_id=REFERENCE.activity_id,
+                service_url=REFERENCE.service_url,
+                id=uuid.uuid4(),
+            )
+            return response
+
+        self.connector_client_mock.conversations.reply_to_activity.side_effect = (
+            mock_reply_to_activity
+        )
+        self.connector_client_mock.conversations.send_to_conversation.side_effect = (
+            mock_send_to_conversation
+        )
+        self.connector_client_mock.conversations.update_activity.side_effect = (
+            mock_update_activity
+        )
+        self.connector_client_mock.conversations.delete_activity.side_effect = (
+            mock_delete_activity
+        )
+        self.connector_client_mock.conversations.create_conversation.side_effect = (
+            mock_create_conversation
+        )
+
+        return self.connector_client_mock
+
+
+async def process_activity(
+    channel_id: str, channel_data_tenant_id: str, conversation_tenant_id: str
+):
+    activity = None
+    mock_claims = unittest.mock.create_autospec(ClaimsIdentity)
+    mock_credential_provider = unittest.mock.create_autospec(
+        BotFrameworkAdapterSettings
+    )
+
+    sut = BotFrameworkAdapter(mock_credential_provider)
+
+    async def aux_func(context):
+        nonlocal activity
+        activity = context.Activity
+
+    await sut.process_activity(
+        Activity(
+            channel_id=channel_id,
+            service_url="https://smba.trafficmanager.net/amer/",
+            channel_data={"tenant": {"id": channel_data_tenant_id}},
+            conversation=ConversationAccount(tenant_id=conversation_tenant_id),
+        ),
+        mock_claims,
+        aux_func,
+    )
+    return activity
+
+
+class TestBotFrameworkAdapter(aiounittest.AsyncTestCase):
+    async def test_should_create_connector_client(self):
+        adapter = AdapterUnderTest()
+        client = await adapter.aux_test_create_connector_client(REFERENCE.service_url)
+        self.assertIsNotNone(client, "client not returned.")
+        self.assertIsNotNone(client.conversations, "invalid client returned.")
+
+    async def test_should_process_activity(self):
+        called = False
+        adapter = AdapterUnderTest()
+
+        async def aux_func_assert_context(context):
+            self.assertIsNotNone(context, "context not passed.")
+            nonlocal called
+            called = True
+
+        await adapter.process_activity(INCOMING_MESSAGE, "", aux_func_assert_context)
+        self.assertTrue(called, "bot logic not called.")
+
+    async def test_should_fail_to_update_activity_if_service_url_missing(self):
+        adapter = AdapterUnderTest()
+        context = TurnContext(adapter, INCOMING_MESSAGE)
+        cpy = deepcopy(INCOMING_MESSAGE)
+        cpy.service_url = None
+        with self.assertRaises(Exception) as _:
+            await adapter.update_activity(context, cpy)
+
+    async def test_should_fail_to_update_activity_if_conversation_missing(self):
+        adapter = AdapterUnderTest()
+        context = TurnContext(adapter, INCOMING_MESSAGE)
+        cpy = deepcopy(INCOMING_MESSAGE)
+        cpy.conversation = None
+        with self.assertRaises(Exception) as _:
+            await adapter.update_activity(context, cpy)
+
+    async def test_should_fail_to_update_activity_if_activity_id_missing(self):
+        adapter = AdapterUnderTest()
+        context = TurnContext(adapter, INCOMING_MESSAGE)
+        cpy = deepcopy(INCOMING_MESSAGE)
+        cpy.id = None
+        with self.assertRaises(Exception) as _:
+            await adapter.update_activity(context, cpy)
+
+    async def test_should_migrate_tenant_id_for_msteams(self):
+        incoming = TurnContext.apply_conversation_reference(
+            activity=Activity(
+                type=ActivityTypes.message,
+                text="foo",
+                channel_data={"tenant": {"id": "1234"}},
+            ),
+            reference=REFERENCE,
+            is_incoming=True,
+        )
+
+        incoming.channel_id = "msteams"
+        adapter = AdapterUnderTest()
+
+        async def aux_func_assert_tenant_id_copied(context):
+            self.assertEqual(
+                context.activity.conversation.tenant_id,
+                "1234",
+                "should have copied tenant id from "
+                "channel_data to conversation address",
+            )
+
+        await adapter.process_activity(incoming, "", aux_func_assert_tenant_id_copied)
+
+    async def test_should_create_valid_conversation_for_msteams(self):
+
+        tenant_id = "testTenant"
+
+        reference = deepcopy(REFERENCE)
+        reference.conversation.tenant_id = tenant_id
+        reference.channel_data = {"tenant": {"id": tenant_id}}
+        adapter = AdapterUnderTest()
+
+        called = False
+
+        async def aux_func_assert_valid_conversation(context):
+            self.assertIsNotNone(context, "context not passed")
+            self.assertIsNotNone(context.activity, "context has no request")
+            self.assertIsNotNone(
+                context.activity.conversation, "request has invalid conversation"
+            )
+            self.assertEqual(
+                context.activity.conversation.tenant_id,
+                tenant_id,
+                "request has invalid tenant_id on conversation",
+            )
+            self.assertEqual(
+                context.activity.channel_data["tenant"]["tenantId"],
+                tenant_id,
+                "request has invalid tenant_id in channel_data",
+            )
+            nonlocal called
+            called = True
+
+        await adapter.create_conversation(reference, aux_func_assert_valid_conversation)
+        self.assertTrue(called, "bot logic not called.")
+
+    @staticmethod
+    def get_creds_and_assert_values(
+        turn_context: TurnContext,
+        expected_app_id: str,
+        expected_scope: str,
+        creds_count: int,
+    ):
+        if creds_count > 0:
+            # pylint: disable=protected-access
+            credential_cache = turn_context.adapter._app_credential_map
+            cache_key = BotFrameworkAdapter.key_for_app_credentials(
+                expected_app_id, expected_scope
+            )
+            credentials = credential_cache.get(cache_key)
+            assert credentials
+
+            TestBotFrameworkAdapter.assert_credentials_values(
+                credentials, expected_app_id, expected_scope
+            )
+
+            if creds_count:
+                assert creds_count == len(credential_cache)
+
+    @staticmethod
+    def get_client_and_assert_values(
+        turn_context: TurnContext,
+        expected_app_id: str,
+        expected_scope: str,
+        expected_url: str,
+        client_count: int,
+    ):
+        # pylint: disable=protected-access
+        client_cache = turn_context.adapter._connector_client_cache
+        cache_key = BotFrameworkAdapter.key_for_connector_client(
+            expected_url, expected_app_id, expected_scope
+        )
+        client = client_cache.get(cache_key)
+        assert client
+
+        TestBotFrameworkAdapter.assert_connectorclient_vaules(
+            client, expected_app_id, expected_url, expected_scope
+        )
+
+        assert client_count == len(client_cache)
+
+    @staticmethod
+    def assert_connectorclient_vaules(
+        client: ConnectorClient,
+        expected_app_id,
+        expected_service_url: str,
+        expected_scope=AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE,
+    ):
+        creds = client.config.credentials
+        assert TestBotFrameworkAdapter.__str_equal(
+            expected_app_id, creds.microsoft_app_id
+        )
+        assert TestBotFrameworkAdapter.__str_equal(expected_scope, creds.oauth_scope)
+        assert TestBotFrameworkAdapter.__str_equal(
+            expected_service_url, client.config.base_url
+        )
+
+    @staticmethod
+    def __str_equal(str1: str, str2: str) -> bool:
+        return (str1 if str1 is not None else "") == (str2 if str2 is not None else "")
+
+    @staticmethod
+    def assert_credentials_values(
+        credentials: AppCredentials,
+        expected_app_id: str,
+        expected_scope: str = AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE,
+    ):
+        assert expected_app_id == credentials.microsoft_app_id
+        assert expected_scope == credentials.oauth_scope
+
+    async def test_process_activity_creates_correct_creds_and_client_channel_to_bot(
+        self,
+    ):
+        await self.__process_activity_creates_correct_creds_and_client(
+            None,
+            None,
+            None,
+            AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE,
+            0,
+            1,
+        )
+
+    async def test_process_activity_creates_correct_creds_and_client_public_azure(self):
+        await self.__process_activity_creates_correct_creds_and_client(
+            "00000000-0000-0000-0000-000000000001",
+            CallerIdConstants.public_azure_channel,
+            None,
+            AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE,
+            1,
+            1,
+        )
+
+    async def test_process_activity_creates_correct_creds_and_client_us_gov(self):
+        await self.__process_activity_creates_correct_creds_and_client(
+            "00000000-0000-0000-0000-000000000001",
+            CallerIdConstants.us_gov_channel,
+            GovernmentConstants.CHANNEL_SERVICE,
+            GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE,
+            1,
+            1,
+        )
+
+    async def __process_activity_creates_correct_creds_and_client(
+        self,
+        bot_app_id: str,
+        expected_caller_id: str,
+        channel_service: str,
+        expected_scope: str,
+        expected_app_credentials_count: int,
+        expected_client_credentials_count: int,
+    ):
+        identity = ClaimsIdentity({}, True)
+        if bot_app_id:
+            identity.claims = {
+                AuthenticationConstants.AUDIENCE_CLAIM: bot_app_id,
+                AuthenticationConstants.APP_ID_CLAIM: bot_app_id,
+                AuthenticationConstants.VERSION_CLAIM: "1.0",
+            }
+
+        credential_provider = SimpleCredentialProvider(bot_app_id, None)
+        service_url = "https://smba.trafficmanager.net/amer/"
+
+        async def callback(context: TurnContext):
+            TestBotFrameworkAdapter.get_creds_and_assert_values(
+                context, bot_app_id, expected_scope, expected_app_credentials_count,
+            )
+            TestBotFrameworkAdapter.get_client_and_assert_values(
+                context,
+                bot_app_id,
+                expected_scope,
+                service_url,
+                expected_client_credentials_count,
+            )
+
+            assert context.activity.caller_id == expected_caller_id
+
+        settings = BotFrameworkAdapterSettings(
+            bot_app_id,
+            credential_provider=credential_provider,
+            channel_provider=SimpleChannelProvider(channel_service),
+        )
+        sut = BotFrameworkAdapter(settings)
+        await sut.process_activity_with_identity(
+            Activity(channel_id="emulator", service_url=service_url, text="test",),
+            identity,
+            callback,
+        )
+
+    async def test_process_activity_for_forwarded_activity(self):
+        bot_app_id = "00000000-0000-0000-0000-000000000001"
+        skill_1_app_id = "00000000-0000-0000-0000-000000skill1"
+        identity = ClaimsIdentity(
+            claims={
+                AuthenticationConstants.AUDIENCE_CLAIM: skill_1_app_id,
+                AuthenticationConstants.APP_ID_CLAIM: bot_app_id,
+                AuthenticationConstants.VERSION_CLAIM: "1.0",
+            },
+            is_authenticated=True,
+        )
+
+        service_url = "https://root-bot.test.azurewebsites.net/"
+
+        async def callback(context: TurnContext):
+            TestBotFrameworkAdapter.get_creds_and_assert_values(
+                context, skill_1_app_id, bot_app_id, 1,
+            )
+            TestBotFrameworkAdapter.get_client_and_assert_values(
+                context, skill_1_app_id, bot_app_id, service_url, 1,
+            )
+
+            scope = context.turn_state[BotFrameworkAdapter.BOT_OAUTH_SCOPE_KEY]
+            assert bot_app_id == scope
+            assert (
+                context.activity.caller_id
+                == f"{CallerIdConstants.bot_to_bot_prefix}{bot_app_id}"
+            )
+
+        settings = BotFrameworkAdapterSettings(bot_app_id)
+        sut = BotFrameworkAdapter(settings)
+        await sut.process_activity_with_identity(
+            Activity(channel_id="emulator", service_url=service_url, text="test",),
+            identity,
+            callback,
+        )
+
+    async def test_continue_conversation_without_audience(self):
+        mock_credential_provider = unittest.mock.create_autospec(CredentialProvider)
+
+        settings = BotFrameworkAdapterSettings(
+            app_id="bot_id", credential_provider=mock_credential_provider
+        )
+        adapter = BotFrameworkAdapter(settings)
+
+        skill_1_app_id = "00000000-0000-0000-0000-000000skill1"
+        skill_2_app_id = "00000000-0000-0000-0000-000000skill2"
+
+        skills_identity = ClaimsIdentity(
+            claims={
+                AuthenticationConstants.AUDIENCE_CLAIM: skill_1_app_id,
+                AuthenticationConstants.APP_ID_CLAIM: skill_2_app_id,
+                AuthenticationConstants.VERSION_CLAIM: "1.0",
+            },
+            is_authenticated=True,
+        )
+
+        channel_service_url = "https://smba.trafficmanager.net/amer/"
+
+        async def callback(context: TurnContext):
+            TestBotFrameworkAdapter.get_creds_and_assert_values(
+                context,
+                skill_1_app_id,
+                AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE,
+                1,
+            )
+            TestBotFrameworkAdapter.get_client_and_assert_values(
+                context,
+                skill_1_app_id,
+                AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE,
+                channel_service_url,
+                1,
+            )
+
+            # pylint: disable=protected-access
+            client_cache = context.adapter._connector_client_cache
+            client = client_cache.get(
+                BotFrameworkAdapter.key_for_connector_client(
+                    channel_service_url,
+                    skill_1_app_id,
+                    AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE,
+                )
+            )
+            assert client
+
+            turn_state_client = context.turn_state.get(
+                BotFrameworkAdapter.BOT_CONNECTOR_CLIENT_KEY
+            )
+            assert turn_state_client
+            client_creds = turn_state_client.config.credentials
+
+            assert skill_1_app_id == client_creds.microsoft_app_id
+            assert (
+                AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
+                == client_creds.oauth_scope
+            )
+            assert client.config.base_url == turn_state_client.config.base_url
+
+            scope = context.turn_state[BotFrameworkAdapter.BOT_OAUTH_SCOPE_KEY]
+            assert AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE == scope
+
+            # Ensure the serviceUrl was added to the trusted hosts
+            assert AppCredentials.is_trusted_service(channel_service_url)
+
+        refs = ConversationReference(service_url=channel_service_url)
+
+        # Ensure the serviceUrl is NOT in the trusted hosts
+        assert not AppCredentials.is_trusted_service(channel_service_url)
+
+        await adapter.continue_conversation(
+            refs, callback, claims_identity=skills_identity
+        )
+
+    async def test_continue_conversation_with_audience(self):
+        mock_credential_provider = unittest.mock.create_autospec(CredentialProvider)
+
+        settings = BotFrameworkAdapterSettings(
+            app_id="bot_id", credential_provider=mock_credential_provider
+        )
+        adapter = BotFrameworkAdapter(settings)
+
+        skill_1_app_id = "00000000-0000-0000-0000-000000skill1"
+        skill_2_app_id = "00000000-0000-0000-0000-000000skill2"
+
+        skills_identity = ClaimsIdentity(
+            claims={
+                AuthenticationConstants.AUDIENCE_CLAIM: skill_1_app_id,
+                AuthenticationConstants.APP_ID_CLAIM: skill_2_app_id,
+                AuthenticationConstants.VERSION_CLAIM: "1.0",
+            },
+            is_authenticated=True,
+        )
+
+        skill_2_service_url = "https://skill2.com/api/skills/"
+
+        async def callback(context: TurnContext):
+            TestBotFrameworkAdapter.get_creds_and_assert_values(
+                context, skill_1_app_id, skill_2_app_id, 1,
+            )
+            TestBotFrameworkAdapter.get_client_and_assert_values(
+                context, skill_1_app_id, skill_2_app_id, skill_2_service_url, 1,
+            )
+
+            # pylint: disable=protected-access
+            client_cache = context.adapter._connector_client_cache
+            client = client_cache.get(
+                BotFrameworkAdapter.key_for_connector_client(
+                    skill_2_service_url, skill_1_app_id, skill_2_app_id,
+                )
+            )
+            assert client
+
+            turn_state_client = context.turn_state.get(
+                BotFrameworkAdapter.BOT_CONNECTOR_CLIENT_KEY
+            )
+            assert turn_state_client
+            client_creds = turn_state_client.config.credentials
+
+            assert skill_1_app_id == client_creds.microsoft_app_id
+            assert skill_2_app_id == client_creds.oauth_scope
+            assert client.config.base_url == turn_state_client.config.base_url
+
+            scope = context.turn_state[BotFrameworkAdapter.BOT_OAUTH_SCOPE_KEY]
+            assert skill_2_app_id == scope
+
+            # Ensure the serviceUrl was added to the trusted hosts
+            assert AppCredentials.is_trusted_service(skill_2_service_url)
+
+        refs = ConversationReference(service_url=skill_2_service_url)
+
+        # Ensure the serviceUrl is NOT in the trusted hosts
+        assert not AppCredentials.is_trusted_service(skill_2_service_url)
+
+        await adapter.continue_conversation(
+            refs, callback, claims_identity=skills_identity, audience=skill_2_app_id
+        )
+
+    async def test_delivery_mode_expect_replies(self):
+        mock_credential_provider = unittest.mock.create_autospec(CredentialProvider)
+
+        settings = BotFrameworkAdapterSettings(
+            app_id="bot_id", credential_provider=mock_credential_provider
+        )
+        adapter = AdapterUnderTest(settings)
+
+        async def callback(context: TurnContext):
+            await context.send_activity("activity 1")
+            await context.send_activity("activity 2")
+            await context.send_activity("activity 3")
+
+        inbound_activity = Activity(
+            type=ActivityTypes.message,
+            channel_id="emulator",
+            service_url="http://tempuri.org/whatever",
+            delivery_mode=DeliveryModes.expect_replies,
+            text="hello world",
+        )
+
+        identity = ClaimsIdentity(
+            claims={
+                AuthenticationConstants.AUDIENCE_CLAIM: "bot_id",
+                AuthenticationConstants.APP_ID_CLAIM: "bot_id",
+                AuthenticationConstants.VERSION_CLAIM: "1.0",
+            },
+            is_authenticated=True,
+        )
+
+        invoke_response = await adapter.process_activity_with_identity(
+            inbound_activity, identity, callback
+        )
+        assert invoke_response
+        assert invoke_response.status == 200
+        activities = ExpectedReplies().deserialize(invoke_response.body).activities
+        assert len(activities) == 3
+        assert activities[0].text == "activity 1"
+        assert activities[1].text == "activity 2"
+        assert activities[2].text == "activity 3"
+        assert (
+            adapter.connector_client_mock.conversations.send_to_conversation.call_count
+            == 0
+        )
+
+    async def test_delivery_mode_normal(self):
+        mock_credential_provider = unittest.mock.create_autospec(CredentialProvider)
+
+        settings = BotFrameworkAdapterSettings(
+            app_id="bot_id", credential_provider=mock_credential_provider
+        )
+        adapter = AdapterUnderTest(settings)
+
+        async def callback(context: TurnContext):
+            await context.send_activity("activity 1")
+            await context.send_activity("activity 2")
+            await context.send_activity("activity 3")
+
+        inbound_activity = Activity(
+            type=ActivityTypes.message,
+            channel_id="emulator",
+            service_url="http://tempuri.org/whatever",
+            delivery_mode=DeliveryModes.normal,
+            text="hello world",
+            conversation=ConversationAccount(id="conversationId"),
+        )
+
+        identity = ClaimsIdentity(
+            claims={
+                AuthenticationConstants.AUDIENCE_CLAIM: "bot_id",
+                AuthenticationConstants.APP_ID_CLAIM: "bot_id",
+                AuthenticationConstants.VERSION_CLAIM: "1.0",
+            },
+            is_authenticated=True,
+        )
+
+        invoke_response = await adapter.process_activity_with_identity(
+            inbound_activity, identity, callback
+        )
+        assert not invoke_response
+        assert (
+            adapter.connector_client_mock.conversations.send_to_conversation.call_count
+            == 3
+        )
diff --git a/libraries/botbuilder-core/tests/test_bot_state.py b/libraries/botbuilder-core/tests/test_bot_state.py
index 13dec6d53..fdf8ed6fa 100644
--- a/libraries/botbuilder-core/tests/test_bot_state.py
+++ b/libraries/botbuilder-core/tests/test_bot_state.py
@@ -1,483 +1,504 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from unittest.mock import MagicMock
-import aiounittest
-
-from botbuilder.core import (
-    BotState,
-    ConversationState,
-    MemoryStorage,
-    Storage,
-    StoreItem,
-    TurnContext,
-    UserState,
-)
-from botbuilder.core.adapters import TestAdapter
-from botbuilder.schema import Activity, ConversationAccount
-
-from test_utilities import TestUtilities
-
-RECEIVED_MESSAGE = Activity(type="message", text="received")
-STORAGE_KEY = "stateKey"
-
-
-def cached_state(context, state_key):
-    cached = context.services.get(state_key)
-    return cached["state"] if cached is not None else None
-
-
-def key_factory(context):
-    assert context is not None
-    return STORAGE_KEY
-
-
-class BotStateForTest(BotState):
-    def __init__(self, storage: Storage):
-        super().__init__(storage, f"BotState:BotState")
-
-    def get_storage_key(self, turn_context: TurnContext) -> str:
-        return f"botstate/{turn_context.activity.channel_id}/{turn_context.activity.conversation.id}/BotState"
-
-
-class CustomState(StoreItem):
-    def __init__(self, custom_string: str = None, e_tag: str = "*"):
-        super().__init__(custom_string=custom_string, e_tag=e_tag)
-
-
-class TestPocoState:
-    def __init__(self, value=None):
-        self.value = value
-
-
-class TestBotState(aiounittest.AsyncTestCase):
-    storage = MemoryStorage()
-    adapter = TestAdapter()
-    context = TurnContext(adapter, RECEIVED_MESSAGE)
-    middleware = BotState(storage, key_factory)
-
-    def test_state_empty_name(self):
-        # Arrange
-        dictionary = {}
-        user_state = UserState(MemoryStorage(dictionary))
-
-        # Act
-        with self.assertRaises(TypeError) as _:
-            user_state.create_property("")
-
-    def test_state_none_name(self):
-        # Arrange
-        dictionary = {}
-        user_state = UserState(MemoryStorage(dictionary))
-
-        # Act
-        with self.assertRaises(TypeError) as _:
-            user_state.create_property(None)
-
-    async def test_storage_not_called_no_changes(self):
-        """Verify storage not called when no changes are made"""
-        # Mock a storage provider, which counts read/writes
-        dictionary = {}
-
-        async def mock_write_result(self):  # pylint: disable=unused-argument
-            return
-
-        async def mock_read_result(self):  # pylint: disable=unused-argument
-            return {}
-
-        mock_storage = MemoryStorage(dictionary)
-        mock_storage.write = MagicMock(side_effect=mock_write_result)
-        mock_storage.read = MagicMock(side_effect=mock_read_result)
-
-        # Arrange
-        user_state = UserState(mock_storage)
-        context = TestUtilities.create_empty_context()
-
-        # Act
-        property_a = user_state.create_property("property_a")
-        self.assertEqual(mock_storage.write.call_count, 0)
-        await user_state.save_changes(context)
-        await property_a.set(context, "hello")
-        self.assertEqual(mock_storage.read.call_count, 1)  # Initial save bumps count
-        self.assertEqual(mock_storage.write.call_count, 0)  # Initial save bumps count
-        await property_a.set(context, "there")
-        self.assertEqual(
-            mock_storage.write.call_count, 0
-        )  # Set on property should not bump
-        await user_state.save_changes(context)
-        self.assertEqual(mock_storage.write.call_count, 1)  # Explicit save should bump
-        value_a = await property_a.get(context)
-        self.assertEqual("there", value_a)
-        self.assertEqual(mock_storage.write.call_count, 1)  # Gets should not bump
-        await user_state.save_changes(context)
-        self.assertEqual(mock_storage.write.call_count, 1)
-        await property_a.delete(context)  # Delete alone no bump
-        self.assertEqual(mock_storage.write.call_count, 1)
-        await user_state.save_changes(context)  # Save when dirty should bump
-        self.assertEqual(mock_storage.write.call_count, 2)
-        self.assertEqual(mock_storage.read.call_count, 1)
-        await user_state.save_changes(context)  # Save not dirty should not bump
-        self.assertEqual(mock_storage.write.call_count, 2)
-        self.assertEqual(mock_storage.read.call_count, 1)
-
-    async def test_state_set_no_load(self):
-        """Should be able to set a property with no Load"""
-        # Arrange
-        dictionary = {}
-        user_state = UserState(MemoryStorage(dictionary))
-        context = TestUtilities.create_empty_context()
-
-        # Act
-        property_a = user_state.create_property("property_a")
-        await property_a.set(context, "hello")
-
-    async def test_state_multiple_loads(self):
-        """Should be able to load multiple times"""
-        # Arrange
-        dictionary = {}
-        user_state = UserState(MemoryStorage(dictionary))
-        context = TestUtilities.create_empty_context()
-
-        # Act
-        user_state.create_property("property_a")
-        await user_state.load(context)
-        await user_state.load(context)
-
-    async def test_state_get_no_load_with_default(self):
-        """Should be able to get a property with no Load and default"""
-        # Arrange
-        dictionary = {}
-        user_state = UserState(MemoryStorage(dictionary))
-        context = TestUtilities.create_empty_context()
-
-        # Act
-        property_a = user_state.create_property("property_a")
-        value_a = await property_a.get(context, lambda: "Default!")
-        self.assertEqual("Default!", value_a)
-
-    async def test_state_get_no_load_no_default(self):
-        """Cannot get a string with no default set"""
-        # Arrange
-        dictionary = {}
-        user_state = UserState(MemoryStorage(dictionary))
-        context = TestUtilities.create_empty_context()
-
-        # Act
-        property_a = user_state.create_property("property_a")
-        value_a = await property_a.get(context)
-
-        # Assert
-        self.assertIsNone(value_a)
-
-    async def test_state_poco_no_default(self):
-        """Cannot get a POCO with no default set"""
-        # Arrange
-        dictionary = {}
-        user_state = UserState(MemoryStorage(dictionary))
-        context = TestUtilities.create_empty_context()
-
-        # Act
-        test_property = user_state.create_property("test")
-        value = await test_property.get(context)
-
-        # Assert
-        self.assertIsNone(value)
-
-    async def test_state_bool_no_default(self):
-        """Cannot get a bool with no default set"""
-        # Arange
-        dictionary = {}
-        user_state = UserState(MemoryStorage(dictionary))
-        context = TestUtilities.create_empty_context()
-
-        # Act
-        test_property = user_state.create_property("test")
-        value = await test_property.get(context)
-
-        # Assert
-        self.assertFalse(value)
-
-    async def test_state_set_after_save(self):
-        """Verify setting property after save"""
-        # Arrange
-        dictionary = {}
-        user_state = UserState(MemoryStorage(dictionary))
-        context = TestUtilities.create_empty_context()
-
-        # Act
-        property_a = user_state.create_property("property-a")
-        property_b = user_state.create_property("property-b")
-
-        await user_state.load(context)
-        await property_a.set(context, "hello")
-        await property_b.set(context, "world")
-        await user_state.save_changes(context)
-
-        await property_a.set(context, "hello2")
-
-    async def test_state_multiple_save(self):
-        """Verify multiple saves"""
-        # Arrange
-        dictionary = {}
-        user_state = UserState(MemoryStorage(dictionary))
-        context = TestUtilities.create_empty_context()
-
-        # Act
-        property_a = user_state.create_property("property-a")
-        property_b = user_state.create_property("property-b")
-
-        await user_state.load(context)
-        await property_a.set(context, "hello")
-        await property_b.set(context, "world")
-        await user_state.save_changes(context)
-
-        await property_a.set(context, "hello2")
-        await user_state.save_changes(context)
-        value_a = await property_a.get(context)
-        self.assertEqual("hello2", value_a)
-
-    async def test_load_set_save(self):
-        # Arrange
-        dictionary = {}
-        user_state = UserState(MemoryStorage(dictionary))
-        context = TestUtilities.create_empty_context()
-
-        # Act
-        property_a = user_state.create_property("property-a")
-        property_b = user_state.create_property("property-b")
-
-        await user_state.load(context)
-        await property_a.set(context, "hello")
-        await property_b.set(context, "world")
-        await user_state.save_changes(context)
-
-        # Assert
-        obj = dictionary["EmptyContext/users/empty@empty.context.org"]
-        self.assertEqual("hello", obj["property-a"])
-        self.assertEqual("world", obj["property-b"])
-
-    async def test_load_set_save_twice(self):
-        # Arrange
-        dictionary = {}
-        context = TestUtilities.create_empty_context()
-
-        # Act
-        user_state = UserState(MemoryStorage(dictionary))
-
-        property_a = user_state.create_property("property-a")
-        property_b = user_state.create_property("property-b")
-        property_c = user_state.create_property("property-c")
-
-        await user_state.load(context)
-        await property_a.set(context, "hello")
-        await property_b.set(context, "world")
-        await property_c.set(context, "test")
-        await user_state.save_changes(context)
-
-        # Assert
-        obj = dictionary["EmptyContext/users/empty@empty.context.org"]
-        self.assertEqual("hello", obj["property-a"])
-        self.assertEqual("world", obj["property-b"])
-
-        # Act 2
-        user_state2 = UserState(MemoryStorage(dictionary))
-
-        property_a2 = user_state2.create_property("property-a")
-        property_b2 = user_state2.create_property("property-b")
-
-        await user_state2.load(context)
-        await property_a2.set(context, "hello-2")
-        await property_b2.set(context, "world-2")
-        await user_state2.save_changes(context)
-
-        # Assert 2
-        obj2 = dictionary["EmptyContext/users/empty@empty.context.org"]
-        self.assertEqual("hello-2", obj2["property-a"])
-        self.assertEqual("world-2", obj2["property-b"])
-        self.assertEqual("test", obj2["property-c"])
-
-    async def test_load_save_delete(self):
-        # Arrange
-        dictionary = {}
-        context = TestUtilities.create_empty_context()
-
-        # Act
-        user_state = UserState(MemoryStorage(dictionary))
-
-        property_a = user_state.create_property("property-a")
-        property_b = user_state.create_property("property-b")
-
-        await user_state.load(context)
-        await property_a.set(context, "hello")
-        await property_b.set(context, "world")
-        await user_state.save_changes(context)
-
-        # Assert
-        obj = dictionary["EmptyContext/users/empty@empty.context.org"]
-        self.assertEqual("hello", obj["property-a"])
-        self.assertEqual("world", obj["property-b"])
-
-        # Act 2
-        user_state2 = UserState(MemoryStorage(dictionary))
-
-        property_a2 = user_state2.create_property("property-a")
-        property_b2 = user_state2.create_property("property-b")
-
-        await user_state2.load(context)
-        await property_a2.set(context, "hello-2")
-        await property_b2.delete(context)
-        await user_state2.save_changes(context)
-
-        # Assert 2
-        obj2 = dictionary["EmptyContext/users/empty@empty.context.org"]
-        self.assertEqual("hello-2", obj2["property-a"])
-        with self.assertRaises(KeyError) as _:
-            obj2["property-b"]  # pylint: disable=pointless-statement
-
-    async def test_state_use_bot_state_directly(self):
-        async def exec_test(context: TurnContext):
-            # pylint: disable=unnecessary-lambda
-            bot_state_manager = BotStateForTest(MemoryStorage())
-            test_property = bot_state_manager.create_property("test")
-
-            # read initial state object
-            await bot_state_manager.load(context)
-
-            custom_state = await test_property.get(context, lambda: CustomState())
-
-            # this should be a 'CustomState' as nothing is currently stored in storage
-            assert isinstance(custom_state, CustomState)
-
-            # amend property and write to storage
-            custom_state.custom_string = "test"
-            await bot_state_manager.save_changes(context)
-
-            custom_state.custom_string = "asdfsadf"
-
-            # read into context again
-            await bot_state_manager.load(context, True)
-
-            custom_state = await test_property.get(context)
-
-            # check object read from value has the correct value for custom_string
-            assert custom_state.custom_string == "test"
-
-        adapter = TestAdapter(exec_test)
-        await adapter.send("start")
-
-    async def test_user_state_bad_from_throws(self):
-        dictionary = {}
-        user_state = UserState(MemoryStorage(dictionary))
-        context = TestUtilities.create_empty_context()
-        context.activity.from_property = None
-        test_property = user_state.create_property("test")
-        with self.assertRaises(AttributeError):
-            await test_property.get(context)
-
-    async def test_conversation_state_bad_conversation_throws(self):
-        dictionary = {}
-        user_state = ConversationState(MemoryStorage(dictionary))
-        context = TestUtilities.create_empty_context()
-        context.activity.conversation = None
-        test_property = user_state.create_property("test")
-        with self.assertRaises(AttributeError):
-            await test_property.get(context)
-
-    async def test_clear_and_save(self):
-        # pylint: disable=unnecessary-lambda
-        turn_context = TestUtilities.create_empty_context()
-        turn_context.activity.conversation = ConversationAccount(id="1234")
-
-        storage = MemoryStorage({})
-
-        # Turn 0
-        bot_state1 = ConversationState(storage)
-        (
-            await bot_state1.create_property("test-name").get(
-                turn_context, lambda: TestPocoState()
-            )
-        ).value = "test-value"
-        await bot_state1.save_changes(turn_context)
-
-        # Turn 1
-        bot_state2 = ConversationState(storage)
-        value1 = (
-            await bot_state2.create_property("test-name").get(
-                turn_context, lambda: TestPocoState(value="default-value")
-            )
-        ).value
-
-        assert value1 == "test-value"
-
-        # Turn 2
-        bot_state3 = ConversationState(storage)
-        await bot_state3.clear_state(turn_context)
-        await bot_state3.save_changes(turn_context)
-
-        # Turn 3
-        bot_state4 = ConversationState(storage)
-        value2 = (
-            await bot_state4.create_property("test-name").get(
-                turn_context, lambda: TestPocoState(value="default-value")
-            )
-        ).value
-
-        assert value2, "default-value"
-
-    async def test_bot_state_delete(self):
-        # pylint: disable=unnecessary-lambda
-        turn_context = TestUtilities.create_empty_context()
-        turn_context.activity.conversation = ConversationAccount(id="1234")
-
-        storage = MemoryStorage({})
-
-        # Turn 0
-        bot_state1 = ConversationState(storage)
-        (
-            await bot_state1.create_property("test-name").get(
-                turn_context, lambda: TestPocoState()
-            )
-        ).value = "test-value"
-        await bot_state1.save_changes(turn_context)
-
-        # Turn 1
-        bot_state2 = ConversationState(storage)
-        value1 = (
-            await bot_state2.create_property("test-name").get(
-                turn_context, lambda: TestPocoState(value="default-value")
-            )
-        ).value
-
-        assert value1 == "test-value"
-
-        # Turn 2
-        bot_state3 = ConversationState(storage)
-        await bot_state3.delete(turn_context)
-
-        # Turn 3
-        bot_state4 = ConversationState(storage)
-        value2 = (
-            await bot_state4.create_property("test-name").get(
-                turn_context, lambda: TestPocoState(value="default-value")
-            )
-        ).value
-
-        assert value2 == "default-value"
-
-    async def test_bot_state_get(self):
-        # pylint: disable=unnecessary-lambda
-        turn_context = TestUtilities.create_empty_context()
-        turn_context.activity.conversation = ConversationAccount(id="1234")
-
-        storage = MemoryStorage({})
-
-        conversation_state = ConversationState(storage)
-        (
-            await conversation_state.create_property("test-name").get(
-                turn_context, lambda: TestPocoState()
-            )
-        ).value = "test-value"
-
-        result = conversation_state.get(turn_context)
-
-        assert result["test-name"].value == "test-value"
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from unittest.mock import MagicMock
+import aiounittest
+
+from botbuilder.core import (
+    BotState,
+    ConversationState,
+    MemoryStorage,
+    Storage,
+    StoreItem,
+    TurnContext,
+    UserState,
+)
+from botbuilder.core.adapters import TestAdapter
+from botbuilder.schema import Activity, ConversationAccount
+
+from test_utilities import TestUtilities
+
+RECEIVED_MESSAGE = Activity(type="message", text="received")
+STORAGE_KEY = "stateKey"
+
+
+def cached_state(context, state_key):
+    cached = context.services.get(state_key)
+    return cached["state"] if cached is not None else None
+
+
+def key_factory(context):
+    assert context is not None
+    return STORAGE_KEY
+
+
+class BotStateForTest(BotState):
+    def __init__(self, storage: Storage):
+        super().__init__(storage, f"BotState:BotState")
+
+    def get_storage_key(self, turn_context: TurnContext) -> str:
+        return f"botstate/{turn_context.activity.channel_id}/{turn_context.activity.conversation.id}/BotState"
+
+
+class CustomState(StoreItem):
+    def __init__(self, custom_string: str = None, e_tag: str = "*"):
+        super().__init__(custom_string=custom_string, e_tag=e_tag)
+
+
+class TestPocoState:
+    __test__ = False
+
+    def __init__(self, value=None):
+        self.value = value
+
+
+class TestBotState(aiounittest.AsyncTestCase):
+    storage = MemoryStorage()
+    adapter = TestAdapter()
+    context = TurnContext(adapter, RECEIVED_MESSAGE)
+    middleware = BotState(storage, key_factory)
+
+    def test_state_empty_name(self):
+        # Arrange
+        dictionary = {}
+        user_state = UserState(MemoryStorage(dictionary))
+
+        # Act
+        with self.assertRaises(TypeError) as _:
+            user_state.create_property("")
+
+    def test_state_none_name(self):
+        # Arrange
+        dictionary = {}
+        user_state = UserState(MemoryStorage(dictionary))
+
+        # Act
+        with self.assertRaises(TypeError) as _:
+            user_state.create_property(None)
+
+    async def test_storage_not_called_no_changes(self):
+        """Verify storage not called when no changes are made"""
+        # Mock a storage provider, which counts read/writes
+        dictionary = {}
+
+        async def mock_write_result(self):  # pylint: disable=unused-argument
+            return
+
+        async def mock_read_result(self):  # pylint: disable=unused-argument
+            return {}
+
+        mock_storage = MemoryStorage(dictionary)
+        mock_storage.write = MagicMock(side_effect=mock_write_result)
+        mock_storage.read = MagicMock(side_effect=mock_read_result)
+
+        # Arrange
+        user_state = UserState(mock_storage)
+        context = TestUtilities.create_empty_context()
+
+        # Act
+        property_a = user_state.create_property("property_a")
+        self.assertEqual(mock_storage.write.call_count, 0)
+        await user_state.save_changes(context)
+        await property_a.set(context, "hello")
+        self.assertEqual(mock_storage.read.call_count, 1)  # Initial save bumps count
+        self.assertEqual(mock_storage.write.call_count, 0)  # Initial save bumps count
+        await property_a.set(context, "there")
+        self.assertEqual(
+            mock_storage.write.call_count, 0
+        )  # Set on property should not bump
+        await user_state.save_changes(context)
+        self.assertEqual(mock_storage.write.call_count, 1)  # Explicit save should bump
+        value_a = await property_a.get(context)
+        self.assertEqual("there", value_a)
+        self.assertEqual(mock_storage.write.call_count, 1)  # Gets should not bump
+        await user_state.save_changes(context)
+        self.assertEqual(mock_storage.write.call_count, 1)
+        await property_a.delete(context)  # Delete alone no bump
+        self.assertEqual(mock_storage.write.call_count, 1)
+        await user_state.save_changes(context)  # Save when dirty should bump
+        self.assertEqual(mock_storage.write.call_count, 2)
+        self.assertEqual(mock_storage.read.call_count, 1)
+        await user_state.save_changes(context)  # Save not dirty should not bump
+        self.assertEqual(mock_storage.write.call_count, 2)
+        self.assertEqual(mock_storage.read.call_count, 1)
+
+    async def test_state_set_no_load(self):
+        """Should be able to set a property with no Load"""
+        # Arrange
+        dictionary = {}
+        user_state = UserState(MemoryStorage(dictionary))
+        context = TestUtilities.create_empty_context()
+
+        # Act
+        property_a = user_state.create_property("property_a")
+        await property_a.set(context, "hello")
+
+    async def test_state_multiple_loads(self):
+        """Should be able to load multiple times"""
+        # Arrange
+        dictionary = {}
+        user_state = UserState(MemoryStorage(dictionary))
+        context = TestUtilities.create_empty_context()
+
+        # Act
+        user_state.create_property("property_a")
+        await user_state.load(context)
+        await user_state.load(context)
+
+    async def test_state_get_no_load_with_default(self):
+        """Should be able to get a property with no Load and default"""
+        # Arrange
+        dictionary = {}
+        user_state = UserState(MemoryStorage(dictionary))
+        context = TestUtilities.create_empty_context()
+
+        # Act
+        property_a = user_state.create_property("property_a")
+        value_a = await property_a.get(context, lambda: "Default!")
+        self.assertEqual("Default!", value_a)
+
+    async def test_state_get_no_load_no_default(self):
+        """Cannot get a string with no default set"""
+        # Arrange
+        dictionary = {}
+        user_state = UserState(MemoryStorage(dictionary))
+        context = TestUtilities.create_empty_context()
+
+        # Act
+        property_a = user_state.create_property("property_a")
+        value_a = await property_a.get(context)
+
+        # Assert
+        self.assertIsNone(value_a)
+
+    async def test_state_poco_no_default(self):
+        """Cannot get a POCO with no default set"""
+        # Arrange
+        dictionary = {}
+        user_state = UserState(MemoryStorage(dictionary))
+        context = TestUtilities.create_empty_context()
+
+        # Act
+        test_property = user_state.create_property("test")
+        value = await test_property.get(context)
+
+        # Assert
+        self.assertIsNone(value)
+
+    async def test_state_bool_no_default(self):
+        """Cannot get a bool with no default set"""
+        # Arange
+        dictionary = {}
+        user_state = UserState(MemoryStorage(dictionary))
+        context = TestUtilities.create_empty_context()
+
+        # Act
+        test_property = user_state.create_property("test")
+        value = await test_property.get(context)
+
+        # Assert
+        self.assertFalse(value)
+
+    async def test_state_set_after_save(self):
+        """Verify setting property after save"""
+        # Arrange
+        dictionary = {}
+        user_state = UserState(MemoryStorage(dictionary))
+        context = TestUtilities.create_empty_context()
+
+        # Act
+        property_a = user_state.create_property("property-a")
+        property_b = user_state.create_property("property-b")
+
+        await user_state.load(context)
+        await property_a.set(context, "hello")
+        await property_b.set(context, "world")
+        await user_state.save_changes(context)
+
+        await property_a.set(context, "hello2")
+
+    async def test_state_multiple_save(self):
+        """Verify multiple saves"""
+        # Arrange
+        dictionary = {}
+        user_state = UserState(MemoryStorage(dictionary))
+        context = TestUtilities.create_empty_context()
+
+        # Act
+        property_a = user_state.create_property("property-a")
+        property_b = user_state.create_property("property-b")
+
+        await user_state.load(context)
+        await property_a.set(context, "hello")
+        await property_b.set(context, "world")
+        await user_state.save_changes(context)
+
+        await property_a.set(context, "hello2")
+        await user_state.save_changes(context)
+        value_a = await property_a.get(context)
+        self.assertEqual("hello2", value_a)
+
+    async def test_load_set_save(self):
+        # Arrange
+        dictionary = {}
+        user_state = UserState(MemoryStorage(dictionary))
+        context = TestUtilities.create_empty_context()
+
+        # Act
+        property_a = user_state.create_property("property-a")
+        property_b = user_state.create_property("property-b")
+
+        await user_state.load(context)
+        await property_a.set(context, "hello")
+        await property_b.set(context, "world")
+        await user_state.save_changes(context)
+
+        # Assert
+        obj = dictionary["EmptyContext/users/empty@empty.context.org"]
+        self.assertEqual("hello", obj["property-a"])
+        self.assertEqual("world", obj["property-b"])
+
+    async def test_load_set_save_twice(self):
+        # Arrange
+        dictionary = {}
+        context = TestUtilities.create_empty_context()
+
+        # Act
+        user_state = UserState(MemoryStorage(dictionary))
+
+        property_a = user_state.create_property("property-a")
+        property_b = user_state.create_property("property-b")
+        property_c = user_state.create_property("property-c")
+
+        await user_state.load(context)
+        await property_a.set(context, "hello")
+        await property_b.set(context, "world")
+        await property_c.set(context, "test")
+        await user_state.save_changes(context)
+
+        # Assert
+        obj = dictionary["EmptyContext/users/empty@empty.context.org"]
+        self.assertEqual("hello", obj["property-a"])
+        self.assertEqual("world", obj["property-b"])
+
+        # Act 2
+        user_state2 = UserState(MemoryStorage(dictionary))
+
+        property_a2 = user_state2.create_property("property-a")
+        property_b2 = user_state2.create_property("property-b")
+
+        await user_state2.load(context)
+        await property_a2.set(context, "hello-2")
+        await property_b2.set(context, "world-2")
+        await user_state2.save_changes(context)
+
+        # Assert 2
+        obj2 = dictionary["EmptyContext/users/empty@empty.context.org"]
+        self.assertEqual("hello-2", obj2["property-a"])
+        self.assertEqual("world-2", obj2["property-b"])
+        self.assertEqual("test", obj2["property-c"])
+
+    async def test_load_save_delete(self):
+        # Arrange
+        dictionary = {}
+        context = TestUtilities.create_empty_context()
+
+        # Act
+        user_state = UserState(MemoryStorage(dictionary))
+
+        property_a = user_state.create_property("property-a")
+        property_b = user_state.create_property("property-b")
+
+        await user_state.load(context)
+        await property_a.set(context, "hello")
+        await property_b.set(context, "world")
+        await user_state.save_changes(context)
+
+        # Assert
+        obj = dictionary["EmptyContext/users/empty@empty.context.org"]
+        self.assertEqual("hello", obj["property-a"])
+        self.assertEqual("world", obj["property-b"])
+
+        # Act 2
+        user_state2 = UserState(MemoryStorage(dictionary))
+
+        property_a2 = user_state2.create_property("property-a")
+        property_b2 = user_state2.create_property("property-b")
+
+        await user_state2.load(context)
+        await property_a2.set(context, "hello-2")
+        await property_b2.delete(context)
+        await user_state2.save_changes(context)
+
+        # Assert 2
+        obj2 = dictionary["EmptyContext/users/empty@empty.context.org"]
+        self.assertEqual("hello-2", obj2["property-a"])
+        with self.assertRaises(KeyError) as _:
+            obj2["property-b"]  # pylint: disable=pointless-statement
+
+    async def test_state_use_bot_state_directly(self):
+        async def exec_test(context: TurnContext):
+            # pylint: disable=unnecessary-lambda
+            bot_state_manager = BotStateForTest(MemoryStorage())
+            test_property = bot_state_manager.create_property("test")
+
+            # read initial state object
+            await bot_state_manager.load(context)
+
+            custom_state = await test_property.get(context, lambda: CustomState())
+
+            # this should be a 'CustomState' as nothing is currently stored in storage
+            assert isinstance(custom_state, CustomState)
+
+            # amend property and write to storage
+            custom_state.custom_string = "test"
+            await bot_state_manager.save_changes(context)
+
+            custom_state.custom_string = "asdfsadf"
+
+            # read into context again
+            await bot_state_manager.load(context, True)
+
+            custom_state = await test_property.get(context)
+
+            # check object read from value has the correct value for custom_string
+            assert custom_state.custom_string == "test"
+
+        adapter = TestAdapter(exec_test)
+        await adapter.send("start")
+
+    async def test_user_state_bad_from_throws(self):
+        dictionary = {}
+        user_state = UserState(MemoryStorage(dictionary))
+        context = TestUtilities.create_empty_context()
+        context.activity.from_property = None
+        test_property = user_state.create_property("test")
+        with self.assertRaises(AttributeError):
+            await test_property.get(context)
+
+    async def test_conversation_state_bad_conversation_throws(self):
+        dictionary = {}
+        user_state = ConversationState(MemoryStorage(dictionary))
+        context = TestUtilities.create_empty_context()
+        context.activity.conversation = None
+        test_property = user_state.create_property("test")
+        with self.assertRaises(AttributeError):
+            await test_property.get(context)
+
+    async def test_clear_and_save(self):
+        # pylint: disable=unnecessary-lambda
+        turn_context = TestUtilities.create_empty_context()
+        turn_context.activity.conversation = ConversationAccount(id="1234")
+
+        storage = MemoryStorage({})
+
+        # Turn 0
+        bot_state1 = ConversationState(storage)
+        (
+            await bot_state1.create_property("test-name").get(
+                turn_context, lambda: TestPocoState()
+            )
+        ).value = "test-value"
+        await bot_state1.save_changes(turn_context)
+
+        # Turn 1
+        bot_state2 = ConversationState(storage)
+        value1 = (
+            await bot_state2.create_property("test-name").get(
+                turn_context, lambda: TestPocoState(value="default-value")
+            )
+        ).value
+
+        assert value1 == "test-value"
+
+        # Turn 2
+        bot_state3 = ConversationState(storage)
+        await bot_state3.clear_state(turn_context)
+        await bot_state3.save_changes(turn_context)
+
+        # Turn 3
+        bot_state4 = ConversationState(storage)
+        value2 = (
+            await bot_state4.create_property("test-name").get(
+                turn_context, lambda: TestPocoState(value="default-value")
+            )
+        ).value
+
+        assert value2, "default-value"
+
+    async def test_bot_state_delete(self):
+        # pylint: disable=unnecessary-lambda
+        turn_context = TestUtilities.create_empty_context()
+        turn_context.activity.conversation = ConversationAccount(id="1234")
+
+        storage = MemoryStorage({})
+
+        # Turn 0
+        bot_state1 = ConversationState(storage)
+        (
+            await bot_state1.create_property("test-name").get(
+                turn_context, lambda: TestPocoState()
+            )
+        ).value = "test-value"
+        await bot_state1.save_changes(turn_context)
+
+        # Turn 1
+        bot_state2 = ConversationState(storage)
+        value1 = (
+            await bot_state2.create_property("test-name").get(
+                turn_context, lambda: TestPocoState(value="default-value")
+            )
+        ).value
+
+        assert value1 == "test-value"
+
+        # Turn 2
+        bot_state3 = ConversationState(storage)
+        await bot_state3.delete(turn_context)
+
+        # Turn 3
+        bot_state4 = ConversationState(storage)
+        value2 = (
+            await bot_state4.create_property("test-name").get(
+                turn_context, lambda: TestPocoState(value="default-value")
+            )
+        ).value
+
+        assert value2 == "default-value"
+
+    async def test_bot_state_get(self):
+        # pylint: disable=unnecessary-lambda
+        turn_context = TestUtilities.create_empty_context()
+        turn_context.activity.conversation = ConversationAccount(id="1234")
+
+        storage = MemoryStorage({})
+
+        test_bot_state = BotStateForTest(storage)
+        (
+            await test_bot_state.create_property("test-name").get(
+                turn_context, lambda: TestPocoState()
+            )
+        ).value = "test-value"
+
+        result = test_bot_state.get(turn_context)
+
+        assert result["test-name"].value == "test-value"
+
+    async def test_bot_state_get_cached_state(self):
+        # pylint: disable=unnecessary-lambda
+        turn_context = TestUtilities.create_empty_context()
+        turn_context.activity.conversation = ConversationAccount(id="1234")
+
+        storage = MemoryStorage({})
+
+        test_bot_state = BotStateForTest(storage)
+        (
+            await test_bot_state.create_property("test-name").get(
+                turn_context, lambda: TestPocoState()
+            )
+        ).value = "test-value"
+
+        result = test_bot_state.get_cached_state(turn_context)
+
+        assert result is not None
+        assert result == test_bot_state.get_cached_state(turn_context)
diff --git a/libraries/botbuilder-core/tests/test_channel_service_handler.py b/libraries/botbuilder-core/tests/test_channel_service_handler.py
new file mode 100644
index 000000000..8f0d9df12
--- /dev/null
+++ b/libraries/botbuilder-core/tests/test_channel_service_handler.py
@@ -0,0 +1,46 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import aiounittest
+from botbuilder.core import ChannelServiceHandler
+from botframework.connector.auth import (
+    AuthenticationConfiguration,
+    ClaimsIdentity,
+    SimpleCredentialProvider,
+    JwtTokenValidation,
+    AuthenticationConstants,
+)
+import botbuilder.schema
+
+
+class TestChannelServiceHandler(ChannelServiceHandler):
+    def __init__(self):
+        self.claims_identity = None
+        ChannelServiceHandler.__init__(
+            self, SimpleCredentialProvider("", ""), AuthenticationConfiguration()
+        )
+
+    async def on_reply_to_activity(
+        self,
+        claims_identity: ClaimsIdentity,
+        conversation_id: str,
+        activity_id: str,
+        activity: botbuilder.schema.Activity,
+    ) -> botbuilder.schema.ResourceResponse:
+        self.claims_identity = claims_identity
+        return botbuilder.schema.ResourceResponse()
+
+
+class ChannelServiceHandlerTests(aiounittest.AsyncTestCase):
+    async def test_should_authenticate_anonymous_skill_claim(self):
+        sut = TestChannelServiceHandler()
+        await sut.handle_reply_to_activity(None, "123", "456", {})
+
+        assert (
+            sut.claims_identity.authentication_type
+            == AuthenticationConstants.ANONYMOUS_AUTH_TYPE
+        )
+        assert (
+            JwtTokenValidation.get_app_id_from_claims(sut.claims_identity.claims)
+            == AuthenticationConstants.ANONYMOUS_SKILL_APP_ID
+        )
diff --git a/libraries/botbuilder-core/tests/test_inspection_middleware.py b/libraries/botbuilder-core/tests/test_inspection_middleware.py
index 34d90463b..68259a1b4 100644
--- a/libraries/botbuilder-core/tests/test_inspection_middleware.py
+++ b/libraries/botbuilder-core/tests/test_inspection_middleware.py
@@ -14,7 +14,7 @@
 )
 from botbuilder.core.adapters import TestAdapter
 from botbuilder.core.inspection import InspectionMiddleware, InspectionState
-from botbuilder.schema import Activity, ActivityTypes, ChannelAccount, Mention
+from botbuilder.schema import Activity, ActivityTypes, ChannelAccount, Entity, Mention
 
 
 class TestConversationState(aiounittest.AsyncTestCase):
@@ -37,7 +37,7 @@ async def aux_func(context: TurnContext):
         assert outbound_activity.text, "hi"
 
     async def test_should_replicate_activity_data_to_listening_emulator_following_open_and_attach(
-        self
+        self,
     ):
         inbound_expectation, outbound_expectation, state_expectation = (
             False,
@@ -147,7 +147,7 @@ async def exec_test2(turn_context):
         assert mocker.call_count, 3
 
     async def test_should_replicate_activity_data_to_listening_emulator_following_open_and_attach_with_at_mention(
-        self
+        self,
     ):
         inbound_expectation, outbound_expectation, state_expectation = (
             False,
@@ -249,10 +249,12 @@ async def exec_test2(turn_context):
                 text=attach_command,
                 recipient=ChannelAccount(id=recipient_id),
                 entities=[
-                    Mention(
-                        type="mention",
-                        text=f"{recipient_id}",
-                        mentioned=ChannelAccount(name="Bot", id=recipient_id),
+                    Entity().deserialize(
+                        Mention(
+                            type="mention",
+                            text=f"{recipient_id}",
+                            mentioned=ChannelAccount(name="Bot", id=recipient_id),
+                        ).serialize()
                     )
                 ],
             )
diff --git a/libraries/botbuilder-core/tests/test_memory_storage.py b/libraries/botbuilder-core/tests/test_memory_storage.py
index 16afdb0e2..a34e2a94e 100644
--- a/libraries/botbuilder-core/tests/test_memory_storage.py
+++ b/libraries/botbuilder-core/tests/test_memory_storage.py
@@ -1,9 +1,14 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License.
 
-import aiounittest
+import pytest
 
 from botbuilder.core import MemoryStorage, StoreItem
+from botbuilder.testing import StorageBaseTests
+
+
+def get_storage():
+    return MemoryStorage()
 
 
 class SimpleStoreItem(StoreItem):
@@ -13,7 +18,7 @@ def __init__(self, counter=1, e_tag="*"):
         self.e_tag = e_tag
 
 
-class TestMemoryStorage(aiounittest.AsyncTestCase):
+class TestMemoryStorageConstructor:
     def test_initializing_memory_storage_without_data_should_still_have_memory(self):
         storage = MemoryStorage()
         assert storage.memory is not None
@@ -23,8 +28,9 @@ def test_memory_storage__e_tag_should_start_at_0(self):
         storage = MemoryStorage()
         assert storage._e_tag == 0  # pylint: disable=protected-access
 
+    @pytest.mark.asyncio
     async def test_memory_storage_initialized_with_memory_should_have_accessible_data(
-        self
+        self,
     ):
         storage = MemoryStorage({"test": SimpleStoreItem()})
         data = await storage.read(["test"])
@@ -32,28 +38,77 @@ async def test_memory_storage_initialized_with_memory_should_have_accessible_dat
         assert data["test"].counter == 1
         assert len(data.keys()) == 1
 
-    async def test_memory_storage_read_should_return_data_with_valid_key(self):
-        storage = MemoryStorage()
-        await storage.write({"user": SimpleStoreItem()})
 
-        data = await storage.read(["user"])
-        assert "user" in data
-        assert data["user"].counter == 1
-        assert len(data.keys()) == 1
-        assert storage._e_tag == 1  # pylint: disable=protected-access
-        assert int(data["user"].e_tag) == 0
+class TestMemoryStorageBaseTests:
+    @pytest.mark.asyncio
+    async def test_return_empty_object_when_reading_unknown_key(self):
+        test_ran = await StorageBaseTests.return_empty_object_when_reading_unknown_key(
+            get_storage()
+        )
 
-    async def test_memory_storage_write_should_add_new_value(self):
-        storage = MemoryStorage()
-        aux = {"user": SimpleStoreItem(counter=1)}
-        await storage.write(aux)
+        assert test_ran
+
+    @pytest.mark.asyncio
+    async def test_handle_null_keys_when_reading(self):
+        test_ran = await StorageBaseTests.handle_null_keys_when_reading(get_storage())
+
+        assert test_ran
+
+    @pytest.mark.asyncio
+    async def test_handle_null_keys_when_writing(self):
+        test_ran = await StorageBaseTests.handle_null_keys_when_writing(get_storage())
+
+        assert test_ran
+
+    @pytest.mark.asyncio
+    async def test_does_not_raise_when_writing_no_items(self):
+        test_ran = await StorageBaseTests.does_not_raise_when_writing_no_items(
+            get_storage()
+        )
+
+        assert test_ran
+
+    @pytest.mark.asyncio
+    async def test_create_object(self):
+        test_ran = await StorageBaseTests.create_object(get_storage())
+
+        assert test_ran
+
+    @pytest.mark.asyncio
+    async def test_handle_crazy_keys(self):
+        test_ran = await StorageBaseTests.handle_crazy_keys(get_storage())
+
+        assert test_ran
+
+    @pytest.mark.asyncio
+    async def test_update_object(self):
+        test_ran = await StorageBaseTests.update_object(get_storage())
+
+        assert test_ran
+
+    @pytest.mark.asyncio
+    async def test_delete_object(self):
+        test_ran = await StorageBaseTests.delete_object(get_storage())
+
+        assert test_ran
+
+    @pytest.mark.asyncio
+    async def test_perform_batch_operations(self):
+        test_ran = await StorageBaseTests.perform_batch_operations(get_storage())
+
+        assert test_ran
+
+    @pytest.mark.asyncio
+    async def test_proceeds_through_waterfall(self):
+        test_ran = await StorageBaseTests.proceeds_through_waterfall(get_storage())
+
+        assert test_ran
 
-        data = await storage.read(["user"])
-        assert "user" in data
-        assert data["user"].counter == 1
 
+class TestMemoryStorage:
+    @pytest.mark.asyncio
     async def test_memory_storage_write_should_overwrite_when_new_e_tag_is_an_asterisk_1(
-        self
+        self,
     ):
         storage = MemoryStorage()
         await storage.write({"user": SimpleStoreItem(e_tag="1")})
@@ -62,8 +117,9 @@ async def test_memory_storage_write_should_overwrite_when_new_e_tag_is_an_asteri
         data = await storage.read(["user"])
         assert data["user"].counter == 10
 
+    @pytest.mark.asyncio
     async def test_memory_storage_write_should_overwrite_when_new_e_tag_is_an_asterisk_2(
-        self
+        self,
     ):
         storage = MemoryStorage()
         await storage.write({"user": SimpleStoreItem(e_tag="1")})
@@ -72,6 +128,7 @@ async def test_memory_storage_write_should_overwrite_when_new_e_tag_is_an_asteri
         data = await storage.read(["user"])
         assert data["user"].counter == 5
 
+    @pytest.mark.asyncio
     async def test_memory_storage_read_with_invalid_key_should_return_empty_dict(self):
         storage = MemoryStorage()
         data = await storage.read(["test"])
@@ -79,6 +136,7 @@ async def test_memory_storage_read_with_invalid_key_should_return_empty_dict(sel
         assert isinstance(data, dict)
         assert not data.keys()
 
+    @pytest.mark.asyncio
     async def test_memory_storage_delete_should_delete_according_cached_data(self):
         storage = MemoryStorage({"test": "test"})
         try:
@@ -91,8 +149,9 @@ async def test_memory_storage_delete_should_delete_according_cached_data(self):
             assert isinstance(data, dict)
             assert not data.keys()
 
+    @pytest.mark.asyncio
     async def test_memory_storage_delete_should_delete_multiple_values_when_given_multiple_valid_keys(
-        self
+        self,
     ):
         storage = MemoryStorage(
             {"test": SimpleStoreItem(), "test2": SimpleStoreItem(2, "2")}
@@ -102,8 +161,9 @@ async def test_memory_storage_delete_should_delete_multiple_values_when_given_mu
         data = await storage.read(["test", "test2"])
         assert not data.keys()
 
+    @pytest.mark.asyncio
     async def test_memory_storage_delete_should_delete_values_when_given_multiple_valid_keys_and_ignore_other_data(
-        self
+        self,
     ):
         storage = MemoryStorage(
             {
@@ -117,8 +177,9 @@ async def test_memory_storage_delete_should_delete_values_when_given_multiple_va
         data = await storage.read(["test", "test2", "test3"])
         assert len(data.keys()) == 1
 
+    @pytest.mark.asyncio
     async def test_memory_storage_delete_invalid_key_should_do_nothing_and_not_affect_cached_data(
-        self
+        self,
     ):
         storage = MemoryStorage({"test": "test"})
 
@@ -128,8 +189,9 @@ async def test_memory_storage_delete_invalid_key_should_do_nothing_and_not_affec
         data = await storage.read(["foo"])
         assert not data.keys()
 
+    @pytest.mark.asyncio
     async def test_memory_storage_delete_invalid_keys_should_do_nothing_and_not_affect_cached_data(
-        self
+        self,
     ):
         storage = MemoryStorage({"test": "test"})
 
diff --git a/libraries/botbuilder-core/tests/test_show_typing_middleware.py b/libraries/botbuilder-core/tests/test_show_typing_middleware.py
index 0d1af513c..9d0e0b7ce 100644
--- a/libraries/botbuilder-core/tests/test_show_typing_middleware.py
+++ b/libraries/botbuilder-core/tests/test_show_typing_middleware.py
@@ -1,68 +1,98 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import time
-import aiounittest
-
-from botbuilder.core import ShowTypingMiddleware
-from botbuilder.core.adapters import TestAdapter
-from botbuilder.schema import ActivityTypes
-
-
-class TestShowTypingMiddleware(aiounittest.AsyncTestCase):
-    async def test_should_automatically_send_a_typing_indicator(self):
-        async def aux(context):
-            time.sleep(0.600)
-            await context.send_activity(f"echo:{context.activity.text}")
-
-        def assert_is_typing(activity, description):  # pylint: disable=unused-argument
-            assert activity.type == ActivityTypes.typing
-
-        adapter = TestAdapter(aux)
-        adapter.use(ShowTypingMiddleware())
-
-        step1 = await adapter.send("foo")
-        step2 = await step1.assert_reply(assert_is_typing)
-        step3 = await step2.assert_reply("echo:foo")
-        step4 = await step3.send("bar")
-        step5 = await step4.assert_reply(assert_is_typing)
-        await step5.assert_reply("echo:bar")
-
-    async def test_should_not_automatically_send_a_typing_indicator_if_no_middleware(
-        self
-    ):
-        async def aux(context):
-            await context.send_activity(f"echo:{context.activity.text}")
-
-        adapter = TestAdapter(aux)
-
-        step1 = await adapter.send("foo")
-        await step1.assert_reply("echo:foo")
-
-    async def test_should_not_immediately_respond_with_message(self):
-        async def aux(context):
-            time.sleep(0.600)
-            await context.send_activity(f"echo:{context.activity.text}")
-
-        def assert_is_not_message(
-            activity, description
-        ):  # pylint: disable=unused-argument
-            assert activity.type != ActivityTypes.message
-
-        adapter = TestAdapter(aux)
-        adapter.use(ShowTypingMiddleware())
-
-        step1 = await adapter.send("foo")
-        await step1.assert_reply(assert_is_not_message)
-
-    async def test_should_immediately_respond_with_message_if_no_middleware(self):
-        async def aux(context):
-            await context.send_activity(f"echo:{context.activity.text}")
-
-        def assert_is_message(activity, description):  # pylint: disable=unused-argument
-            assert activity.type == ActivityTypes.message
-
-        adapter = TestAdapter(aux)
-
-        step1 = await adapter.send("foo")
-        await step1.assert_reply(assert_is_message)
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+import asyncio
+from uuid import uuid4
+import aiounittest
+
+from botbuilder.core import ShowTypingMiddleware, TurnContext
+from botbuilder.core.adapters import TestAdapter
+from botbuilder.schema import Activity, ActivityTypes
+from botframework.connector.auth import AuthenticationConstants, ClaimsIdentity
+
+
+class SkillTestAdapter(TestAdapter):
+    def create_turn_context(self, activity: Activity) -> TurnContext:
+        turn_context = super().create_turn_context(activity)
+
+        claims_identity = ClaimsIdentity(
+            claims={
+                AuthenticationConstants.VERSION_CLAIM: "2.0",
+                AuthenticationConstants.AUDIENCE_CLAIM: str(uuid4()),
+                AuthenticationConstants.AUTHORIZED_PARTY: str(uuid4()),
+            },
+            is_authenticated=True,
+        )
+
+        turn_context.turn_state[self.BOT_IDENTITY_KEY] = claims_identity
+
+        return turn_context
+
+
+class TestShowTypingMiddleware(aiounittest.AsyncTestCase):
+    async def test_should_automatically_send_a_typing_indicator(self):
+        async def aux(context):
+            await asyncio.sleep(0.600)
+            await context.send_activity(f"echo:{context.activity.text}")
+
+        def assert_is_typing(activity, description):  # pylint: disable=unused-argument
+            assert activity.type == ActivityTypes.typing
+
+        adapter = TestAdapter(aux)
+        adapter.use(ShowTypingMiddleware())
+
+        step1 = await adapter.send("foo")
+        step2 = await step1.assert_reply(assert_is_typing)
+        step3 = await step2.assert_reply("echo:foo")
+        step4 = await step3.send("bar")
+        step5 = await step4.assert_reply(assert_is_typing)
+        await step5.assert_reply("echo:bar")
+
+    async def test_should_not_automatically_send_a_typing_indicator_if_no_middleware(
+        self,
+    ):
+        async def aux(context):
+            await context.send_activity(f"echo:{context.activity.text}")
+
+        adapter = TestAdapter(aux)
+
+        step1 = await adapter.send("foo")
+        await step1.assert_reply("echo:foo")
+
+    async def test_should_not_immediately_respond_with_message(self):
+        async def aux(context):
+            await asyncio.sleep(0.600)
+            await context.send_activity(f"echo:{context.activity.text}")
+
+        def assert_is_not_message(
+            activity, description
+        ):  # pylint: disable=unused-argument
+            assert activity.type != ActivityTypes.message
+
+        adapter = TestAdapter(aux)
+        adapter.use(ShowTypingMiddleware())
+
+        step1 = await adapter.send("foo")
+        await step1.assert_reply(assert_is_not_message)
+
+    async def test_should_immediately_respond_with_message_if_no_middleware(self):
+        async def aux(context):
+            await context.send_activity(f"echo:{context.activity.text}")
+
+        def assert_is_message(activity, description):  # pylint: disable=unused-argument
+            assert activity.type == ActivityTypes.message
+
+        adapter = TestAdapter(aux)
+
+        step1 = await adapter.send("foo")
+        await step1.assert_reply(assert_is_message)
+
+    async def test_not_send_not_send_typing_indicator_when_bot_running_as_skill(self):
+        async def aux(context):
+            await asyncio.sleep(1)
+            await context.send_activity(f"echo:{context.activity.text}")
+
+        skill_adapter = SkillTestAdapter(aux)
+        skill_adapter.use(ShowTypingMiddleware(0.001, 1))
+
+        step1 = await skill_adapter.send("foo")
+        await step1.assert_reply("echo:foo")
diff --git a/libraries/botbuilder-core/tests/test_telemetry_middleware.py b/libraries/botbuilder-core/tests/test_telemetry_middleware.py
index 7128dab8d..6a5cc8e5d 100644
--- a/libraries/botbuilder-core/tests/test_telemetry_middleware.py
+++ b/libraries/botbuilder-core/tests/test_telemetry_middleware.py
@@ -3,14 +3,18 @@
 
 # pylint: disable=line-too-long,missing-docstring,unused-variable
 import copy
+import uuid
 from typing import Dict
 from unittest.mock import Mock
 import aiounittest
+from botframework.connector import Channels
+
 from botbuilder.core import (
     NullTelemetryClient,
     TelemetryLoggerMiddleware,
     TelemetryLoggerConstants,
     TurnContext,
+    MessageFactory,
 )
 from botbuilder.core.adapters import TestAdapter, TestFlow
 from botbuilder.schema import (
@@ -18,7 +22,9 @@
     ActivityTypes,
     ChannelAccount,
     ConversationAccount,
+    ConversationReference,
 )
+from botbuilder.schema.teams import TeamInfo, TeamsChannelData, TenantInfo
 
 
 class TestTelemetryMiddleware(aiounittest.AsyncTestCase):
@@ -28,6 +34,47 @@ async def test_create_middleware(self):
         my_logger = TelemetryLoggerMiddleware(telemetry, True)
         assert my_logger
 
+    async def test_do_not_throw_on_null_from(self):
+        telemetry = Mock()
+        my_logger = TelemetryLoggerMiddleware(telemetry, False)
+
+        adapter = TestAdapter(
+            template_or_conversation=Activity(
+                channel_id="test",
+                recipient=ChannelAccount(id="bot", name="Bot"),
+                conversation=ConversationAccount(id=str(uuid.uuid4())),
+            )
+        )
+        adapter.use(my_logger)
+
+        async def send_proactive(context: TurnContext):
+            await context.send_activity("proactive")
+
+        async def logic(context: TurnContext):
+            await adapter.create_conversation(
+                context.activity.channel_id, send_proactive,
+            )
+
+        adapter.logic = logic
+
+        test_flow = TestFlow(None, adapter)
+        await test_flow.send("foo")
+        await test_flow.assert_reply("proactive")
+
+        telemetry_calls = [
+            (
+                TelemetryLoggerConstants.BOT_MSG_RECEIVE_EVENT,
+                {
+                    "fromId": None,
+                    "conversationName": None,
+                    "locale": None,
+                    "recipientId": "bot",
+                    "recipientName": "Bot",
+                },
+            ),
+        ]
+        self.assert_telemetry_calls(telemetry, telemetry_calls)
+
     async def test_should_send_receive(self):
         telemetry = Mock()
         my_logger = TelemetryLoggerMiddleware(telemetry, True)
@@ -55,7 +102,7 @@ async def logic(context: TurnContext):
                     "conversationName": None,
                     "locale": None,
                     "recipientId": "bot",
-                    "recipientName": "user",
+                    "recipientName": "Bot",
                 },
             ),
             (
@@ -76,7 +123,7 @@ async def logic(context: TurnContext):
                     "conversationName": None,
                     "locale": None,
                     "recipientId": "bot",
-                    "recipientName": "user",
+                    "recipientName": "Bot",
                     "fromName": "user",
                 },
             ),
@@ -147,7 +194,7 @@ async def process(context: TurnContext) -> None:
                     "conversationName": None,
                     "locale": None,
                     "recipientId": "bot",
-                    "recipientName": "user",
+                    "recipientName": "Bot",
                 },
             ),
             (
@@ -169,7 +216,7 @@ async def process(context: TurnContext) -> None:
                     "conversationName": None,
                     "locale": None,
                     "recipientId": "bot",
-                    "recipientName": "user",
+                    "recipientName": "Bot",
                     "fromName": "user",
                 },
             ),
@@ -186,6 +233,47 @@ async def process(context: TurnContext) -> None:
         ]
         self.assert_telemetry_calls(telemetry, telemetry_call_expected)
 
+    async def test_log_teams(self):
+        telemetry = Mock()
+        my_logger = TelemetryLoggerMiddleware(telemetry, True)
+
+        adapter = TestAdapter(
+            template_or_conversation=ConversationReference(channel_id=Channels.ms_teams)
+        )
+        adapter.use(my_logger)
+
+        team_info = TeamInfo(id="teamId", name="teamName",)
+
+        channel_data = TeamsChannelData(
+            team=team_info, tenant=TenantInfo(id="tenantId"),
+        )
+
+        activity = MessageFactory.text("test")
+        activity.channel_data = channel_data
+        activity.from_property = ChannelAccount(
+            id="userId", name="userName", aad_object_id="aaId",
+        )
+
+        test_flow = TestFlow(None, adapter)
+        await test_flow.send(activity)
+
+        telemetry_call_expected = [
+            (
+                TelemetryLoggerConstants.BOT_MSG_RECEIVE_EVENT,
+                {
+                    "text": "test",
+                    "fromId": "userId",
+                    "recipientId": "bot",
+                    "recipientName": "Bot",
+                    "TeamsTenantId": TenantInfo(id="tenantId"),
+                    "TeamsUserAadObjectId": "aaId",
+                    "TeamsTeamInfo": TeamInfo.serialize(team_info),
+                },
+            ),
+        ]
+
+        self.assert_telemetry_calls(telemetry, telemetry_call_expected)
+
     def create_reply(self, activity, text, locale=None):
         return Activity(
             type=ActivityTypes.message,
diff --git a/libraries/botbuilder-core/tests/test_test_adapter.py b/libraries/botbuilder-core/tests/test_test_adapter.py
index 8f1fe2db8..447f74ead 100644
--- a/libraries/botbuilder-core/tests/test_test_adapter.py
+++ b/libraries/botbuilder-core/tests/test_test_adapter.py
@@ -1,137 +1,278 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import aiounittest
-from botbuilder.schema import Activity, ConversationReference
-from botbuilder.core import TurnContext
-from botbuilder.core.adapters import TestAdapter
-
-RECEIVED_MESSAGE = Activity(type="message", text="received")
-UPDATED_ACTIVITY = Activity(type="message", text="update")
-DELETED_ACTIVITY_REFERENCE = ConversationReference(activity_id="1234")
-
-
-class TestTestAdapter(aiounittest.AsyncTestCase):
-    async def test_should_call_bog_logic_when_receive_activity_is_called(self):
-        async def logic(context: TurnContext):
-            assert context
-            assert context.activity
-            assert context.activity.type == "message"
-            assert context.activity.text == "test"
-            assert context.activity.id
-            assert context.activity.from_property
-            assert context.activity.recipient
-            assert context.activity.conversation
-            assert context.activity.channel_id
-            assert context.activity.service_url
-
-        adapter = TestAdapter(logic)
-        await adapter.receive_activity("test")
-
-    async def test_should_support_receive_activity_with_activity(self):
-        async def logic(context: TurnContext):
-            assert context.activity.type == "message"
-            assert context.activity.text == "test"
-
-        adapter = TestAdapter(logic)
-        await adapter.receive_activity(Activity(type="message", text="test"))
-
-    async def test_should_set_activity_type_when_receive_activity_receives_activity_without_type(
-        self
-    ):
-        async def logic(context: TurnContext):
-            assert context.activity.type == "message"
-            assert context.activity.text == "test"
-
-        adapter = TestAdapter(logic)
-        await adapter.receive_activity(Activity(text="test"))
-
-    async def test_should_support_custom_activity_id_in_receive_activity(self):
-        async def logic(context: TurnContext):
-            assert context.activity.id == "myId"
-            assert context.activity.type == "message"
-            assert context.activity.text == "test"
-
-        adapter = TestAdapter(logic)
-        await adapter.receive_activity(Activity(type="message", text="test", id="myId"))
-
-    async def test_should_call_bot_logic_when_send_is_called(self):
-        async def logic(context: TurnContext):
-            assert context.activity.text == "test"
-
-        adapter = TestAdapter(logic)
-        await adapter.send("test")
-
-    async def test_should_send_and_receive_when_test_is_called(self):
-        async def logic(context: TurnContext):
-            await context.send_activity(RECEIVED_MESSAGE)
-
-        adapter = TestAdapter(logic)
-        await adapter.test("test", "received")
-
-    async def test_should_send_and_throw_assertion_error_when_test_is_called(self):
-        async def logic(context: TurnContext):
-            await context.send_activity(RECEIVED_MESSAGE)
-
-        adapter = TestAdapter(logic)
-        try:
-            await adapter.test("test", "foobar")
-        except AssertionError:
-            pass
-        else:
-            raise AssertionError("Assertion error should have been raised")
-
-    async def test_tests_should_call_test_for_each_tuple(self):
-        counter = 0
-
-        async def logic(context: TurnContext):
-            nonlocal counter
-            counter += 1
-            await context.send_activity(Activity(type="message", text=str(counter)))
-
-        adapter = TestAdapter(logic)
-        await adapter.tests(("test", "1"), ("test", "2"), ("test", "3"))
-        assert counter == 3
-
-    async def test_tests_should_call_test_for_each_list(self):
-        counter = 0
-
-        async def logic(context: TurnContext):
-            nonlocal counter
-            counter += 1
-            await context.send_activity(Activity(type="message", text=str(counter)))
-
-        adapter = TestAdapter(logic)
-        await adapter.tests(["test", "1"], ["test", "2"], ["test", "3"])
-        assert counter == 3
-
-    async def test_should_assert_reply_after_send(self):
-        async def logic(context: TurnContext):
-            await context.send_activity(RECEIVED_MESSAGE)
-
-        adapter = TestAdapter(logic)
-        test_flow = await adapter.send("test")
-        await test_flow.assert_reply("received")
-
-    async def test_should_support_context_update_activity_call(self):
-        async def logic(context: TurnContext):
-            await context.update_activity(UPDATED_ACTIVITY)
-            await context.send_activity(RECEIVED_MESSAGE)
-
-        adapter = TestAdapter(logic)
-        await adapter.test("test", "received")
-        assert len(adapter.updated_activities) == 1
-        assert adapter.updated_activities[0].text == UPDATED_ACTIVITY.text
-
-    async def test_should_support_context_delete_activity_call(self):
-        async def logic(context: TurnContext):
-            await context.delete_activity(DELETED_ACTIVITY_REFERENCE)
-            await context.send_activity(RECEIVED_MESSAGE)
-
-        adapter = TestAdapter(logic)
-        await adapter.test("test", "received")
-        assert len(adapter.deleted_activities) == 1
-        assert (
-            adapter.deleted_activities[0].activity_id
-            == DELETED_ACTIVITY_REFERENCE.activity_id
-        )
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import aiounittest
+
+from botframework.connector.auth import MicrosoftAppCredentials
+from botbuilder.core import TurnContext
+from botbuilder.core.adapters import TestAdapter
+from botbuilder.schema import Activity, ConversationReference, ChannelAccount
+
+RECEIVED_MESSAGE = Activity(type="message", text="received")
+UPDATED_ACTIVITY = Activity(type="message", text="update")
+DELETED_ACTIVITY_REFERENCE = ConversationReference(activity_id="1234")
+
+
+class TestTestAdapter(aiounittest.AsyncTestCase):
+    async def test_should_call_bog_logic_when_receive_activity_is_called(self):
+        async def logic(context: TurnContext):
+            assert context
+            assert context.activity
+            assert context.activity.type == "message"
+            assert context.activity.text == "test"
+            assert context.activity.id
+            assert context.activity.from_property
+            assert context.activity.recipient
+            assert context.activity.conversation
+            assert context.activity.channel_id
+            assert context.activity.service_url
+
+        adapter = TestAdapter(logic)
+        await adapter.receive_activity("test")
+
+    async def test_should_support_receive_activity_with_activity(self):
+        async def logic(context: TurnContext):
+            assert context.activity.type == "message"
+            assert context.activity.text == "test"
+
+        adapter = TestAdapter(logic)
+        await adapter.receive_activity(Activity(type="message", text="test"))
+
+    async def test_should_set_activity_type_when_receive_activity_receives_activity_without_type(
+        self,
+    ):
+        async def logic(context: TurnContext):
+            assert context.activity.type == "message"
+            assert context.activity.text == "test"
+
+        adapter = TestAdapter(logic)
+        await adapter.receive_activity(Activity(text="test"))
+
+    async def test_should_support_custom_activity_id_in_receive_activity(self):
+        async def logic(context: TurnContext):
+            assert context.activity.id == "myId"
+            assert context.activity.type == "message"
+            assert context.activity.text == "test"
+
+        adapter = TestAdapter(logic)
+        await adapter.receive_activity(Activity(type="message", text="test", id="myId"))
+
+    async def test_should_call_bot_logic_when_send_is_called(self):
+        async def logic(context: TurnContext):
+            assert context.activity.text == "test"
+
+        adapter = TestAdapter(logic)
+        await adapter.send("test")
+
+    async def test_should_send_and_receive_when_test_is_called(self):
+        async def logic(context: TurnContext):
+            await context.send_activity(RECEIVED_MESSAGE)
+
+        adapter = TestAdapter(logic)
+        await adapter.test("test", "received")
+
+    async def test_should_send_and_throw_assertion_error_when_test_is_called(self):
+        async def logic(context: TurnContext):
+            await context.send_activity(RECEIVED_MESSAGE)
+
+        adapter = TestAdapter(logic)
+        try:
+            await adapter.test("test", "foobar")
+        except AssertionError:
+            pass
+        else:
+            raise AssertionError("Assertion error should have been raised")
+
+    async def test_tests_should_call_test_for_each_tuple(self):
+        counter = 0
+
+        async def logic(context: TurnContext):
+            nonlocal counter
+            counter += 1
+            await context.send_activity(Activity(type="message", text=str(counter)))
+
+        adapter = TestAdapter(logic)
+        await adapter.tests(("test", "1"), ("test", "2"), ("test", "3"))
+        assert counter == 3
+
+    async def test_tests_should_call_test_for_each_list(self):
+        counter = 0
+
+        async def logic(context: TurnContext):
+            nonlocal counter
+            counter += 1
+            await context.send_activity(Activity(type="message", text=str(counter)))
+
+        adapter = TestAdapter(logic)
+        await adapter.tests(["test", "1"], ["test", "2"], ["test", "3"])
+        assert counter == 3
+
+    async def test_should_assert_reply_after_send(self):
+        async def logic(context: TurnContext):
+            await context.send_activity(RECEIVED_MESSAGE)
+
+        adapter = TestAdapter(logic)
+        test_flow = await adapter.send("test")
+        await test_flow.assert_reply("received")
+
+    async def test_should_support_context_update_activity_call(self):
+        async def logic(context: TurnContext):
+            await context.update_activity(UPDATED_ACTIVITY)
+            await context.send_activity(RECEIVED_MESSAGE)
+
+        adapter = TestAdapter(logic)
+        await adapter.test("test", "received")
+        assert len(adapter.updated_activities) == 1
+        assert adapter.updated_activities[0].text == UPDATED_ACTIVITY.text
+
+    async def test_should_support_context_delete_activity_call(self):
+        async def logic(context: TurnContext):
+            await context.delete_activity(DELETED_ACTIVITY_REFERENCE)
+            await context.send_activity(RECEIVED_MESSAGE)
+
+        adapter = TestAdapter(logic)
+        await adapter.test("test", "received")
+        assert len(adapter.deleted_activities) == 1
+        assert (
+            adapter.deleted_activities[0].activity_id
+            == DELETED_ACTIVITY_REFERENCE.activity_id
+        )
+
+    async def test_get_user_token_returns_null(self):
+        adapter = TestAdapter()
+        activity = Activity(
+            channel_id="directline", from_property=ChannelAccount(id="testuser")
+        )
+
+        turn_context = TurnContext(adapter, activity)
+
+        token_response = await adapter.get_user_token(turn_context, "myConnection")
+        assert not token_response
+
+        oauth_app_credentials = MicrosoftAppCredentials(None, None)
+        token_response = await adapter.get_user_token(
+            turn_context, "myConnection", oauth_app_credentials=oauth_app_credentials
+        )
+        assert not token_response
+
+    async def test_get_user_token_returns_null_with_code(self):
+        adapter = TestAdapter()
+        activity = Activity(
+            channel_id="directline", from_property=ChannelAccount(id="testuser")
+        )
+
+        turn_context = TurnContext(adapter, activity)
+
+        token_response = await adapter.get_user_token(
+            turn_context, "myConnection", "abc123"
+        )
+        assert not token_response
+
+        oauth_app_credentials = MicrosoftAppCredentials(None, None)
+        token_response = await adapter.get_user_token(
+            turn_context,
+            "myConnection",
+            "abc123",
+            oauth_app_credentials=oauth_app_credentials,
+        )
+        assert not token_response
+
+    async def test_get_user_token_returns_token(self):
+        adapter = TestAdapter()
+        connection_name = "myConnection"
+        channel_id = "directline"
+        user_id = "testUser"
+        token = "abc123"
+        activity = Activity(
+            channel_id=channel_id, from_property=ChannelAccount(id=user_id)
+        )
+
+        turn_context = TurnContext(adapter, activity)
+
+        adapter.add_user_token(connection_name, channel_id, user_id, token)
+
+        token_response = await adapter.get_user_token(turn_context, connection_name)
+        assert token_response
+        assert token == token_response.token
+        assert connection_name == token_response.connection_name
+
+        oauth_app_credentials = MicrosoftAppCredentials(None, None)
+        token_response = await adapter.get_user_token(
+            turn_context, connection_name, oauth_app_credentials=oauth_app_credentials
+        )
+        assert token_response
+        assert token == token_response.token
+        assert connection_name == token_response.connection_name
+
+    async def test_get_user_token_returns_token_with_magice_code(self):
+        adapter = TestAdapter()
+        connection_name = "myConnection"
+        channel_id = "directline"
+        user_id = "testUser"
+        token = "abc123"
+        magic_code = "888999"
+        activity = Activity(
+            channel_id=channel_id, from_property=ChannelAccount(id=user_id)
+        )
+
+        turn_context = TurnContext(adapter, activity)
+
+        adapter.add_user_token(connection_name, channel_id, user_id, token, magic_code)
+
+        # First no magic_code
+        token_response = await adapter.get_user_token(turn_context, connection_name)
+        assert not token_response
+
+        # Can be retrieved with magic code
+        token_response = await adapter.get_user_token(
+            turn_context, connection_name, magic_code
+        )
+        assert token_response
+        assert token == token_response.token
+        assert connection_name == token_response.connection_name
+
+        # Then can be retrieved without magic code
+        token_response = await adapter.get_user_token(turn_context, connection_name)
+        assert token_response
+        assert token == token_response.token
+        assert connection_name == token_response.connection_name
+
+        # Then can be retrieved using customized AppCredentials
+        oauth_app_credentials = MicrosoftAppCredentials(None, None)
+        token_response = await adapter.get_user_token(
+            turn_context, connection_name, oauth_app_credentials=oauth_app_credentials
+        )
+        assert token_response
+        assert token == token_response.token
+        assert connection_name == token_response.connection_name
+
+    async def test_should_validate_no_reply_when_no_reply_expected(self):
+        async def logic(context: TurnContext):
+            await context.send_activity(RECEIVED_MESSAGE)
+
+        adapter = TestAdapter(logic)
+        test_flow = await adapter.test("test", "received")
+        await test_flow.assert_no_reply("should be no additional replies")
+
+    async def test_should_timeout_waiting_for_assert_no_reply_when_no_reply_expected(
+        self,
+    ):
+        async def logic(context: TurnContext):
+            await context.send_activity(RECEIVED_MESSAGE)
+
+        adapter = TestAdapter(logic)
+        test_flow = await adapter.test("test", "received")
+        await test_flow.assert_no_reply("no reply received", 500)
+
+    async def test_should_throw_error_with_assert_no_reply_when_no_reply_expected_but_was_received(
+        self,
+    ):
+        async def logic(context: TurnContext):
+            activities = [RECEIVED_MESSAGE, RECEIVED_MESSAGE]
+            await context.send_activities(activities)
+
+        adapter = TestAdapter(logic)
+        test_flow = await adapter.test("test", "received")
+
+        with self.assertRaises(Exception):
+            await test_flow.assert_no_reply("should be no additional replies")
diff --git a/libraries/botbuilder-core/tests/test_transcript_logger_middleware.py b/libraries/botbuilder-core/tests/test_transcript_logger_middleware.py
new file mode 100644
index 000000000..2752043e5
--- /dev/null
+++ b/libraries/botbuilder-core/tests/test_transcript_logger_middleware.py
@@ -0,0 +1,44 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import aiounittest
+
+from botbuilder.core import (
+    MemoryTranscriptStore,
+    TranscriptLoggerMiddleware,
+    TurnContext,
+)
+from botbuilder.core.adapters import TestAdapter, TestFlow
+from botbuilder.schema import Activity, ActivityEventNames, ActivityTypes
+
+
+class TestTranscriptLoggerMiddleware(aiounittest.AsyncTestCase):
+    async def test_should_not_log_continue_conversation(self):
+        transcript_store = MemoryTranscriptStore()
+        conversation_id = ""
+        sut = TranscriptLoggerMiddleware(transcript_store)
+
+        async def aux_logic(context: TurnContext):
+            nonlocal conversation_id
+            conversation_id = context.activity.conversation.id
+
+        adapter = TestAdapter(aux_logic)
+        adapter.use(sut)
+
+        continue_conversation_activity = Activity(
+            type=ActivityTypes.event, name=ActivityEventNames.continue_conversation
+        )
+
+        test_flow = TestFlow(None, adapter)
+        step1 = await test_flow.send("foo")
+        step2 = await step1.send("bar")
+        await step2.send(continue_conversation_activity)
+
+        paged_result = await transcript_store.get_transcript_activities(
+            "test", conversation_id
+        )
+        self.assertEqual(
+            len(paged_result.items),
+            2,
+            "only the two message activities should be logged",
+        )
diff --git a/libraries/botbuilder-core/tests/test_turn_context.py b/libraries/botbuilder-core/tests/test_turn_context.py
index 8e7c6f407..8f4d3b6b6 100644
--- a/libraries/botbuilder-core/tests/test_turn_context.py
+++ b/libraries/botbuilder-core/tests/test_turn_context.py
@@ -1,300 +1,376 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import aiounittest
-
-from botbuilder.schema import (
-    Activity,
-    ChannelAccount,
-    ConversationAccount,
-    Mention,
-    ResourceResponse,
-)
-from botbuilder.core import BotAdapter, TurnContext
-
-ACTIVITY = Activity(
-    id="1234",
-    type="message",
-    text="test",
-    from_property=ChannelAccount(id="user", name="User Name"),
-    recipient=ChannelAccount(id="bot", name="Bot Name"),
-    conversation=ConversationAccount(id="convo", name="Convo Name"),
-    channel_id="UnitTest",
-    service_url="https://example.org",
-)
-
-
-class SimpleAdapter(BotAdapter):
-    async def send_activities(self, context, activities):
-        responses = []
-        assert context is not None
-        assert activities is not None
-        assert isinstance(activities, list)
-        assert activities
-        for (idx, activity) in enumerate(activities):  # pylint: disable=unused-variable
-            assert isinstance(activity, Activity)
-            assert activity.type == "message"
-            responses.append(ResourceResponse(id="5678"))
-        return responses
-
-    async def update_activity(self, context, activity):
-        assert context is not None
-        assert activity is not None
-
-    async def delete_activity(self, context, reference):
-        assert context is not None
-        assert reference is not None
-        assert reference.activity_id == "1234"
-
-
-class TestBotContext(aiounittest.AsyncTestCase):
-    def test_should_create_context_with_request_and_adapter(self):
-        TurnContext(SimpleAdapter(), ACTIVITY)
-
-    def test_should_not_create_context_without_request(self):
-        try:
-            TurnContext(SimpleAdapter(), None)
-        except TypeError:
-            pass
-        except Exception as error:
-            raise error
-
-    def test_should_not_create_context_without_adapter(self):
-        try:
-            TurnContext(None, ACTIVITY)
-        except TypeError:
-            pass
-        except Exception as error:
-            raise error
-
-    def test_should_create_context_with_older_context(self):
-        context = TurnContext(SimpleAdapter(), ACTIVITY)
-        TurnContext(context)
-
-    def test_copy_to_should_copy_all_references(self):
-        # pylint: disable=protected-access
-        old_adapter = SimpleAdapter()
-        old_activity = Activity(id="2", type="message", text="test copy")
-        old_context = TurnContext(old_adapter, old_activity)
-        old_context.responded = True
-
-        async def send_activities_handler(context, activities, next_handler):
-            assert context is not None
-            assert activities is not None
-            assert next_handler is not None
-            await next_handler
-
-        async def delete_activity_handler(context, reference, next_handler):
-            assert context is not None
-            assert reference is not None
-            assert next_handler is not None
-            await next_handler
-
-        async def update_activity_handler(context, activity, next_handler):
-            assert context is not None
-            assert activity is not None
-            assert next_handler is not None
-            await next_handler
-
-        old_context.on_send_activities(send_activities_handler)
-        old_context.on_delete_activity(delete_activity_handler)
-        old_context.on_update_activity(update_activity_handler)
-
-        adapter = SimpleAdapter()
-        new_context = TurnContext(adapter, ACTIVITY)
-        assert not new_context._on_send_activities  # pylint: disable=protected-access
-        assert not new_context._on_update_activity  # pylint: disable=protected-access
-        assert not new_context._on_delete_activity  # pylint: disable=protected-access
-
-        old_context.copy_to(new_context)
-
-        assert new_context.adapter == old_adapter
-        assert new_context.activity == old_activity
-        assert new_context.responded is True
-        assert (
-            len(new_context._on_send_activities) == 1
-        )  # pylint: disable=protected-access
-        assert (
-            len(new_context._on_update_activity) == 1
-        )  # pylint: disable=protected-access
-        assert (
-            len(new_context._on_delete_activity) == 1
-        )  # pylint: disable=protected-access
-
-    def test_responded_should_be_automatically_set_to_false(self):
-        context = TurnContext(SimpleAdapter(), ACTIVITY)
-        assert context.responded is False
-
-    def test_should_be_able_to_set_responded_to_true(self):
-        context = TurnContext(SimpleAdapter(), ACTIVITY)
-        assert context.responded is False
-        context.responded = True
-        assert context.responded
-
-    def test_should_not_be_able_to_set_responded_to_false(self):
-        context = TurnContext(SimpleAdapter(), ACTIVITY)
-        try:
-            context.responded = False
-        except ValueError:
-            pass
-        except Exception as error:
-            raise error
-
-    async def test_should_call_on_delete_activity_handlers_before_deletion(self):
-        context = TurnContext(SimpleAdapter(), ACTIVITY)
-        called = False
-
-        async def delete_handler(context, reference, next_handler_coroutine):
-            nonlocal called
-            called = True
-            assert reference is not None
-            assert context is not None
-            assert reference.activity_id == "1234"
-            await next_handler_coroutine()
-
-        context.on_delete_activity(delete_handler)
-        await context.delete_activity(ACTIVITY.id)
-        assert called is True
-
-    async def test_should_call_multiple_on_delete_activity_handlers_in_order(self):
-        context = TurnContext(SimpleAdapter(), ACTIVITY)
-        called_first = False
-        called_second = False
-
-        async def first_delete_handler(context, reference, next_handler_coroutine):
-            nonlocal called_first, called_second
-            assert (
-                called_first is False
-            ), "called_first should not be True before first_delete_handler is called."
-            called_first = True
-            assert (
-                called_second is False
-            ), "Second on_delete_activity handler was called before first."
-            assert reference is not None
-            assert context is not None
-            assert reference.activity_id == "1234"
-            await next_handler_coroutine()
-
-        async def second_delete_handler(context, reference, next_handler_coroutine):
-            nonlocal called_first, called_second
-            assert called_first
-            assert (
-                called_second is False
-            ), "called_second was set to True before second handler was called."
-            called_second = True
-            assert reference is not None
-            assert context is not None
-            assert reference.activity_id == "1234"
-            await next_handler_coroutine()
-
-        context.on_delete_activity(first_delete_handler)
-        context.on_delete_activity(second_delete_handler)
-        await context.delete_activity(ACTIVITY.id)
-        assert called_first is True
-        assert called_second is True
-
-    async def test_should_call_send_on_activities_handler_before_send(self):
-        context = TurnContext(SimpleAdapter(), ACTIVITY)
-        called = False
-
-        async def send_handler(context, activities, next_handler_coroutine):
-            nonlocal called
-            called = True
-            assert activities is not None
-            assert context is not None
-            assert activities[0].id == "1234"
-            await next_handler_coroutine()
-
-        context.on_send_activities(send_handler)
-        await context.send_activity(ACTIVITY)
-        assert called is True
-
-    async def test_should_call_on_update_activity_handler_before_update(self):
-        context = TurnContext(SimpleAdapter(), ACTIVITY)
-        called = False
-
-        async def update_handler(context, activity, next_handler_coroutine):
-            nonlocal called
-            called = True
-            assert activity is not None
-            assert context is not None
-            assert activity.id == "1234"
-            await next_handler_coroutine()
-
-        context.on_update_activity(update_handler)
-        await context.update_activity(ACTIVITY)
-        assert called is True
-
-    def test_get_conversation_reference_should_return_valid_reference(self):
-        reference = TurnContext.get_conversation_reference(ACTIVITY)
-
-        assert reference.activity_id == ACTIVITY.id
-        assert reference.user == ACTIVITY.from_property
-        assert reference.bot == ACTIVITY.recipient
-        assert reference.conversation == ACTIVITY.conversation
-        assert reference.channel_id == ACTIVITY.channel_id
-        assert reference.service_url == ACTIVITY.service_url
-
-    def test_apply_conversation_reference_should_return_prepare_reply_when_is_incoming_is_false(
-        self
-    ):
-        reference = TurnContext.get_conversation_reference(ACTIVITY)
-        reply = TurnContext.apply_conversation_reference(
-            Activity(type="message", text="reply"), reference
-        )
-
-        assert reply.recipient == ACTIVITY.from_property
-        assert reply.from_property == ACTIVITY.recipient
-        assert reply.conversation == ACTIVITY.conversation
-        assert reply.service_url == ACTIVITY.service_url
-        assert reply.channel_id == ACTIVITY.channel_id
-
-    def test_apply_conversation_reference_when_is_incoming_is_true_should_not_prepare_a_reply(
-        self
-    ):
-        reference = TurnContext.get_conversation_reference(ACTIVITY)
-        reply = TurnContext.apply_conversation_reference(
-            Activity(type="message", text="reply"), reference, True
-        )
-
-        assert reply.recipient == ACTIVITY.recipient
-        assert reply.from_property == ACTIVITY.from_property
-        assert reply.conversation == ACTIVITY.conversation
-        assert reply.service_url == ACTIVITY.service_url
-        assert reply.channel_id == ACTIVITY.channel_id
-
-    async def test_should_get_conversation_reference_using_get_reply_conversation_reference(
-        self
-    ):
-        context = TurnContext(SimpleAdapter(), ACTIVITY)
-        reply = await context.send_activity("test")
-
-        assert reply.id, "reply has an id"
-
-        reference = TurnContext.get_reply_conversation_reference(
-            context.activity, reply
-        )
-
-        assert reference.activity_id, "reference has an activity id"
-        assert (
-            reference.activity_id == reply.id
-        ), "reference id matches outgoing reply id"
-
-    def test_should_remove_at_mention_from_activity(self):
-        activity = Activity(
-            type="message",
-            text="TestOAuth619 test activity",
-            recipient=ChannelAccount(id="TestOAuth619"),
-            entities=[
-                Mention(
-                    type="mention",
-                    text="TestOAuth619",
-                    mentioned=ChannelAccount(name="Bot", id="TestOAuth619"),
-                )
-            ],
-        )
-
-        text = TurnContext.remove_recipient_mention(activity)
-
-        assert text, " test activity"
-        assert activity.text, " test activity"
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from typing import Callable, List
+import aiounittest
+
+from botbuilder.schema import (
+    Activity,
+    ActivityTypes,
+    ChannelAccount,
+    ConversationAccount,
+    Entity,
+    Mention,
+    ResourceResponse,
+)
+from botbuilder.core import BotAdapter, MessageFactory, TurnContext
+
+ACTIVITY = Activity(
+    id="1234",
+    type="message",
+    text="test",
+    from_property=ChannelAccount(id="user", name="User Name"),
+    recipient=ChannelAccount(id="bot", name="Bot Name"),
+    conversation=ConversationAccount(id="convo", name="Convo Name"),
+    channel_id="UnitTest",
+    locale="en-uS",  # Intentionally oddly-cased to check that it isn't defaulted somewhere, but tests stay in English
+    service_url="https://example.org",
+)
+
+
+class SimpleAdapter(BotAdapter):
+    async def send_activities(self, context, activities) -> List[ResourceResponse]:
+        responses = []
+        assert context is not None
+        assert activities is not None
+        assert isinstance(activities, list)
+        assert activities
+        for (idx, activity) in enumerate(activities):  # pylint: disable=unused-variable
+            assert isinstance(activity, Activity)
+            assert activity.type == "message" or activity.type == ActivityTypes.trace
+            responses.append(ResourceResponse(id="5678"))
+        return responses
+
+    async def update_activity(self, context, activity):
+        assert context is not None
+        assert activity is not None
+        return ResourceResponse(id=activity.id)
+
+    async def delete_activity(self, context, reference):
+        assert context is not None
+        assert reference is not None
+        assert reference.activity_id == ACTIVITY.id
+
+
+class TestBotContext(aiounittest.AsyncTestCase):
+    def test_should_create_context_with_request_and_adapter(self):
+        TurnContext(SimpleAdapter(), ACTIVITY)
+
+    def test_should_not_create_context_without_request(self):
+        try:
+            TurnContext(SimpleAdapter(), None)
+        except TypeError:
+            pass
+        except Exception as error:
+            raise error
+
+    def test_should_not_create_context_without_adapter(self):
+        try:
+            TurnContext(None, ACTIVITY)
+        except TypeError:
+            pass
+        except Exception as error:
+            raise error
+
+    def test_should_create_context_with_older_context(self):
+        context = TurnContext(SimpleAdapter(), ACTIVITY)
+        TurnContext(context)
+
+    def test_copy_to_should_copy_all_references(self):
+        # pylint: disable=protected-access
+        old_adapter = SimpleAdapter()
+        old_activity = Activity(id="2", type="message", text="test copy")
+        old_context = TurnContext(old_adapter, old_activity)
+        old_context.responded = True
+
+        async def send_activities_handler(context, activities, next_handler):
+            assert context is not None
+            assert activities is not None
+            assert next_handler is not None
+            await next_handler
+
+        async def delete_activity_handler(context, reference, next_handler):
+            assert context is not None
+            assert reference is not None
+            assert next_handler is not None
+            await next_handler
+
+        async def update_activity_handler(context, activity, next_handler):
+            assert context is not None
+            assert activity is not None
+            assert next_handler is not None
+            await next_handler
+
+        old_context.on_send_activities(send_activities_handler)
+        old_context.on_delete_activity(delete_activity_handler)
+        old_context.on_update_activity(update_activity_handler)
+
+        adapter = SimpleAdapter()
+        new_context = TurnContext(adapter, ACTIVITY)
+        assert not new_context._on_send_activities  # pylint: disable=protected-access
+        assert not new_context._on_update_activity  # pylint: disable=protected-access
+        assert not new_context._on_delete_activity  # pylint: disable=protected-access
+
+        old_context.copy_to(new_context)
+
+        assert new_context.adapter == old_adapter
+        assert new_context.activity == old_activity
+        assert new_context.responded is True
+        assert (
+            len(new_context._on_send_activities) == 1
+        )  # pylint: disable=protected-access
+        assert (
+            len(new_context._on_update_activity) == 1
+        )  # pylint: disable=protected-access
+        assert (
+            len(new_context._on_delete_activity) == 1
+        )  # pylint: disable=protected-access
+
+    def test_responded_should_be_automatically_set_to_false(self):
+        context = TurnContext(SimpleAdapter(), ACTIVITY)
+        assert context.responded is False
+
+    def test_should_be_able_to_set_responded_to_true(self):
+        context = TurnContext(SimpleAdapter(), ACTIVITY)
+        assert context.responded is False
+        context.responded = True
+        assert context.responded
+
+    def test_should_not_be_able_to_set_responded_to_false(self):
+        context = TurnContext(SimpleAdapter(), ACTIVITY)
+        try:
+            context.responded = False
+        except ValueError:
+            pass
+        except Exception as error:
+            raise error
+
+    async def test_should_call_on_delete_activity_handlers_before_deletion(self):
+        context = TurnContext(SimpleAdapter(), ACTIVITY)
+        called = False
+
+        async def delete_handler(context, reference, next_handler_coroutine):
+            nonlocal called
+            called = True
+            assert reference is not None
+            assert context is not None
+            assert reference.activity_id == "1234"
+            await next_handler_coroutine()
+
+        context.on_delete_activity(delete_handler)
+        await context.delete_activity(ACTIVITY.id)
+        assert called is True
+
+    async def test_should_call_multiple_on_delete_activity_handlers_in_order(self):
+        context = TurnContext(SimpleAdapter(), ACTIVITY)
+        called_first = False
+        called_second = False
+
+        async def first_delete_handler(context, reference, next_handler_coroutine):
+            nonlocal called_first, called_second
+            assert (
+                called_first is False
+            ), "called_first should not be True before first_delete_handler is called."
+            called_first = True
+            assert (
+                called_second is False
+            ), "Second on_delete_activity handler was called before first."
+            assert reference is not None
+            assert context is not None
+            assert reference.activity_id == "1234"
+            await next_handler_coroutine()
+
+        async def second_delete_handler(context, reference, next_handler_coroutine):
+            nonlocal called_first, called_second
+            assert called_first
+            assert (
+                called_second is False
+            ), "called_second was set to True before second handler was called."
+            called_second = True
+            assert reference is not None
+            assert context is not None
+            assert reference.activity_id == "1234"
+            await next_handler_coroutine()
+
+        context.on_delete_activity(first_delete_handler)
+        context.on_delete_activity(second_delete_handler)
+        await context.delete_activity(ACTIVITY.id)
+        assert called_first is True
+        assert called_second is True
+
+    async def test_should_call_send_on_activities_handler_before_send(self):
+        context = TurnContext(SimpleAdapter(), ACTIVITY)
+        called = False
+
+        async def send_handler(context, activities, next_handler_coroutine):
+            nonlocal called
+            called = True
+            assert activities is not None
+            assert context is not None
+            assert not activities[0].id
+            await next_handler_coroutine()
+
+        context.on_send_activities(send_handler)
+        await context.send_activity(ACTIVITY)
+        assert called is True
+
+    async def test_should_call_on_update_activity_handler_before_update(self):
+        context = TurnContext(SimpleAdapter(), ACTIVITY)
+        called = False
+
+        async def update_handler(context, activity, next_handler_coroutine):
+            nonlocal called
+            called = True
+            assert activity is not None
+            assert context is not None
+            assert activity.id == "1234"
+            await next_handler_coroutine()
+
+        context.on_update_activity(update_handler)
+        await context.update_activity(ACTIVITY)
+        assert called is True
+
+    async def test_update_activity_should_apply_conversation_reference(self):
+        activity_id = "activity ID"
+        context = TurnContext(SimpleAdapter(), ACTIVITY)
+        called = False
+
+        async def update_handler(context, activity, next_handler_coroutine):
+            nonlocal called
+            called = True
+            assert context is not None
+            assert activity.id == activity_id
+            assert activity.conversation.id == ACTIVITY.conversation.id
+            await next_handler_coroutine()
+
+        context.on_update_activity(update_handler)
+        new_activity = MessageFactory.text("test text")
+        new_activity.id = activity_id
+        update_result = await context.update_activity(new_activity)
+        assert called is True
+        assert update_result.id == activity_id
+
+    def test_get_conversation_reference_should_return_valid_reference(self):
+        reference = TurnContext.get_conversation_reference(ACTIVITY)
+
+        assert reference.activity_id == ACTIVITY.id
+        assert reference.user == ACTIVITY.from_property
+        assert reference.bot == ACTIVITY.recipient
+        assert reference.conversation == ACTIVITY.conversation
+        assert reference.channel_id == ACTIVITY.channel_id
+        assert reference.locale == ACTIVITY.locale
+        assert reference.service_url == ACTIVITY.service_url
+
+    def test_apply_conversation_reference_should_return_prepare_reply_when_is_incoming_is_false(
+        self,
+    ):
+        reference = TurnContext.get_conversation_reference(ACTIVITY)
+        reply = TurnContext.apply_conversation_reference(
+            Activity(type="message", text="reply"), reference
+        )
+
+        assert reply.recipient == ACTIVITY.from_property
+        assert reply.from_property == ACTIVITY.recipient
+        assert reply.conversation == ACTIVITY.conversation
+        assert reply.locale == ACTIVITY.locale
+        assert reply.service_url == ACTIVITY.service_url
+        assert reply.channel_id == ACTIVITY.channel_id
+
+    def test_apply_conversation_reference_when_is_incoming_is_true_should_not_prepare_a_reply(
+        self,
+    ):
+        reference = TurnContext.get_conversation_reference(ACTIVITY)
+        reply = TurnContext.apply_conversation_reference(
+            Activity(type="message", text="reply"), reference, True
+        )
+
+        assert reply.recipient == ACTIVITY.recipient
+        assert reply.from_property == ACTIVITY.from_property
+        assert reply.conversation == ACTIVITY.conversation
+        assert reply.locale == ACTIVITY.locale
+        assert reply.service_url == ACTIVITY.service_url
+        assert reply.channel_id == ACTIVITY.channel_id
+
+    async def test_should_get_conversation_reference_using_get_reply_conversation_reference(
+        self,
+    ):
+        context = TurnContext(SimpleAdapter(), ACTIVITY)
+        reply = await context.send_activity("test")
+
+        assert reply.id, "reply has an id"
+
+        reference = TurnContext.get_reply_conversation_reference(
+            context.activity, reply
+        )
+
+        assert reference.activity_id, "reference has an activity id"
+        assert (
+            reference.activity_id == reply.id
+        ), "reference id matches outgoing reply id"
+
+    def test_should_remove_at_mention_from_activity(self):
+        activity = Activity(
+            type="message",
+            text="TestOAuth619 test activity",
+            recipient=ChannelAccount(id="TestOAuth619"),
+            entities=[
+                Entity().deserialize(
+                    Mention(
+                        type="mention",
+                        text="TestOAuth619",
+                        mentioned=ChannelAccount(name="Bot", id="TestOAuth619"),
+                    ).serialize()
+                )
+            ],
+        )
+
+        text = TurnContext.remove_recipient_mention(activity)
+
+        assert text == " test activity"
+        assert activity.text == " test activity"
+
+    def test_should_remove_at_mention_with_regex_characters(self):
+        activity = Activity(
+            type="message",
+            text="Test (*.[]$%#^&?) test activity",
+            recipient=ChannelAccount(id="Test (*.[]$%#^&?)"),
+            entities=[
+                Entity().deserialize(
+                    Mention(
+                        type="mention",
+                        text="Test (*.[]$%#^&?)",
+                        mentioned=ChannelAccount(name="Bot", id="Test (*.[]$%#^&?)"),
+                    ).serialize()
+                )
+            ],
+        )
+
+        text = TurnContext.remove_recipient_mention(activity)
+
+        assert text == " test activity"
+        assert activity.text == " test activity"
+
+    async def test_should_send_a_trace_activity(self):
+        context = TurnContext(SimpleAdapter(), ACTIVITY)
+        called = False
+
+        #  pylint: disable=unused-argument
+        async def aux_func(
+            ctx: TurnContext, activities: List[Activity], next: Callable
+        ):
+            nonlocal called
+            called = True
+            assert isinstance(activities, list), "activities not array."
+            assert len(activities) == 1, "invalid count of activities."
+            assert activities[0].type == ActivityTypes.trace, "type wrong."
+            assert activities[0].name == "name-text", "name wrong."
+            assert activities[0].value == "value-text", "value worng."
+            assert activities[0].value_type == "valueType-text", "valeuType wrong."
+            assert activities[0].label == "label-text", "label wrong."
+            return []
+
+        context.on_send_activities(aux_func)
+        await context.send_trace_activity(
+            "name-text", "value-text", "valueType-text", "label-text"
+        )
+        assert called
diff --git a/libraries/botbuilder-dialogs/README.rst b/libraries/botbuilder-dialogs/README.rst
index 6c8208769..f76dc1983 100644
--- a/libraries/botbuilder-dialogs/README.rst
+++ b/libraries/botbuilder-dialogs/README.rst
@@ -3,8 +3,8 @@
 BotBuilder-Dialogs SDK for Python
 =================================
 
-.. image:: https://fuselabs.visualstudio.com/SDK_v4/_apis/build/status/Python/SDK_v4-Python-CI?branchName=master
-   :target:  https://fuselabs.visualstudio.com/SDK_v4/_apis/build/status/Python/SDK_v4-Python-CI
+.. image:: https://dev.azure.com/FuseLabs/SDK_v4/_apis/build/status/Python/Python-CI-PR-yaml?branchName=master
+   :target:  https://dev.azure.com/FuseLabs/SDK_v4/_apis/build/status/Python/Python-CI-PR-yaml?branchName=master
    :align: right
    :alt: Azure DevOps status for master branch
 .. image:: https://badge.fury.io/py/botbuilder-dialogs.svg
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/__init__.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/__init__.py
index 2d0447c3e..37c305536 100644
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/__init__.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/__init__.py
@@ -7,29 +7,46 @@
 
 from .about import __version__
 from .component_dialog import ComponentDialog
+from .dialog_container import DialogContainer
 from .dialog_context import DialogContext
+from .dialog_event import DialogEvent
+from .dialog_events import DialogEvents
 from .dialog_instance import DialogInstance
 from .dialog_reason import DialogReason
 from .dialog_set import DialogSet
 from .dialog_state import DialogState
 from .dialog_turn_result import DialogTurnResult
 from .dialog_turn_status import DialogTurnStatus
+from .dialog_manager import DialogManager
+from .dialog_manager_result import DialogManagerResult
 from .dialog import Dialog
+from .dialogs_component_registration import DialogsComponentRegistration
+from .persisted_state_keys import PersistedStateKeys
+from .persisted_state import PersistedState
 from .waterfall_dialog import WaterfallDialog
 from .waterfall_step_context import WaterfallStepContext
+from .dialog_extensions import DialogExtensions
 from .prompts import *
 from .choices import *
+from .skills import *
+from .object_path import ObjectPath
 
 __all__ = [
     "ComponentDialog",
+    "DialogContainer",
     "DialogContext",
+    "DialogEvent",
+    "DialogEvents",
     "DialogInstance",
     "DialogReason",
     "DialogSet",
     "DialogState",
     "DialogTurnResult",
     "DialogTurnStatus",
+    "DialogManager",
+    "DialogManagerResult",
     "Dialog",
+    "DialogsComponentRegistration",
     "WaterfallDialog",
     "WaterfallStepContext",
     "ConfirmPrompt",
@@ -38,10 +55,14 @@
     "NumberPrompt",
     "OAuthPrompt",
     "OAuthPromptSettings",
+    "PersistedStateKeys",
+    "PersistedState",
     "PromptRecognizerResult",
     "PromptValidatorContext",
     "Prompt",
     "PromptOptions",
     "TextPrompt",
+    "DialogExtensions",
+    "ObjectPath",
     "__version__",
 ]
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/about.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/about.py
index f8c29f033..7aa7b0a4f 100644
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/about.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/about.py
@@ -1,14 +1,14 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import os
-
-__title__ = "botbuilder-dialogs"
-__version__ = (
-    os.environ["packageVersion"] if "packageVersion" in os.environ else "4.4.0b1"
-)
-__uri__ = "https://www.github.com/Microsoft/botbuilder-python"
-__author__ = "Microsoft"
-__description__ = "Microsoft Bot Framework Bot Builder"
-__summary__ = "Microsoft Bot Framework Bot Builder SDK for Python."
-__license__ = "MIT"
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+
+__title__ = "botbuilder-dialogs"
+__version__ = (
+    os.environ["packageVersion"] if "packageVersion" in os.environ else "4.12.0"
+)
+__uri__ = "https://www.github.com/Microsoft/botbuilder-python"
+__author__ = "Microsoft"
+__description__ = "Microsoft Bot Framework Bot Builder"
+__summary__ = "Microsoft Bot Framework Bot Builder SDK for Python."
+__license__ = "MIT"
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/channel.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/channel.py
index 763791957..d3d532b22 100644
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/channel.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/channel.py
@@ -32,7 +32,6 @@ def supports_suggested_actions(channel_id: str, button_cnt: int = 100) -> bool:
             # https://dev.kik.com/#/docs/messaging#text-response-object
             Channels.kik: 20,
             Channels.telegram: 100,
-            Channels.slack: 100,
             Channels.emulator: 100,
             Channels.direct_line: 100,
             Channels.webchat: 100,
@@ -62,6 +61,7 @@ def supports_card_actions(channel_id: str, button_cnt: int = 100) -> bool:
             Channels.ms_teams: 3,
             Channels.line: 99,
             Channels.slack: 100,
+            Channels.telegram: 100,
             Channels.emulator: 100,
             Channels.direct_line: 100,
             Channels.webchat: 100,
@@ -88,7 +88,7 @@ def has_message_feed(channel_id: str) -> bool:
 
     @staticmethod
     def max_action_title_length(  # pylint: disable=unused-argument
-        channel_id: str
+        channel_id: str,
     ) -> int:
         """Maximum length allowed for Action Titles.
 
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/choice_factory.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/choice_factory.py
index a9d17f16f..52bf778b3 100644
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/choice_factory.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/choice_factory.py
@@ -69,7 +69,7 @@ def for_channel(
             # If the titles are short and there are 3 or less choices we'll use an inline list.
             return ChoiceFactory.inline(choices, text, speak, options)
         # Show a numbered list.
-        return [choices, text, speak, options]
+        return ChoiceFactory.list_style(choices, text, speak, options)
 
     @staticmethod
     def inline(
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/choice_recognizers.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/choice_recognizers.py
index 8ac8eb1a4..02fb71e6e 100644
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/choice_recognizers.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/choice_recognizers.py
@@ -1,140 +1,141 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from typing import List, Union
-from recognizers_number import NumberModel, NumberRecognizer, OrdinalModel
-from recognizers_text import Culture
-
-
-from .choice import Choice
-from .find import Find
-from .find_choices_options import FindChoicesOptions
-from .found_choice import FoundChoice
-from .model_result import ModelResult
-
-
-class ChoiceRecognizers:
-    """ Contains methods for matching user input against a list of choices. """
-
-    @staticmethod
-    def recognize_choices(
-        utterance: str,
-        choices: List[Union[str, Choice]],
-        options: FindChoicesOptions = None,
-    ) -> List[ModelResult]:
-        """
-        Matches user input against a list of choices.
-
-        This is layered above the `Find.find_choices()` function, and adds logic to let the user specify
-        their choice by index (they can say "one" to pick `choice[0]`) or ordinal position
-         (they can say "the second one" to pick `choice[1]`.)
-        The user's utterance is recognized in the following order:
-
-        - By name using `find_choices()`
-        - By 1's based ordinal position.
-        - By 1's based index position.
-
-        Parameters:
-        -----------
-
-        utterance: The input.
-
-        choices: The list of choices.
-
-        options: (Optional) Options to control the recognition strategy.
-
-        Returns:
-        --------
-        A list of found choices, sorted by most relevant first.
-        """
-        if utterance is None:
-            utterance = ""
-
-        # Normalize list of choices
-        choices_list = [
-            Choice(value=choice) if isinstance(choice, str) else choice
-            for choice in choices
-        ]
-
-        # Try finding choices by text search first
-        # - We only want to use a single strategy for returning results to avoid issues where utterances
-        #   like the "the third one" or "the red one" or "the first division book" would miss-recognize as
-        #   a numerical index or ordinal as well.
-        locale = options.locale if (options and options.locale) else Culture.English
-        matched = Find.find_choices(utterance, choices_list, options)
-        if not matched:
-            # Next try finding by ordinal
-            matches = ChoiceRecognizers._recognize_ordinal(utterance, locale)
-
-            if matches:
-                for match in matches:
-                    ChoiceRecognizers._match_choice_by_index(
-                        choices_list, matched, match
-                    )
-            else:
-                # Finally try by numerical index
-                matches = ChoiceRecognizers._recognize_number(utterance, locale)
-
-                for match in matches:
-                    ChoiceRecognizers._match_choice_by_index(
-                        choices_list, matched, match
-                    )
-
-            # Sort any found matches by their position within the utterance.
-            # - The results from find_choices() are already properly sorted so we just need this
-            #   for ordinal & numerical lookups.
-            matched = sorted(matched, key=lambda model_result: model_result.start)
-
-        return matched
-
-    @staticmethod
-    def _recognize_ordinal(utterance: str, culture: str) -> List[ModelResult]:
-        model: OrdinalModel = NumberRecognizer(culture).get_ordinal_model(culture)
-
-        return list(
-            map(ChoiceRecognizers._found_choice_constructor, model.parse(utterance))
-        )
-
-    @staticmethod
-    def _match_choice_by_index(
-        choices: List[Choice], matched: List[ModelResult], match: ModelResult
-    ):
-        try:
-            index: int = int(match.resolution.value) - 1
-            if 0 <= index < len(choices):
-                choice = choices[index]
-
-                matched.append(
-                    ModelResult(
-                        start=match.start,
-                        end=match.end,
-                        type_name="choice",
-                        text=match.text,
-                        resolution=FoundChoice(
-                            value=choice.value, index=index, score=1.0
-                        ),
-                    )
-                )
-        except:
-            # noop here, as in dotnet/node repos
-            pass
-
-    @staticmethod
-    def _recognize_number(utterance: str, culture: str) -> List[ModelResult]:
-        model: NumberModel = NumberRecognizer(culture).get_number_model(culture)
-
-        return list(
-            map(ChoiceRecognizers._found_choice_constructor, model.parse(utterance))
-        )
-
-    @staticmethod
-    def _found_choice_constructor(value_model: ModelResult) -> ModelResult:
-        return ModelResult(
-            start=value_model.start,
-            end=value_model.end,
-            type_name="choice",
-            text=value_model.text,
-            resolution=FoundChoice(
-                value=value_model.resolution["value"], index=0, score=1.0
-            ),
-        )
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from typing import List, Union
+from recognizers_number import NumberModel, NumberRecognizer, OrdinalModel
+from recognizers_text import Culture
+
+
+from .choice import Choice
+from .find import Find
+from .find_choices_options import FindChoicesOptions
+from .found_choice import FoundChoice
+from .model_result import ModelResult
+
+
+class ChoiceRecognizers:
+    """ Contains methods for matching user input against a list of choices. """
+
+    @staticmethod
+    def recognize_choices(
+        utterance: str,
+        choices: List[Union[str, Choice]],
+        options: FindChoicesOptions = None,
+    ) -> List[ModelResult]:
+        """
+        Matches user input against a list of choices.
+
+        This is layered above the `Find.find_choices()` function, and adds logic to let the user specify
+        their choice by index (they can say "one" to pick `choice[0]`) or ordinal position
+         (they can say "the second one" to pick `choice[1]`.)
+        The user's utterance is recognized in the following order:
+
+        - By name using `find_choices()`
+        - By 1's based ordinal position.
+        - By 1's based index position.
+
+        Parameters:
+        -----------
+
+        utterance: The input.
+
+        choices: The list of choices.
+
+        options: (Optional) Options to control the recognition strategy.
+
+        Returns:
+        --------
+        A list of found choices, sorted by most relevant first.
+        """
+        if utterance is None:
+            utterance = ""
+
+        # Normalize list of choices
+        choices_list = [
+            Choice(value=choice) if isinstance(choice, str) else choice
+            for choice in choices
+        ]
+
+        # Try finding choices by text search first
+        # - We only want to use a single strategy for returning results to avoid issues where utterances
+        #   like the "the third one" or "the red one" or "the first division book" would miss-recognize as
+        #   a numerical index or ordinal as well.
+        locale = options.locale if (options and options.locale) else Culture.English
+        matched = Find.find_choices(utterance, choices_list, options)
+        if not matched:
+            matches = []
+
+            if not options or options.recognize_ordinals:
+                # Next try finding by ordinal
+                matches = ChoiceRecognizers._recognize_ordinal(utterance, locale)
+                for match in matches:
+                    ChoiceRecognizers._match_choice_by_index(
+                        choices_list, matched, match
+                    )
+
+            if not matches and (not options or options.recognize_numbers):
+                # Then try by numerical index
+                matches = ChoiceRecognizers._recognize_number(utterance, locale)
+                for match in matches:
+                    ChoiceRecognizers._match_choice_by_index(
+                        choices_list, matched, match
+                    )
+
+            # Sort any found matches by their position within the utterance.
+            # - The results from find_choices() are already properly sorted so we just need this
+            #   for ordinal & numerical lookups.
+            matched = sorted(matched, key=lambda model_result: model_result.start)
+
+        return matched
+
+    @staticmethod
+    def _recognize_ordinal(utterance: str, culture: str) -> List[ModelResult]:
+        model: OrdinalModel = NumberRecognizer(culture).get_ordinal_model(culture)
+
+        return list(
+            map(ChoiceRecognizers._found_choice_constructor, model.parse(utterance))
+        )
+
+    @staticmethod
+    def _match_choice_by_index(
+        choices: List[Choice], matched: List[ModelResult], match: ModelResult
+    ):
+        try:
+            index: int = int(match.resolution.value) - 1
+            if 0 <= index < len(choices):
+                choice = choices[index]
+
+                matched.append(
+                    ModelResult(
+                        start=match.start,
+                        end=match.end,
+                        type_name="choice",
+                        text=match.text,
+                        resolution=FoundChoice(
+                            value=choice.value, index=index, score=1.0
+                        ),
+                    )
+                )
+        except:
+            # noop here, as in dotnet/node repos
+            pass
+
+    @staticmethod
+    def _recognize_number(utterance: str, culture: str) -> List[ModelResult]:
+        model: NumberModel = NumberRecognizer(culture).get_number_model(culture)
+
+        return list(
+            map(ChoiceRecognizers._found_choice_constructor, model.parse(utterance))
+        )
+
+    @staticmethod
+    def _found_choice_constructor(value_model: ModelResult) -> ModelResult:
+        return ModelResult(
+            start=value_model.start,
+            end=value_model.end,
+            type_name="choice",
+            text=value_model.text,
+            resolution=FoundChoice(
+                value=value_model.resolution["value"], index=0, score=1.0
+            ),
+        )
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/find.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/find.py
index 3cc951bb2..f7f5b3cab 100644
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/find.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/find.py
@@ -54,7 +54,7 @@ def find_choices(
                 synonyms.append(SortedValue(value=choice.action.title, index=index))
 
             if choice.synonyms is not None:
-                for synonym in synonyms:
+                for synonym in choice.synonyms:
                     synonyms.append(SortedValue(value=synonym, index=index))
 
         def found_choice_constructor(value_model: ModelResult) -> ModelResult:
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/find_choices_options.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/find_choices_options.py
index 8a51fce8e..418781ddb 100644
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/find_choices_options.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/choices/find_choices_options.py
@@ -1,23 +1,38 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from .find_values_options import FindValuesOptions
-
-
-class FindChoicesOptions(FindValuesOptions):
-    """ Contains options to control how input is matched against a list of choices """
-
-    def __init__(self, no_value: bool = None, no_action: bool = None, **kwargs):
-        """
-        Parameters:
-        -----------
-
-        no_value: (Optional) If `True`, the choices `value` field will NOT be search over. Defaults to `False`.
-
-        no_action: (Optional) If `True`, the choices `action.title` field will NOT be searched over.
-         Defaults to `False`.
-        """
-
-        super().__init__(**kwargs)
-        self.no_value = no_value
-        self.no_action = no_action
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from .find_values_options import FindValuesOptions
+
+
+class FindChoicesOptions(FindValuesOptions):
+    """ Contains options to control how input is matched against a list of choices """
+
+    def __init__(
+        self,
+        no_value: bool = None,
+        no_action: bool = None,
+        recognize_numbers: bool = True,
+        recognize_ordinals: bool = True,
+        **kwargs,
+    ):
+        """
+        Parameters:
+        -----------
+
+        no_value: (Optional) If `True`, the choices `value` field will NOT be search over. Defaults to `False`.
+
+        no_action: (Optional) If `True`, the choices `action.title` field will NOT be searched over.
+         Defaults to `False`.
+
+        recognize_numbers: (Optional) Indicates whether the recognizer should check for Numbers using the
+        NumberRecognizer's NumberModel.
+
+        recognize_ordinals: (Options) Indicates whether the recognizer should check for Ordinal Numbers using
+        the NumberRecognizer's OrdinalModel.
+        """
+
+        super().__init__(**kwargs)
+        self.no_value = no_value
+        self.no_action = no_action
+        self.recognize_numbers = recognize_numbers
+        self.recognize_ordinals = recognize_ordinals
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/component_dialog.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/component_dialog.py
index b4c531b23..f063b4827 100644
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/component_dialog.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/component_dialog.py
@@ -14,9 +14,22 @@
 
 
 class ComponentDialog(Dialog):
+    """
+    A :class:`botbuilder.dialogs.Dialog` that is composed of other dialogs
+
+    :var persisted_dialog state:
+    :vartype persisted_dialog_state: str
+    """
+
     persisted_dialog_state = "dialogs"
 
     def __init__(self, dialog_id: str):
+        """
+        Initializes a new instance of the :class:`ComponentDialog`
+
+        :param dialog_id: The ID to assign to the new dialog within the parent dialog set.
+        :type dialog_id: str
+        """
         super(ComponentDialog, self).__init__(dialog_id)
 
         if dialog_id is None:
@@ -30,6 +43,19 @@ def __init__(self, dialog_id: str):
     async def begin_dialog(
         self, dialog_context: DialogContext, options: object = None
     ) -> DialogTurnResult:
+        """
+        Called when the dialog is started and pushed onto the parent's dialog stack.
+
+        If the task is successful, the result indicates whether the dialog is still
+        active after the turn has been processed by the dialog.
+
+        :param dialog_context: The :class:`botbuilder.dialogs.DialogContext` for the current turn of the conversation.
+        :type dialog_context: :class:`botbuilder.dialogs.DialogContext`
+        :param options: Optional, initial information to pass to the dialog.
+        :type options: object
+        :return: Signals the end of the turn
+        :rtype: :class:`botbuilder.dialogs.Dialog.end_of_turn`
+        """
         if dialog_context is None:
             raise TypeError("ComponentDialog.begin_dialog(): outer_dc cannot be None.")
 
@@ -49,6 +75,27 @@ async def begin_dialog(
         return Dialog.end_of_turn
 
     async def continue_dialog(self, dialog_context: DialogContext) -> DialogTurnResult:
+        """
+        Called when the dialog is continued, where it is the active dialog and the
+        user replies with a new activity.
+
+        .. remarks::
+            If the task is successful, the result indicates whether the dialog is still
+            active after the turn has been processed by the dialog. The result may also
+            contain a return value.
+
+            If this method is *not* overriden the component dialog calls the
+            :meth:`botbuilder.dialogs.DialogContext.continue_dialog` method on it's inner dialog
+            context. If the inner dialog stack is empty, the component dialog ends,
+            and if a :class:`botbuilder.dialogs.DialogTurnResult.result` is available, the component dialog
+            uses that as it's return value.
+
+
+        :param dialog_context: The parent dialog context for the current turn of the conversation.
+        :type dialog_context: :class:`botbuilder.dialogs.DialogContext`
+        :return: Signals the end of the turn
+        :rtype: :class:`botbuilder.dialogs.Dialog.end_of_turn`
+        """
         if dialog_context is None:
             raise TypeError("ComponentDialog.begin_dialog(): outer_dc cannot be None.")
         # Continue execution of inner dialog.
@@ -65,17 +112,41 @@ async def continue_dialog(self, dialog_context: DialogContext) -> DialogTurnResu
     async def resume_dialog(
         self, dialog_context: DialogContext, reason: DialogReason, result: object = None
     ) -> DialogTurnResult:
-        # Containers are typically leaf nodes on the stack but the dev is free to push other dialogs
-        # on top of the stack which will result in the container receiving an unexpected call to
-        # resume_dialog() when the pushed on dialog ends.
-        # To avoid the container prematurely ending we need to implement this method and simply
-        # ask our inner dialog stack to re-prompt.
+        """
+        Called when a child dialog on the parent's dialog stack completed this turn, returning
+        control to this dialog component.
+
+        .. remarks::
+            Containers are typically leaf nodes on the stack but the dev is free to push other dialogs
+            on top of the stack which will result in the container receiving an unexpected call to
+            :meth:`ComponentDialog.resume_dialog()` when the pushed on dialog ends.
+            To avoid the container prematurely ending we need to implement this method and simply
+            ask our inner dialog stack to re-prompt.
+
+        :param dialog_context: The dialog context for the current turn of the conversation.
+        :type dialog_context: :class:`botbuilder.dialogs.DialogContext`
+        :param reason: Reason why the dialog resumed.
+        :type reason: :class:`botbuilder.dialogs.DialogReason`
+        :param result: Optional, value returned from the dialog that was called.
+        :type result: object
+        :return: Signals the end of the turn
+        :rtype: :class:`botbuilder.dialogs.Dialog.end_of_turn`
+        """
+
         await self.reprompt_dialog(dialog_context.context, dialog_context.active_dialog)
         return Dialog.end_of_turn
 
     async def reprompt_dialog(
         self, context: TurnContext, instance: DialogInstance
     ) -> None:
+        """
+        Called when the dialog should re-prompt the user for input.
+
+        :param context: The context object for this turn.
+        :type context: :class:`botbuilder.core.TurnContext`
+        :param instance: State information for this dialog.
+        :type instance: :class:`botbuilder.dialogs.DialogInstance`
+        """
         # Delegate to inner dialog.
         dialog_state = instance.state[self.persisted_dialog_state]
         inner_dc = DialogContext(self._dialogs, context, dialog_state)
@@ -87,7 +158,17 @@ async def reprompt_dialog(
     async def end_dialog(
         self, context: TurnContext, instance: DialogInstance, reason: DialogReason
     ) -> None:
-        # Forward cancel to inner dialogs
+        """
+        Called when the dialog is ending.
+
+        :param context: The context object for this turn.
+        :type context: :class:`botbuilder.core.TurnContext`
+        :param instance: State information associated with the instance of this component dialog.
+        :type instance: :class:`botbuilder.dialogs.DialogInstance`
+        :param reason: Reason why the dialog ended.
+        :type reason: :class:`botbuilder.dialogs.DialogReason`
+        """
+        # Forward cancel to inner dialog
         if reason == DialogReason.CancelCalled:
             dialog_state = instance.state[self.persisted_dialog_state]
             inner_dc = DialogContext(self._dialogs, context, dialog_state)
@@ -96,44 +177,99 @@ async def end_dialog(
 
     def add_dialog(self, dialog: Dialog) -> object:
         """
-        Adds a dialog to the component dialog.
-        Adding a new dialog will inherit the BotTelemetryClient of the ComponentDialog.
+        Adds a :class:`Dialog` to the component dialog and returns the updated component.
+
         :param dialog: The dialog to add.
-        :return: The updated ComponentDialog
+        :return: The updated :class:`ComponentDialog`.
+        :rtype: :class:`ComponentDialog`
         """
         self._dialogs.add(dialog)
         if not self.initial_dialog_id:
             self.initial_dialog_id = dialog.id
         return self
 
-    def find_dialog(self, dialog_id: str) -> Dialog:
+    async def find_dialog(self, dialog_id: str) -> Dialog:
         """
         Finds a dialog by ID.
-        Adding a new dialog will inherit the BotTelemetryClient of the ComponentDialog.
+
         :param dialog_id: The dialog to add.
         :return: The dialog; or None if there is not a match for the ID.
+        :rtype: :class:`botbuilder.dialogs.Dialog`
         """
-        return self._dialogs.find(dialog_id)
+        return await self._dialogs.find(dialog_id)
 
     async def on_begin_dialog(
         self, inner_dc: DialogContext, options: object
     ) -> DialogTurnResult:
+        """
+        Called when the dialog is started and pushed onto the parent's dialog stack.
+
+        .. remarks::
+            If the task is successful, the result indicates whether the dialog is still
+            active after the turn has been processed by the dialog.
+
+            By default, this calls the :meth:`botbuilder.dialogs.Dialog.begin_dialog()`
+            method of the component dialog's initial dialog.
+
+            Override this method in a derived class to implement interrupt logic.
+
+        :param inner_dc: The inner dialog context for the current turn of conversation.
+        :type inner_dc: :class:`botbuilder.dialogs.DialogContext`
+        :param options: Optional, initial information to pass to the dialog.
+        :type options: object
+        """
         return await inner_dc.begin_dialog(self.initial_dialog_id, options)
 
     async def on_continue_dialog(self, inner_dc: DialogContext) -> DialogTurnResult:
+        """
+        Called when the dialog is continued, where it is the active dialog and the user replies with a new activity.
+
+        :param inner_dc: The inner dialog context for the current turn of conversation.
+        :type inner_dc: :class:`botbuilder.dialogs.DialogContext`
+        """
         return await inner_dc.continue_dialog()
 
     async def on_end_dialog(  # pylint: disable=unused-argument
         self, context: TurnContext, instance: DialogInstance, reason: DialogReason
     ) -> None:
+        """
+        Ends the component dialog in its parent's context.
+
+        :param turn_context: The :class:`botbuilder.core.TurnContext` for the current turn of the conversation.
+        :type turn_context: :class:`botbuilder.core.TurnContext`
+        :param instance: State information associated with the inner dialog stack of this component dialog.
+        :type instance: :class:`botbuilder.dialogs.DialogInstance`
+        :param reason: Reason why the dialog ended.
+        :type reason: :class:`botbuilder.dialogs.DialogReason`
+        """
         return
 
     async def on_reprompt_dialog(  # pylint: disable=unused-argument
         self, turn_context: TurnContext, instance: DialogInstance
     ) -> None:
+        """
+        :param turn_context: The :class:`botbuilder.core.TurnContext` for the current turn of the conversation.
+        :type turn_context: :class:`botbuilder.dialogs.DialogInstance`
+        :param instance: State information associated with the inner dialog stack of this component dialog.
+        :type instance: :class:`botbuilder.dialogs.DialogInstance`
+        """
         return
 
     async def end_component(
         self, outer_dc: DialogContext, result: object  # pylint: disable=unused-argument
     ) -> DialogTurnResult:
+        """
+        Ends the component dialog in its parent's context.
+
+        .. remarks::
+            If the task is successful, the result indicates that the dialog ended after the
+            turn was processed by the dialog.
+
+        :param outer_dc: The parent dialog context for the current turn of conversation.
+        :type outer_dc: class:`botbuilder.dialogs.DialogContext`
+        :param result: Optional, value to return from the dialog component to the parent context.
+        :type result: object
+        :return: Value to return.
+        :rtype: :class:`botbuilder.dialogs.DialogTurnResult.result`
+        """
         return await outer_dc.end_dialog(result)
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog.py
index 63d816b94..22dfe342b 100644
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog.py
@@ -4,6 +4,7 @@
 
 from botbuilder.core import TurnContext, NullTelemetryClient, BotTelemetryClient
 from .dialog_reason import DialogReason
+from .dialog_event import DialogEvent
 from .dialog_turn_status import DialogTurnStatus
 from .dialog_turn_result import DialogTurnResult
 from .dialog_instance import DialogInstance
@@ -105,3 +106,83 @@ async def end_dialog(  # pylint: disable=unused-argument
         """
         # No-op by default
         return
+
+    def get_version(self) -> str:
+        return self.id
+
+    async def on_dialog_event(
+        self, dialog_context: "DialogContext", dialog_event: DialogEvent
+    ) -> bool:
+        """
+        Called when an event has been raised, using `DialogContext.emitEvent()`, by either the current dialog or a
+         dialog that the current dialog started.
+        :param dialog_context: The dialog context for the current turn of conversation.
+        :param dialog_event: The event being raised.
+        :return: True if the event is handled by the current dialog and bubbling should stop.
+        """
+        # Before bubble
+        handled = await self._on_pre_bubble_event(dialog_context, dialog_event)
+
+        # Bubble as needed
+        if (not handled) and dialog_event.bubble and dialog_context.parent:
+            handled = await dialog_context.parent.emit(
+                dialog_event.name, dialog_event.value, True, False
+            )
+
+        # Post bubble
+        if not handled:
+            handled = await self._on_post_bubble_event(dialog_context, dialog_event)
+
+        return handled
+
+    async def _on_pre_bubble_event(  # pylint: disable=unused-argument
+        self, dialog_context: "DialogContext", dialog_event: DialogEvent
+    ) -> bool:
+        """
+        Called before an event is bubbled to its parent.
+        This is a good place to perform interception of an event as returning `true` will prevent
+        any further bubbling of the event to the dialogs parents and will also prevent any child
+        dialogs from performing their default processing.
+        :param dialog_context: The dialog context for the current turn of conversation.
+        :param dialog_event: The event being raised.
+        :return: Whether the event is handled by the current dialog and further processing should stop.
+        """
+        return False
+
+    async def _on_post_bubble_event(  # pylint: disable=unused-argument
+        self, dialog_context: "DialogContext", dialog_event: DialogEvent
+    ) -> bool:
+        """
+        Called after an event was bubbled to all parents and wasn't handled.
+        This is a good place to perform default processing logic for an event. Returning `true` will
+        prevent any processing of the event by child dialogs.
+        :param dialog_context: The dialog context for the current turn of conversation.
+        :param dialog_event: The event being raised.
+        :return: Whether the event is handled by the current dialog and further processing should stop.
+        """
+        return False
+
+    def _on_compute_id(self) -> str:
+        """
+        Computes an unique ID for a dialog.
+        :return: An unique ID for a dialog
+        """
+        return self.__class__.__name__
+
+    def _register_source_location(
+        self, path: str, line_number: int
+    ):  # pylint: disable=unused-argument
+        """
+        Registers a SourceRange in the provided location.
+        :param path: The path to the source file.
+        :param line_number: The line number where the source will be located on the file.
+        :return:
+        """
+        if path:
+            # This will be added when debbuging support is ported.
+            # DebugSupport.source_map.add(self, SourceRange(
+            #     path = path,
+            #     start_point = SourcePoint(line_index = line_number, char_index = 0 ),
+            #     end_point = SourcePoint(line_index = line_number + 1, char_index = 0 ),
+            # )
+            return
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_container.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_container.py
new file mode 100644
index 000000000..ad2326419
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_container.py
@@ -0,0 +1,83 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from abc import ABC, abstractmethod
+
+
+from .dialog import Dialog
+from .dialog_context import DialogContext
+from .dialog_event import DialogEvent
+from .dialog_events import DialogEvents
+from .dialog_set import DialogSet
+
+
+class DialogContainer(Dialog, ABC):
+    def __init__(self, dialog_id: str = None):
+        super().__init__(dialog_id)
+
+        self.dialogs = DialogSet()
+
+    @abstractmethod
+    def create_child_context(self, dialog_context: DialogContext) -> DialogContext:
+        raise NotImplementedError()
+
+    def find_dialog(self, dialog_id: str) -> Dialog:
+        # TODO: deprecate DialogSet.find
+        return self.dialogs.find_dialog(dialog_id)
+
+    async def on_dialog_event(
+        self, dialog_context: DialogContext, dialog_event: DialogEvent
+    ) -> bool:
+        """
+        Called when an event has been raised, using `DialogContext.emitEvent()`, by either the current dialog or a
+         dialog that the current dialog started.
+        :param dialog_context: The dialog context for the current turn of conversation.
+        :param dialog_event: The event being raised.
+        :return: True if the event is handled by the current dialog and bubbling should stop.
+        """
+        handled = await super().on_dialog_event(dialog_context, dialog_event)
+
+        # Trace unhandled "versionChanged" events.
+        if not handled and dialog_event.name == DialogEvents.version_changed:
+
+            trace_message = (
+                f"Unhandled dialog event: {dialog_event.name}. Active Dialog: "
+                f"{dialog_context.active_dialog.id}"
+            )
+
+            await dialog_context.context.send_trace_activity(trace_message)
+
+        return handled
+
+    def get_internal_version(self) -> str:
+        """
+        GetInternalVersion - Returns internal version identifier for this container.
+        DialogContainers detect changes of all sub-components in the container and map that to an DialogChanged event.
+        Because they do this, DialogContainers "hide" the internal changes and just have the .id. This isolates changes
+        to the container level unless a container doesn't handle it.  To support this DialogContainers define a
+        protected virtual method GetInternalVersion() which computes if this dialog or child dialogs have changed
+        which is then examined via calls to check_for_version_change_async().
+        :return: version which represents the change of the internals of this container.
+        """
+        return self.dialogs.get_version()
+
+    async def check_for_version_change_async(self, dialog_context: DialogContext):
+        """
+        :param dialog_context: dialog context.
+        :return: task.
+        Checks to see if a containers child dialogs have changed since the current dialog instance
+        was started.
+
+        This should be called at the start of `beginDialog()`, `continueDialog()`, and `resumeDialog()`.
+        """
+        current = dialog_context.active_dialog.version
+        dialog_context.active_dialog.version = self.get_internal_version()
+
+        # Check for change of previously stored hash
+        if current and current != dialog_context.active_dialog.version:
+            # Give bot an opportunity to handle the change.
+            # - If bot handles it the changeHash will have been updated as to avoid triggering the
+            #   change again.
+            await dialog_context.emit_event(
+                DialogEvents.version_changed, self.id, True, False
+            )
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_context.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_context.py
index 2862acfb0..b10a63978 100644
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_context.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_context.py
@@ -1,7 +1,14 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License.
 
+from typing import List, Optional
+
 from botbuilder.core.turn_context import TurnContext
+from botbuilder.dialogs.memory import DialogStateManager
+
+from .dialog_event import DialogEvent
+from .dialog_events import DialogEvents
+from .dialog_set import DialogSet
 from .dialog_state import DialogState
 from .dialog_turn_status import DialogTurnStatus
 from .dialog_turn_result import DialogTurnResult
@@ -12,7 +19,7 @@
 
 class DialogContext:
     def __init__(
-        self, dialog_set: object, turn_context: TurnContext, state: DialogState
+        self, dialog_set: DialogSet, turn_context: TurnContext, state: DialogState
     ):
         if dialog_set is None:
             raise TypeError("DialogContext(): dialog_set cannot be None.")
@@ -21,16 +28,17 @@ def __init__(
             raise TypeError("DialogContext(): turn_context cannot be None.")
         self._turn_context = turn_context
         self._dialogs = dialog_set
-        # self._id = dialog_id;
         self._stack = state.dialog_stack
-        self.parent = None
+        self.services = {}
+        self.parent: DialogContext = None
+        self.state = DialogStateManager(self)
 
     @property
-    def dialogs(self):
+    def dialogs(self) -> DialogSet:
         """Gets the set of dialogs that can be called from this context.
 
         :param:
-        :return str:
+        :return DialogSet:
         """
         return self._dialogs
 
@@ -39,16 +47,16 @@ def context(self) -> TurnContext:
         """Gets the context for the current turn of conversation.
 
         :param:
-        :return str:
+        :return TurnContext:
         """
         return self._turn_context
 
     @property
-    def stack(self):
+    def stack(self) -> List:
         """Gets the current dialog stack.
 
         :param:
-        :return str:
+        :return list:
         """
         return self._stack
 
@@ -57,38 +65,63 @@ def active_dialog(self):
         """Return the container link in the database.
 
         :param:
-        :return str:
+        :return:
         """
         if self._stack:
             return self._stack[0]
         return None
 
+    @property
+    def child(self) -> Optional["DialogContext"]:
+        """Return the container link in the database.
+
+        :param:
+        :return DialogContext:
+        """
+        # pylint: disable=import-outside-toplevel
+        instance = self.active_dialog
+
+        if instance:
+            dialog = self.find_dialog_sync(instance.id)
+
+            # This import prevents circular dependency issues
+            from .dialog_container import DialogContainer
+
+            if isinstance(dialog, DialogContainer):
+                return dialog.create_child_context(self)
+
+        return None
+
     async def begin_dialog(self, dialog_id: str, options: object = None):
         """
         Pushes a new dialog onto the dialog stack.
-        :param dialog_id: ID of the dialog to start..
+        :param dialog_id: ID of the dialog to start
         :param options: (Optional) additional argument(s) to pass to the dialog being started.
         """
-        if not dialog_id:
-            raise TypeError("Dialog(): dialogId cannot be None.")
-        # Look up dialog
-        dialog = await self.find_dialog(dialog_id)
-        if dialog is None:
-            raise Exception(
-                "'DialogContext.begin_dialog(): A dialog with an id of '%s' wasn't found."
-                " The dialog must be included in the current or parent DialogSet."
-                " For example, if subclassing a ComponentDialog you can call add_dialog() within your constructor."
-                % dialog_id
-            )
-        # Push new instance onto stack
-        instance = DialogInstance()
-        instance.id = dialog_id
-        instance.state = {}
-
-        self._stack.insert(0, (instance))
-
-        # Call dialog's begin_dialog() method
-        return await dialog.begin_dialog(self, options)
+        try:
+            if not dialog_id:
+                raise TypeError("Dialog(): dialog_id cannot be None.")
+            # Look up dialog
+            dialog = await self.find_dialog(dialog_id)
+            if dialog is None:
+                raise Exception(
+                    "'DialogContext.begin_dialog(): A dialog with an id of '%s' wasn't found."
+                    " The dialog must be included in the current or parent DialogSet."
+                    " For example, if subclassing a ComponentDialog you can call add_dialog() within your constructor."
+                    % dialog_id
+                )
+            # Push new instance onto stack
+            instance = DialogInstance()
+            instance.id = dialog_id
+            instance.state = {}
+
+            self._stack.insert(0, (instance))
+
+            # Call dialog's begin_dialog() method
+            return await dialog.begin_dialog(self, options)
+        except Exception as err:
+            self.__set_exception_context_data(err)
+            raise
 
     # TODO: Fix options: PromptOptions instead of object
     async def prompt(self, dialog_id: str, options) -> DialogTurnResult:
@@ -99,13 +132,17 @@ async def prompt(self, dialog_id: str, options) -> DialogTurnResult:
         :param options: Contains a Prompt, potentially a RetryPrompt and if using ChoicePrompt, Choices.
         :return:
         """
-        if not dialog_id:
-            raise TypeError("DialogContext.prompt(): dialogId cannot be None.")
+        try:
+            if not dialog_id:
+                raise TypeError("DialogContext.prompt(): dialogId cannot be None.")
 
-        if not options:
-            raise TypeError("DialogContext.prompt(): options cannot be None.")
+            if not options:
+                raise TypeError("DialogContext.prompt(): options cannot be None.")
 
-        return await self.begin_dialog(dialog_id, options)
+            return await self.begin_dialog(dialog_id, options)
+        except Exception as err:
+            self.__set_exception_context_data(err)
+            raise
 
     async def continue_dialog(self):
         """
@@ -114,20 +151,25 @@ async def continue_dialog(self):
         to determine if a dialog was run and a reply was sent to the user.
         :return:
         """
-        # Check for a dialog on the stack
-        if self.active_dialog is not None:
-            # Look up dialog
-            dialog = await self.find_dialog(self.active_dialog.id)
-            if not dialog:
-                raise Exception(
-                    "DialogContext.continue_dialog(): Can't continue dialog. A dialog with an id of '%s' wasn't found."
-                    % self.active_dialog.id
-                )
-
-            # Continue execution of dialog
-            return await dialog.continue_dialog(self)
-
-        return DialogTurnResult(DialogTurnStatus.Empty)
+        try:
+            # Check for a dialog on the stack
+            if self.active_dialog is not None:
+                # Look up dialog
+                dialog = await self.find_dialog(self.active_dialog.id)
+                if not dialog:
+                    raise Exception(
+                        "DialogContext.continue_dialog(): Can't continue dialog. "
+                        "A dialog with an id of '%s' wasn't found."
+                        % self.active_dialog.id
+                    )
+
+                # Continue execution of dialog
+                return await dialog.continue_dialog(self)
+
+            return DialogTurnResult(DialogTurnStatus.Empty)
+        except Exception as err:
+            self.__set_exception_context_data(err)
+            raise
 
     # TODO: instance is DialogInstance
     async def end_dialog(self, result: object = None):
@@ -142,35 +184,89 @@ async def end_dialog(self, result: object = None):
         :param result: (Optional) result to pass to the parent dialogs.
         :return:
         """
-        await self.end_active_dialog(DialogReason.EndCalled)
-
-        # Resume previous dialog
-        if self.active_dialog is not None:
-            # Look up dialog
-            dialog = await self.find_dialog(self.active_dialog.id)
-            if not dialog:
-                raise Exception(
-                    "DialogContext.EndDialogAsync(): Can't resume previous dialog."
-                    " A dialog with an id of '%s' wasn't found." % self.active_dialog.id
-                )
-
-            # Return result to previous dialog
-            return await dialog.resume_dialog(self, DialogReason.EndCalled, result)
-
-        return DialogTurnResult(DialogTurnStatus.Complete, result)
-
-    async def cancel_all_dialogs(self):
+        try:
+            await self.end_active_dialog(DialogReason.EndCalled)
+
+            # Resume previous dialog
+            if self.active_dialog is not None:
+                # Look up dialog
+                dialog = await self.find_dialog(self.active_dialog.id)
+                if not dialog:
+                    raise Exception(
+                        "DialogContext.EndDialogAsync(): Can't resume previous dialog."
+                        " A dialog with an id of '%s' wasn't found."
+                        % self.active_dialog.id
+                    )
+
+                # Return result to previous dialog
+                return await dialog.resume_dialog(self, DialogReason.EndCalled, result)
+
+            return DialogTurnResult(DialogTurnStatus.Complete, result)
+        except Exception as err:
+            self.__set_exception_context_data(err)
+            raise
+
+    async def cancel_all_dialogs(
+        self,
+        cancel_parents: bool = None,
+        event_name: str = None,
+        event_value: object = None,
+    ):
         """
         Deletes any existing dialog stack thus cancelling all dialogs on the stack.
-        :param result: (Optional) result to pass to the parent dialogs.
+        :param cancel_parents:
+        :param event_name:
+        :param event_value:
         :return:
         """
-        if self.stack:
-            while self.stack:
-                await self.end_active_dialog(DialogReason.CancelCalled)
-            return DialogTurnResult(DialogTurnStatus.Cancelled)
-
-        return DialogTurnResult(DialogTurnStatus.Empty)
+        # pylint: disable=too-many-nested-blocks
+        try:
+            if cancel_parents is None:
+                event_name = event_name or DialogEvents.cancel_dialog
+
+                if self.stack or self.parent:
+                    # Cancel all local and parent dialogs while checking for interception
+                    notify = False
+                    dialog_context = self
+
+                    while dialog_context:
+                        if dialog_context.stack:
+                            # Check to see if the dialog wants to handle the event
+                            if notify:
+                                event_handled = await dialog_context.emit_event(
+                                    event_name,
+                                    event_value,
+                                    bubble=False,
+                                    from_leaf=False,
+                                )
+
+                                if event_handled:
+                                    break
+
+                            # End the active dialog
+                            await dialog_context.end_active_dialog(
+                                DialogReason.CancelCalled
+                            )
+                        else:
+                            dialog_context = (
+                                dialog_context.parent if cancel_parents else None
+                            )
+
+                        notify = True
+
+                    return DialogTurnResult(DialogTurnStatus.Cancelled)
+                # Stack was empty and no parent
+                return DialogTurnResult(DialogTurnStatus.Empty)
+
+            if self.stack:
+                while self.stack:
+                    await self.end_active_dialog(DialogReason.CancelCalled)
+                return DialogTurnResult(DialogTurnStatus.Cancelled)
+
+            return DialogTurnResult(DialogTurnStatus.Empty)
+        except Exception as err:
+            self.__set_exception_context_data(err)
+            raise
 
     async def find_dialog(self, dialog_id: str) -> Dialog:
         """
@@ -179,10 +275,27 @@ async def find_dialog(self, dialog_id: str) -> Dialog:
         :param dialog_id: ID of the dialog to search for.
         :return:
         """
-        dialog = await self.dialogs.find(dialog_id)
+        try:
+            dialog = await self.dialogs.find(dialog_id)
+
+            if dialog is None and self.parent is not None:
+                dialog = await self.parent.find_dialog(dialog_id)
+            return dialog
+        except Exception as err:
+            self.__set_exception_context_data(err)
+            raise
+
+    def find_dialog_sync(self, dialog_id: str) -> Dialog:
+        """
+        If the dialog cannot be found within the current `DialogSet`, the parent `DialogContext`
+        will be searched if there is one.
+        :param dialog_id: ID of the dialog to search for.
+        :return:
+        """
+        dialog = self.dialogs.find_dialog(dialog_id)
 
         if dialog is None and self.parent is not None:
-            dialog = await self.parent.find_dialog(dialog_id)
+            dialog = self.parent.find_dialog_sync(dialog_id)
         return dialog
 
     async def replace_dialog(
@@ -195,29 +308,37 @@ async def replace_dialog(
         :param options: (Optional) additional argument(s) to pass to the new dialog.
         :return:
         """
-        # End the current dialog and giving the reason.
-        await self.end_active_dialog(DialogReason.ReplaceCalled)
+        try:
+            # End the current dialog and giving the reason.
+            await self.end_active_dialog(DialogReason.ReplaceCalled)
 
-        # Start replacement dialog
-        return await self.begin_dialog(dialog_id, options)
+            # Start replacement dialog
+            return await self.begin_dialog(dialog_id, options)
+        except Exception as err:
+            self.__set_exception_context_data(err)
+            raise
 
     async def reprompt_dialog(self):
         """
         Calls reprompt on the currently active dialog, if there is one. Used with Prompts that have a reprompt behavior.
         :return:
         """
-        # Check for a dialog on the stack
-        if self.active_dialog is not None:
-            # Look up dialog
-            dialog = await self.find_dialog(self.active_dialog.id)
-            if not dialog:
-                raise Exception(
-                    "DialogSet.reprompt_dialog(): Can't find A dialog with an id of '%s'."
-                    % self.active_dialog.id
-                )
-
-            # Ask dialog to re-prompt if supported
-            await dialog.reprompt_dialog(self.context, self.active_dialog)
+        try:
+            # Check for a dialog on the stack
+            if self.active_dialog is not None:
+                # Look up dialog
+                dialog = await self.find_dialog(self.active_dialog.id)
+                if not dialog:
+                    raise Exception(
+                        "DialogSet.reprompt_dialog(): Can't find A dialog with an id of '%s'."
+                        % self.active_dialog.id
+                    )
+
+                # Ask dialog to re-prompt if supported
+                await dialog.reprompt_dialog(self.context, self.active_dialog)
+        except Exception as err:
+            self.__set_exception_context_data(err)
+            raise
 
     async def end_active_dialog(self, reason: DialogReason):
         instance = self.active_dialog
@@ -230,3 +351,71 @@ async def end_active_dialog(self, reason: DialogReason):
 
             # Pop dialog off stack
             self._stack.pop(0)
+
+    async def emit_event(
+        self,
+        name: str,
+        value: object = None,
+        bubble: bool = True,
+        from_leaf: bool = False,
+    ) -> bool:
+        """
+        Searches for a dialog with a given ID.
+        Emits a named event for the current dialog, or someone who started it, to handle.
+        :param name: Name of the event to raise.
+        :param value: Value to send along with the event.
+        :param bubble: Flag to control whether the event should be bubbled to its parent if not handled locally.
+        Defaults to a value of `True`.
+        :param from_leaf: Whether the event is emitted from a leaf node.
+        :param cancellationToken: The cancellation token.
+        :return: True if the event was handled.
+        """
+        try:
+            # Initialize event
+            dialog_event = DialogEvent(bubble=bubble, name=name, value=value,)
+
+            dialog_context = self
+
+            # Find starting dialog
+            if from_leaf:
+                while True:
+                    child_dc = dialog_context.child
+
+                    if child_dc:
+                        dialog_context = child_dc
+                    else:
+                        break
+
+            # Dispatch to active dialog first
+            instance = dialog_context.active_dialog
+
+            if instance:
+                dialog = await dialog_context.find_dialog(instance.id)
+
+                if dialog:
+                    return await dialog.on_dialog_event(dialog_context, dialog_event)
+
+            return False
+        except Exception as err:
+            self.__set_exception_context_data(err)
+            raise
+
+    def __set_exception_context_data(self, exception: Exception):
+        if not hasattr(exception, "data"):
+            exception.data = {}
+
+        if not type(self).__name__ in exception.data:
+            stack = []
+            current_dc = self
+
+            while current_dc is not None:
+                stack = stack + [x.id for x in current_dc.stack]
+                current_dc = current_dc.parent
+
+            exception.data[type(self).__name__] = {
+                "active_dialog": None
+                if self.active_dialog is None
+                else self.active_dialog.id,
+                "parent": None if self.parent is None else self.parent.active_dialog.id,
+                "stack": self.stack,
+            }
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_event.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_event.py
new file mode 100644
index 000000000..64753e824
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_event.py
@@ -0,0 +1,9 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+
+class DialogEvent:
+    def __init__(self, bubble: bool = False, name: str = "", value: object = None):
+        self.bubble = bubble
+        self.name = name
+        self.value: object = value
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_events.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_events.py
new file mode 100644
index 000000000..d3d0cb4a1
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_events.py
@@ -0,0 +1,15 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from enum import Enum
+
+
+class DialogEvents(str, Enum):
+
+    begin_dialog = "beginDialog"
+    reprompt_dialog = "repromptDialog"
+    cancel_dialog = "cancelDialog"
+    activity_received = "activityReceived"
+    version_changed = "versionChanged"
+    error = "error"
+    custom = "custom"
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_extensions.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_extensions.py
new file mode 100644
index 000000000..9f414e9cd
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_extensions.py
@@ -0,0 +1,127 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botframework.connector.auth import (
+    ClaimsIdentity,
+    SkillValidation,
+    AuthenticationConstants,
+    GovernmentConstants,
+)
+from botbuilder.core import BotAdapter, StatePropertyAccessor, TurnContext
+from botbuilder.core.skills import SkillHandler, SkillConversationReference
+from botbuilder.dialogs import (
+    Dialog,
+    DialogEvents,
+    DialogSet,
+    DialogTurnStatus,
+)
+from botbuilder.schema import Activity, ActivityTypes, EndOfConversationCodes
+
+
+class DialogExtensions:
+    @staticmethod
+    async def run_dialog(
+        dialog: Dialog, turn_context: TurnContext, accessor: StatePropertyAccessor
+    ):
+        """
+        Creates a dialog stack and starts a dialog, pushing it onto the stack.
+        """
+
+        dialog_set = DialogSet(accessor)
+        dialog_set.add(dialog)
+
+        dialog_context = await dialog_set.create_context(turn_context)
+
+        # Handle EoC and Reprompt event from a parent bot (can be root bot to skill or skill to skill)
+        if DialogExtensions.__is_from_parent_to_skill(turn_context):
+            # Handle remote cancellation request from parent.
+            if turn_context.activity.type == ActivityTypes.end_of_conversation:
+                if not dialog_context.stack:
+                    # No dialogs to cancel, just return.
+                    return
+
+                remote_cancel_text = "Skill was canceled through an EndOfConversation activity from the parent."
+                await turn_context.send_trace_activity(
+                    f"Extension {Dialog.__name__}.run_dialog", label=remote_cancel_text,
+                )
+
+                # Send cancellation message to the dialog to ensure all the parents are canceled
+                # in the right order.
+                await dialog_context.cancel_all_dialogs()
+                return
+
+            # Handle a reprompt event sent from the parent.
+            if (
+                turn_context.activity.type == ActivityTypes.event
+                and turn_context.activity.name == DialogEvents.reprompt_dialog
+            ):
+                if not dialog_context.stack:
+                    # No dialogs to reprompt, just return.
+                    return
+
+                await dialog_context.reprompt_dialog()
+                return
+
+        # Continue or start the dialog.
+        result = await dialog_context.continue_dialog()
+        if result.status == DialogTurnStatus.Empty:
+            result = await dialog_context.begin_dialog(dialog.id)
+
+        # Skills should send EoC when the dialog completes.
+        if (
+            result.status == DialogTurnStatus.Complete
+            or result.status == DialogTurnStatus.Cancelled
+        ):
+            if DialogExtensions.__send_eoc_to_parent(turn_context):
+                end_message_text = (
+                    f"Dialog {dialog.id} has **completed**. Sending EndOfConversation."
+                )
+                await turn_context.send_trace_activity(
+                    f"Extension {Dialog.__name__}.run_dialog",
+                    label=end_message_text,
+                    value=result.result,
+                )
+
+                activity = Activity(
+                    type=ActivityTypes.end_of_conversation,
+                    value=result.result,
+                    locale=turn_context.activity.locale,
+                    code=EndOfConversationCodes.completed_successfully
+                    if result.status == DialogTurnStatus.Complete
+                    else EndOfConversationCodes.user_cancelled,
+                )
+                await turn_context.send_activity(activity)
+
+    @staticmethod
+    def __is_from_parent_to_skill(turn_context: TurnContext) -> bool:
+        if turn_context.turn_state.get(SkillHandler.SKILL_CONVERSATION_REFERENCE_KEY):
+            return False
+
+        claims_identity = turn_context.turn_state.get(BotAdapter.BOT_IDENTITY_KEY)
+        return isinstance(
+            claims_identity, ClaimsIdentity
+        ) and SkillValidation.is_skill_claim(claims_identity.claims)
+
+    @staticmethod
+    def __send_eoc_to_parent(turn_context: TurnContext) -> bool:
+        claims_identity = turn_context.turn_state.get(BotAdapter.BOT_IDENTITY_KEY)
+        if isinstance(
+            claims_identity, ClaimsIdentity
+        ) and SkillValidation.is_skill_claim(claims_identity.claims):
+            # EoC Activities returned by skills are bounced back to the bot by SkillHandler.
+            # In those cases we will have a SkillConversationReference instance in state.
+            skill_conversation_reference: SkillConversationReference = turn_context.turn_state.get(
+                SkillHandler.SKILL_CONVERSATION_REFERENCE_KEY
+            )
+            if skill_conversation_reference:
+                # If the skillConversationReference.OAuthScope is for one of the supported channels,
+                # we are at the root and we should not send an EoC.
+                return (
+                    skill_conversation_reference.oauth_scope
+                    != AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
+                    and skill_conversation_reference.oauth_scope
+                    != GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
+                )
+            return True
+
+        return False
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_instance.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_instance.py
index 3b5b4423f..0d4e3400b 100644
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_instance.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_instance.py
@@ -9,11 +9,28 @@ class DialogInstance:
     Tracking information for a dialog on the stack.
     """
 
-    def __init__(self):
-        self.id: str = None  # pylint: disable=invalid-name
-        self.state: Dict[str, object] = {}
+    def __init__(
+        self, id: str = None, state: Dict[str, object] = None
+    ):  # pylint: disable=invalid-name
+        """
+        Gets or sets the ID of the dialog and gets or sets the instance's persisted state.
+
+        :var self.id: The ID of the dialog
+        :vartype self.id: str
+        :var self.state: The instance's persisted state.
+        :vartype self.state: :class:`typing.Dict[str, object]`
+        """
+        self.id = id  # pylint: disable=invalid-name
+
+        self.state = state or {}
 
     def __str__(self):
+        """
+        Gets or sets a stack index.
+
+        :return: Returns stack index.
+        :rtype: str
+        """
         result = "\ndialog_instance_id: %s\n" % self.id
         if self.state is not None:
             for key, value in self.state.items():
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_manager.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_manager.py
new file mode 100644
index 000000000..c1d3088d1
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_manager.py
@@ -0,0 +1,384 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from datetime import datetime, timedelta
+from threading import Lock
+
+from botbuilder.core import (
+    BotAdapter,
+    BotStateSet,
+    ConversationState,
+    UserState,
+    TurnContext,
+)
+from botbuilder.core.skills import SkillConversationReference, SkillHandler
+from botbuilder.dialogs.memory import (
+    DialogStateManager,
+    DialogStateManagerConfiguration,
+)
+from botbuilder.schema import Activity, ActivityTypes, EndOfConversationCodes
+from botframework.connector.auth import (
+    AuthenticationConstants,
+    ClaimsIdentity,
+    GovernmentConstants,
+    SkillValidation,
+)
+
+from .dialog import Dialog
+from .dialog_context import DialogContext
+from .dialog_events import DialogEvents
+from .dialog_set import DialogSet
+from .dialog_state import DialogState
+from .dialog_manager_result import DialogManagerResult
+from .dialog_turn_status import DialogTurnStatus
+from .dialog_turn_result import DialogTurnResult
+
+
+class DialogManager:
+    """
+    Class which runs the dialog system.
+    """
+
+    def __init__(self, root_dialog: Dialog = None, dialog_state_property: str = None):
+        """
+        Initializes a instance of the  class.
+        :param root_dialog: Root dialog to use.
+        :param dialog_state_property: alternate name for the dialog_state property. (Default is "DialogState").
+        """
+        self.last_access = "_lastAccess"
+        self._root_dialog_id = ""
+        self._dialog_state_property = dialog_state_property or "DialogState"
+        self._lock = Lock()
+
+        # Gets or sets root dialog to use to start conversation.
+        self.root_dialog = root_dialog
+
+        # Gets or sets the ConversationState.
+        self.conversation_state: ConversationState = None
+
+        # Gets or sets the UserState.
+        self.user_state: UserState = None
+
+        # Gets InitialTurnState collection to copy into the TurnState on every turn.
+        self.initial_turn_state = {}
+
+        # Gets or sets global dialogs that you want to have be callable.
+        self.dialogs = DialogSet()
+
+        # Gets or sets the DialogStateManagerConfiguration.
+        self.state_configuration: DialogStateManagerConfiguration = None
+
+        # Gets or sets (optional) number of milliseconds to expire the bot's state after.
+        self.expire_after: int = None
+
+    async def on_turn(self, context: TurnContext) -> DialogManagerResult:
+        """
+        Runs dialog system in the context of an ITurnContext.
+        :param context: turn context.
+        :return:
+        """
+        # pylint: disable=too-many-statements
+        # Lazy initialize RootDialog so it can refer to assets like LG function templates
+        if not self._root_dialog_id:
+            with self._lock:
+                if not self._root_dialog_id:
+                    self._root_dialog_id = self.root_dialog.id
+                    # self.dialogs = self.root_dialog.telemetry_client
+                    self.dialogs.add(self.root_dialog)
+
+        bot_state_set = BotStateSet([])
+
+        # Preload TurnState with DM TurnState.
+        for key, val in self.initial_turn_state.items():
+            context.turn_state[key] = val
+
+        # register DialogManager with TurnState.
+        context.turn_state[DialogManager.__name__] = self
+        conversation_state_name = ConversationState.__name__
+        if self.conversation_state is None:
+            if conversation_state_name not in context.turn_state:
+                raise Exception(
+                    f"Unable to get an instance of {conversation_state_name} from turn_context."
+                )
+            self.conversation_state: ConversationState = context.turn_state[
+                conversation_state_name
+            ]
+        else:
+            context.turn_state[conversation_state_name] = self.conversation_state
+
+        bot_state_set.add(self.conversation_state)
+
+        user_state_name = UserState.__name__
+        if self.user_state is None:
+            self.user_state = context.turn_state.get(user_state_name, None)
+        else:
+            context.turn_state[user_state_name] = self.user_state
+
+        if self.user_state is not None:
+            self.user_state: UserState = self.user_state
+            bot_state_set.add(self.user_state)
+
+        # create property accessors
+        # DateTime(last_access)
+        last_access_property = self.conversation_state.create_property(self.last_access)
+        last_access: datetime = await last_access_property.get(context, datetime.now)
+
+        # Check for expired conversation
+        if self.expire_after is not None and (
+            datetime.now() - last_access
+        ) >= timedelta(milliseconds=float(self.expire_after)):
+            # Clear conversation state
+            await self.conversation_state.clear_state(context)
+
+        last_access = datetime.now()
+        await last_access_property.set(context, last_access)
+
+        # get dialog stack
+        dialogs_property = self.conversation_state.create_property(
+            self._dialog_state_property
+        )
+        dialog_state: DialogState = await dialogs_property.get(context, DialogState)
+
+        # Create DialogContext
+        dialog_context = DialogContext(self.dialogs, context, dialog_state)
+
+        # promote initial TurnState into dialog_context.services for contextual services
+        for key, service in dialog_context.services.items():
+            dialog_context.services[key] = service
+
+        # map TurnState into root dialog context.services
+        for key, service in context.turn_state.items():
+            dialog_context.services[key] = service
+
+        # get the DialogStateManager configuration
+        dialog_state_manager = DialogStateManager(
+            dialog_context, self.state_configuration
+        )
+        await dialog_state_manager.load_all_scopes()
+        dialog_context.context.turn_state[
+            dialog_state_manager.__class__.__name__
+        ] = dialog_state_manager
+
+        turn_result: DialogTurnResult = None
+
+        # Loop as long as we are getting valid OnError handled we should continue executing the actions for the turn.
+
+        # NOTE: We loop around this block because each pass through we either complete the turn and break out of the
+        # loop or we have had an exception AND there was an OnError action which captured the error.  We need to
+        # continue the turn based on the actions the OnError handler introduced.
+        end_of_turn = False
+        while not end_of_turn:
+            try:
+                claims_identity: ClaimsIdentity = context.turn_state.get(
+                    BotAdapter.BOT_IDENTITY_KEY, None
+                )
+                if isinstance(
+                    claims_identity, ClaimsIdentity
+                ) and SkillValidation.is_skill_claim(claims_identity.claims):
+                    # The bot is running as a skill.
+                    turn_result = await self.handle_skill_on_turn(dialog_context)
+                else:
+                    # The bot is running as root bot.
+                    turn_result = await self.handle_bot_on_turn(dialog_context)
+
+                # turn successfully completed, break the loop
+                end_of_turn = True
+            except Exception as err:
+                # fire error event, bubbling from the leaf.
+                handled = await dialog_context.emit_event(
+                    DialogEvents.error, err, bubble=True, from_leaf=True
+                )
+
+                if not handled:
+                    # error was NOT handled, throw the exception and end the turn. (This will trigger the
+                    # Adapter.OnError handler and end the entire dialog stack)
+                    raise
+
+        # save all state scopes to their respective botState locations.
+        await dialog_state_manager.save_all_changes()
+
+        # save BotState changes
+        await bot_state_set.save_all_changes(dialog_context.context, False)
+
+        return DialogManagerResult(turn_result=turn_result)
+
+    @staticmethod
+    async def send_state_snapshot_trace(
+        dialog_context: DialogContext, trace_label: str
+    ):
+        """
+        Helper to send a trace activity with a memory snapshot of the active dialog DC.
+        :param dialog_context:
+        :param trace_label:
+        :return:
+        """
+        # send trace of memory
+        snapshot = DialogManager.get_active_dialog_context(
+            dialog_context
+        ).state.get_memory_snapshot()
+        trace_activity = Activity.create_trace_activity(
+            "BotState",
+            "https://www.botframework.com/schemas/botState",
+            snapshot,
+            trace_label,
+        )
+        await dialog_context.context.send_activity(trace_activity)
+
+    @staticmethod
+    def is_from_parent_to_skill(turn_context: TurnContext) -> bool:
+        if turn_context.turn_state.get(
+            SkillHandler.SKILL_CONVERSATION_REFERENCE_KEY, None
+        ):
+            return False
+
+        claims_identity: ClaimsIdentity = turn_context.turn_state.get(
+            BotAdapter.BOT_IDENTITY_KEY, None
+        )
+        return isinstance(
+            claims_identity, ClaimsIdentity
+        ) and SkillValidation.is_skill_claim(claims_identity.claims)
+
+    # Recursively walk up the DC stack to find the active DC.
+    @staticmethod
+    def get_active_dialog_context(dialog_context: DialogContext) -> DialogContext:
+        """
+        Recursively walk up the DC stack to find the active DC.
+        :param dialog_context:
+        :return:
+        """
+        child = dialog_context.child
+        if not child:
+            return dialog_context
+
+        return DialogManager.get_active_dialog_context(child)
+
+    @staticmethod
+    def should_send_end_of_conversation_to_parent(
+        context: TurnContext, turn_result: DialogTurnResult
+    ) -> bool:
+        """
+        Helper to determine if we should send an EndOfConversation to the parent or not.
+        :param context:
+        :param turn_result:
+        :return:
+        """
+        if not (
+            turn_result.status == DialogTurnStatus.Complete
+            or turn_result.status == DialogTurnStatus.Cancelled
+        ):
+            # The dialog is still going, don't return EoC.
+            return False
+        claims_identity: ClaimsIdentity = context.turn_state.get(
+            BotAdapter.BOT_IDENTITY_KEY, None
+        )
+        if isinstance(
+            claims_identity, ClaimsIdentity
+        ) and SkillValidation.is_skill_claim(claims_identity.claims):
+            # EoC Activities returned by skills are bounced back to the bot by SkillHandler.
+            # In those cases we will have a SkillConversationReference instance in state.
+            skill_conversation_reference: SkillConversationReference = context.turn_state.get(
+                SkillHandler.SKILL_CONVERSATION_REFERENCE_KEY
+            )
+            if skill_conversation_reference:
+                # If the skill_conversation_reference.OAuthScope is for one of the supported channels, we are at the
+                # root and we should not send an EoC.
+                return skill_conversation_reference.oauth_scope not in (
+                    AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE,
+                    GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE,
+                )
+
+            return True
+
+        return False
+
+    async def handle_skill_on_turn(
+        self, dialog_context: DialogContext
+    ) -> DialogTurnResult:
+        # the bot is running as a skill.
+        turn_context = dialog_context.context
+
+        # Process remote cancellation
+        if (
+            turn_context.activity.type == ActivityTypes.end_of_conversation
+            and dialog_context.active_dialog is not None
+            and self.is_from_parent_to_skill(turn_context)
+        ):
+            # Handle remote cancellation request from parent.
+            active_dialog_context = self.get_active_dialog_context(dialog_context)
+
+            remote_cancel_text = "Skill was canceled through an EndOfConversation activity from the parent."
+            await turn_context.send_trace_activity(
+                f"{self.__class__.__name__}.on_turn_async()",
+                label=f"{remote_cancel_text}",
+            )
+
+            # Send cancellation message to the top dialog in the stack to ensure all the parents are canceled in the
+            # right order.
+            return await active_dialog_context.cancel_all_dialogs(True)
+
+        # Handle reprompt
+        # Process a reprompt event sent from the parent.
+        if (
+            turn_context.activity.type == ActivityTypes.event
+            and turn_context.activity.name == DialogEvents.reprompt_dialog
+        ):
+            if not dialog_context.active_dialog:
+                return DialogTurnResult(DialogTurnStatus.Empty)
+
+            await dialog_context.reprompt_dialog()
+            return DialogTurnResult(DialogTurnStatus.Waiting)
+
+        # Continue execution
+        # - This will apply any queued up interruptions and execute the current/next step(s).
+        turn_result = await dialog_context.continue_dialog()
+        if turn_result.status == DialogTurnStatus.Empty:
+            # restart root dialog
+            start_message_text = f"Starting {self._root_dialog_id}."
+            await turn_context.send_trace_activity(
+                f"{self.__class__.__name__}.handle_skill_on_turn_async()",
+                label=f"{start_message_text}",
+            )
+            turn_result = await dialog_context.begin_dialog(self._root_dialog_id)
+
+        await DialogManager.send_state_snapshot_trace(dialog_context, "Skill State")
+
+        if self.should_send_end_of_conversation_to_parent(turn_context, turn_result):
+            end_message_text = f"Dialog {self._root_dialog_id} has **completed**. Sending EndOfConversation."
+            await turn_context.send_trace_activity(
+                f"{self.__class__.__name__}.handle_skill_on_turn_async()",
+                label=f"{end_message_text}",
+                value=turn_result.result,
+            )
+
+            # Send End of conversation at the end.
+            activity = Activity(
+                type=ActivityTypes.end_of_conversation,
+                value=turn_result.result,
+                locale=turn_context.activity.locale,
+                code=EndOfConversationCodes.completed_successfully
+                if turn_result.status == DialogTurnStatus.Complete
+                else EndOfConversationCodes.user_cancelled,
+            )
+            await turn_context.send_activity(activity)
+
+        return turn_result
+
+    async def handle_bot_on_turn(
+        self, dialog_context: DialogContext
+    ) -> DialogTurnResult:
+        # the bot is running as a root bot.
+        if dialog_context.active_dialog is None:
+            # start root dialog
+            turn_result = await dialog_context.begin_dialog(self._root_dialog_id)
+        else:
+            # Continue execution
+            # - This will apply any queued up interruptions and execute the current/next step(s).
+            turn_result = await dialog_context.continue_dialog()
+
+            if turn_result.status == DialogTurnStatus.Empty:
+                # restart root dialog
+                turn_result = await dialog_context.begin_dialog(self._root_dialog_id)
+
+        await self.send_state_snapshot_trace(dialog_context, "Bot State")
+
+        return turn_result
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_manager_result.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_manager_result.py
new file mode 100644
index 000000000..c184f0df2
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_manager_result.py
@@ -0,0 +1,21 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from typing import List
+
+from botbuilder.schema import Activity
+
+from .dialog_turn_result import DialogTurnResult
+from .persisted_state import PersistedState
+
+
+class DialogManagerResult:
+    def __init__(
+        self,
+        turn_result: DialogTurnResult = None,
+        activities: List[Activity] = None,
+        persisted_state: PersistedState = None,
+    ):
+        self.turn_result = turn_result
+        self.activities = activities
+        self.persisted_state = persisted_state
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_reason.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_reason.py
index c20f2e3b2..4383ab0d4 100644
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_reason.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_reason.py
@@ -4,15 +4,31 @@
 
 
 class DialogReason(Enum):
-    # A dialog is being started through a call to `DialogContext.begin()`.
+    """
+    Indicates in which a dialog-related method is being called.
+
+    :var BeginCalled: A dialog is being started through a call to :meth:`DialogContext.begin()`.
+    :vartype BeginCalled: int
+    :var ContinueCalled: A dialog is being continued through a call to :meth:`DialogContext.continue_dialog()`.
+    :vartype ContinueCalled: int
+    :var EndCalled: A dialog ended normally through a call to :meth:`DialogContext.end_dialog()
+    :vartype EndCalled: int
+    :var ReplaceCalled: A dialog is ending and replaced through a call to :meth:``DialogContext.replace_dialog()`.
+    :vartype ReplacedCalled: int
+    :var CancelCalled: A dialog was cancelled as part of a call to :meth:`DialogContext.cancel_all_dialogs()`.
+    :vartype CancelCalled: int
+    :var NextCalled: A preceding step was skipped through a call to :meth:`WaterfallStepContext.next()`.
+    :vartype NextCalled: int
+    """
+
     BeginCalled = 1
-    # A dialog is being continued through a call to `DialogContext.continue_dialog()`.
+
     ContinueCalled = 2
-    # A dialog ended normally through a call to `DialogContext.end_dialog()`.
+
     EndCalled = 3
-    # A dialog is ending because it's being replaced through a call to `DialogContext.replace_dialog()`.
+
     ReplaceCalled = 4
-    # A dialog was cancelled as part of a call to `DialogContext.cancel_all_dialogs()`.
+
     CancelCalled = 5
-    # A step was advanced through a call to `WaterfallStepContext.next()`.
+
     NextCalled = 6
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_set.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_set.py
index d6870128a..5820a3422 100644
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_set.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_set.py
@@ -1,16 +1,17 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License.
 import inspect
+from hashlib import sha256
 from typing import Dict
 
 from botbuilder.core import TurnContext, BotAssert, StatePropertyAccessor
 from .dialog import Dialog
 from .dialog_state import DialogState
-from .dialog_context import DialogContext
 
 
 class DialogSet:
     def __init__(self, dialog_state: StatePropertyAccessor = None):
+        # pylint: disable=import-outside-toplevel
         if dialog_state is None:
             frame = inspect.currentframe().f_back
             try:
@@ -20,10 +21,13 @@ def __init__(self, dialog_state: StatePropertyAccessor = None):
                 except KeyError:
                     raise TypeError("DialogSet(): dialog_state cannot be None.")
                 # Only ComponentDialog can initialize with None dialog_state
-                # pylint: disable=import-outside-toplevel
                 from .component_dialog import ComponentDialog
+                from .dialog_manager import DialogManager
+                from .dialog_container import DialogContainer
 
-                if not isinstance(self_obj, ComponentDialog):
+                if not isinstance(
+                    self_obj, (ComponentDialog, DialogContainer, DialogManager)
+                ):
                     raise TypeError("DialogSet(): dialog_state cannot be None.")
             finally:
                 # make sure to clean up the frame at the end to avoid ref cycles
@@ -32,7 +36,24 @@ def __init__(self, dialog_state: StatePropertyAccessor = None):
         self._dialog_state = dialog_state
         # self.__telemetry_client = NullBotTelemetryClient.Instance;
 
-        self._dialogs: Dict[str, object] = {}
+        self._dialogs: Dict[str, Dialog] = {}
+        self._version: str = None
+
+    def get_version(self) -> str:
+        """
+        Gets a unique string which represents the combined versions of all dialogs in this this dialogset.
+        Version will change when any of the child dialogs version changes.
+        """
+        if not self._version:
+            version = ""
+            for _, dialog in self._dialogs.items():
+                aux_version = dialog.get_version()
+                if aux_version:
+                    version += aux_version
+
+            self._version = sha256(version)
+
+        return self._version
 
     def add(self, dialog: Dialog):
         """
@@ -55,7 +76,11 @@ def add(self, dialog: Dialog):
 
         return self
 
-    async def create_context(self, turn_context: TurnContext) -> DialogContext:
+    async def create_context(self, turn_context: TurnContext) -> "DialogContext":
+        # This import prevents circular dependency issues
+        # pylint: disable=import-outside-toplevel
+        from .dialog_context import DialogContext
+
         # pylint: disable=unnecessary-lambda
         BotAssert.context_not_none(turn_context)
 
@@ -64,7 +89,9 @@ async def create_context(self, turn_context: TurnContext) -> DialogContext:
                 "DialogSet.CreateContextAsync(): DialogSet created with a null IStatePropertyAccessor."
             )
 
-        state = await self._dialog_state.get(turn_context, lambda: DialogState())
+        state: DialogState = await self._dialog_state.get(
+            turn_context, lambda: DialogState()
+        )
 
         return DialogContext(self, turn_context, state)
 
@@ -82,6 +109,20 @@ async def find(self, dialog_id: str) -> Dialog:
 
         return None
 
+    def find_dialog(self, dialog_id: str) -> Dialog:
+        """
+        Finds a dialog that was previously added to the set using add(dialog)
+        :param dialog_id: ID of the dialog/prompt to look up.
+        :return: The dialog if found, otherwise null.
+        """
+        if not dialog_id:
+            raise TypeError("DialogContext.find(): dialog_id cannot be None.")
+
+        if dialog_id in self._dialogs:
+            return self._dialogs[dialog_id]
+
+        return None
+
     def __str__(self):
         if self._dialogs:
             return "dialog set empty!"
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_state.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_state.py
index 278e6b14d..8201225e5 100644
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_state.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_state.py
@@ -6,7 +6,17 @@
 
 
 class DialogState:
+    """
+    Contains state information for the dialog stack.
+    """
+
     def __init__(self, stack: List[DialogInstance] = None):
+        """
+        Initializes a new instance of the :class:`DialogState` class.
+
+        :param stack: The state information to initialize the stack with.
+        :type stack: :class:`typing.List`
+        """
         if stack is None:
             self._dialog_stack = []
         else:
@@ -14,6 +24,12 @@ def __init__(self, stack: List[DialogInstance] = None):
 
     @property
     def dialog_stack(self):
+        """
+        Initializes a new instance of the :class:`DialogState` class.
+
+        :return: The state information to initialize the stack with.
+        :rtype: :class:`typing.List`
+        """
         return self._dialog_stack
 
     def __str__(self):
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_turn_result.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_turn_result.py
index e36504f8b..466cfac0f 100644
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_turn_result.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_turn_result.py
@@ -5,14 +5,36 @@
 
 
 class DialogTurnResult:
+    """
+    Result returned to the caller of one of the various stack manipulation methods.
+    """
+
     def __init__(self, status: DialogTurnStatus, result: object = None):
+        """
+        :param status: The current status of the stack.
+        :type status: :class:`botbuilder.dialogs.DialogTurnStatus`
+        :param result: The result returned by a dialog that was just ended.
+        :type result: object
+        """
         self._status = status
         self._result = result
 
     @property
     def status(self):
+        """
+        Gets or sets the current status of the stack.
+
+        :return self._status: The status of the stack.
+        :rtype self._status: :class:`DialogTurnStatus`
+         """
         return self._status
 
     @property
     def result(self):
+        """
+        Final result returned by a dialog that just completed.
+
+        :return self._result: Final result returned by a dialog that just completed.
+        :rtype self._result: object
+        """
         return self._result
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_turn_status.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_turn_status.py
index e734405a8..6d8b61e51 100644
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_turn_status.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_turn_status.py
@@ -4,14 +4,23 @@
 
 
 class DialogTurnStatus(Enum):
-    # Indicates that there is currently nothing on the dialog stack.
+    """
+    Indicates in which a dialog-related method is being called.
+
+    :var Empty: Indicates that there is currently nothing on the dialog stack.
+    :vartype Empty: int
+    :var Waiting: Indicates that the dialog on top is waiting for a response from the user.
+    :vartype Waiting: int
+    :var Complete: Indicates that the dialog completed successfully, the result is available, and the stack is empty.
+    :vartype Complete: int
+    :var Cancelled: Indicates that the dialog was cancelled and the stack is empty.
+    :vartype Cancelled: int
+    """
+
     Empty = 1
 
-    # Indicates that the dialog on top is waiting for a response from the user.
     Waiting = 2
 
-    # Indicates that the dialog completed successfully, the result is available, and the stack is empty.
     Complete = 3
 
-    # Indicates that the dialog was cancelled and the stack is empty.
     Cancelled = 4
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialogs_component_registration.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialogs_component_registration.py
new file mode 100644
index 000000000..acbddd1e0
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialogs_component_registration.py
@@ -0,0 +1,53 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+from typing import Iterable
+
+from botbuilder.core import ComponentRegistration
+
+from botbuilder.dialogs.memory import (
+    ComponentMemoryScopesBase,
+    ComponentPathResolversBase,
+    PathResolverBase,
+)
+from botbuilder.dialogs.memory.scopes import (
+    TurnMemoryScope,
+    SettingsMemoryScope,
+    DialogMemoryScope,
+    DialogContextMemoryScope,
+    DialogClassMemoryScope,
+    ClassMemoryScope,
+    MemoryScope,
+    ThisMemoryScope,
+    ConversationMemoryScope,
+    UserMemoryScope,
+)
+
+from botbuilder.dialogs.memory.path_resolvers import (
+    AtAtPathResolver,
+    AtPathResolver,
+    DollarPathResolver,
+    HashPathResolver,
+    PercentPathResolver,
+)
+
+
+class DialogsComponentRegistration(
+    ComponentRegistration, ComponentMemoryScopesBase, ComponentPathResolversBase
+):
+    def get_memory_scopes(self) -> Iterable[MemoryScope]:
+        yield TurnMemoryScope()
+        yield SettingsMemoryScope()
+        yield DialogMemoryScope()
+        yield DialogContextMemoryScope()
+        yield DialogClassMemoryScope()
+        yield ClassMemoryScope()
+        yield ThisMemoryScope()
+        yield ConversationMemoryScope()
+        yield UserMemoryScope()
+
+    def get_path_resolvers(self) -> Iterable[PathResolverBase]:
+        yield AtAtPathResolver()
+        yield AtPathResolver()
+        yield DollarPathResolver()
+        yield HashPathResolver()
+        yield PercentPathResolver()
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/__init__.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/__init__.py
new file mode 100644
index 000000000..a43b4cfb8
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/__init__.py
@@ -0,0 +1,24 @@
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+# --------------------------------------------------------------------------
+
+from .dialog_path import DialogPath
+from .dialog_state_manager import DialogStateManager
+from .dialog_state_manager_configuration import DialogStateManagerConfiguration
+from .component_memory_scopes_base import ComponentMemoryScopesBase
+from .component_path_resolvers_base import ComponentPathResolversBase
+from .path_resolver_base import PathResolverBase
+from . import scope_path
+
+__all__ = [
+    "DialogPath",
+    "DialogStateManager",
+    "DialogStateManagerConfiguration",
+    "ComponentMemoryScopesBase",
+    "ComponentPathResolversBase",
+    "PathResolverBase",
+    "scope_path",
+]
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/component_memory_scopes_base.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/component_memory_scopes_base.py
new file mode 100644
index 000000000..428e631ff
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/component_memory_scopes_base.py
@@ -0,0 +1,14 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+
+from abc import ABC, abstractmethod
+from typing import Iterable
+
+from botbuilder.dialogs.memory.scopes import MemoryScope
+
+
+class ComponentMemoryScopesBase(ABC):
+    @abstractmethod
+    def get_memory_scopes(self) -> Iterable[MemoryScope]:
+        raise NotImplementedError()
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/component_path_resolvers_base.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/component_path_resolvers_base.py
new file mode 100644
index 000000000..4c3c0ec73
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/component_path_resolvers_base.py
@@ -0,0 +1,14 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+
+from abc import ABC, abstractmethod
+from typing import Iterable
+
+from .path_resolver_base import PathResolverBase
+
+
+class ComponentPathResolversBase(ABC):
+    @abstractmethod
+    def get_path_resolvers(self) -> Iterable[PathResolverBase]:
+        raise NotImplementedError()
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/dialog_path.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/dialog_path.py
new file mode 100644
index 000000000..be11cb2fb
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/dialog_path.py
@@ -0,0 +1,32 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+
+class DialogPath:
+    # Counter of emitted events.
+    EVENT_COUNTER = "dialog.eventCounter"
+
+    # Currently expected properties.
+    EXPECTED_PROPERTIES = "dialog.expectedProperties"
+
+    # Default operation to use for entities where there is no identified operation entity.
+    DEFAULT_OPERATION = "dialog.defaultOperation"
+
+    # Last surfaced entity ambiguity event.
+    LAST_EVENT = "dialog.lastEvent"
+
+    # Currently required properties.
+    REQUIRED_PROPERTIES = "dialog.requiredProperties"
+
+    # Number of retries for the current Ask.
+    RETRIES = "dialog.retries"
+
+    # Last intent.
+    LAST_INTENT = "dialog.lastIntent"
+
+    # Last trigger event: defined in FormEvent, ask, clarifyEntity etc..
+    LAST_TRIGGER_EVENT = "dialog.lastTriggerEvent"
+
+    @staticmethod
+    def get_property_name(prop: str) -> str:
+        return prop.replace("dialog.", "")
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/dialog_state_manager.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/dialog_state_manager.py
new file mode 100644
index 000000000..0610f3ac5
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/dialog_state_manager.py
@@ -0,0 +1,660 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+import builtins
+
+from inspect import isawaitable
+from traceback import print_tb
+from typing import (
+    Callable,
+    Dict,
+    Iterable,
+    Iterator,
+    List,
+    Tuple,
+    Type,
+    TypeVar,
+)
+
+from botbuilder.core import ComponentRegistration
+
+from botbuilder.dialogs.memory.scopes import MemoryScope
+
+from .component_memory_scopes_base import ComponentMemoryScopesBase
+from .component_path_resolvers_base import ComponentPathResolversBase
+from .dialog_path import DialogPath
+from .dialog_state_manager_configuration import DialogStateManagerConfiguration
+
+# Declare type variable
+T = TypeVar("T")  # pylint: disable=invalid-name
+
+BUILTIN_TYPES = list(filter(lambda x: not x.startswith("_"), dir(builtins)))
+
+
+# 
+# The DialogStateManager manages memory scopes and pathresolvers
+# MemoryScopes are named root level objects, which can exist either in the dialogcontext or off of turn state
+# PathResolvers allow for shortcut behavior for mapping things like $foo -> dialog.foo.
+# 
+class DialogStateManager:
+
+    SEPARATORS = [",", "["]
+
+    def __init__(
+        self,
+        dialog_context: "DialogContext",
+        configuration: DialogStateManagerConfiguration = None,
+    ):
+        """
+        Initializes a new instance of the DialogStateManager class.
+        :param dialog_context: The dialog context for the current turn of the conversation.
+        :param configuration: Configuration for the dialog state manager. Default is None.
+        """
+        # pylint: disable=import-outside-toplevel
+        # These modules are imported at static level to avoid circular dependency problems
+        from botbuilder.dialogs import (
+            DialogsComponentRegistration,
+            ObjectPath,
+        )
+
+        self._object_path_cls = ObjectPath
+        self._dialog_component_registration_cls = DialogsComponentRegistration
+
+        # Information for tracking when path was last modified.
+        self.path_tracker = "dialog._tracker.paths"
+
+        self._dialog_context = dialog_context
+        self._version: int = 0
+
+        ComponentRegistration.add(self._dialog_component_registration_cls())
+
+        if not dialog_context:
+            raise TypeError(f"Expecting: DialogContext, but received None")
+
+        self._configuration = configuration or dialog_context.context.turn_state.get(
+            DialogStateManagerConfiguration.__name__, None
+        )
+        if not self._configuration:
+            self._configuration = DialogStateManagerConfiguration()
+
+            # get all of the component memory scopes
+            memory_component: ComponentMemoryScopesBase
+            for memory_component in filter(
+                lambda comp: isinstance(comp, ComponentMemoryScopesBase),
+                ComponentRegistration.get_components(),
+            ):
+                for memory_scope in memory_component.get_memory_scopes():
+                    self._configuration.memory_scopes.append(memory_scope)
+
+            # get all of the component path resolvers
+            path_component: ComponentPathResolversBase
+            for path_component in filter(
+                lambda comp: isinstance(comp, ComponentPathResolversBase),
+                ComponentRegistration.get_components(),
+            ):
+                for path_resolver in path_component.get_path_resolvers():
+                    self._configuration.path_resolvers.append(path_resolver)
+
+        # cache for any other new dialog_state_manager instances in this turn.
+        dialog_context.context.turn_state[
+            self._configuration.__class__.__name__
+        ] = self._configuration
+
+    def __len__(self) -> int:
+        """
+        Gets the number of memory scopes in the dialog state manager.
+        :return: Number of memory scopes in the configuration.
+        """
+        return len(self._configuration.memory_scopes)
+
+    @property
+    def configuration(self) -> DialogStateManagerConfiguration:
+        """
+        Gets or sets the configured path resolvers and memory scopes for the dialog state manager.
+        :return: The configuration object.
+        """
+        return self._configuration
+
+    @property
+    def keys(self) -> Iterable[str]:
+        """
+        Gets a Iterable containing the keys of the memory scopes
+        :return: Keys of the memory scopes.
+        """
+        return [memory_scope.name for memory_scope in self.configuration.memory_scopes]
+
+    @property
+    def values(self) -> Iterable[object]:
+        """
+        Gets a Iterable containing the values of the memory scopes.
+        :return: Values of the memory scopes.
+        """
+        return [
+            memory_scope.get_memory(self._dialog_context)
+            for memory_scope in self.configuration.memory_scopes
+        ]
+
+    # 
+    # Gets a value indicating whether the dialog state manager is read-only.
+    # 
+    # true.
+    @property
+    def is_read_only(self) -> bool:
+        """
+        Gets a value indicating whether the dialog state manager is read-only.
+        :return: True.
+        """
+        return True
+
+    # 
+    # Gets or sets the elements with the specified key.
+    # 
+    # Key to get or set the element.
+    # The element with the specified key.
+    def __getitem__(self, key):
+        """
+        :param key:
+        :return The value stored at key's position:
+        """
+        return self.get_value(object, key, default_value=lambda: None)
+
+    def __setitem__(self, key, value):
+        if self._index_of_any(key, self.SEPARATORS) == -1:
+            # Root is handled by SetMemory rather than SetValue
+            scope = self.get_memory_scope(key)
+            if not scope:
+                raise IndexError(self._get_bad_scope_message(key))
+            # TODO: C# transforms value to JToken
+            scope.set_memory(self._dialog_context, value)
+        else:
+            self.set_value(key, value)
+
+    def _get_bad_scope_message(self, path: str) -> str:
+        return (
+            f"'{path}' does not match memory scopes:["
+            f"{', '.join((memory_scope.name for memory_scope in self.configuration.memory_scopes))}]"
+        )
+
+    @staticmethod
+    def _index_of_any(string: str, elements_to_search_for) -> int:
+        for element in elements_to_search_for:
+            index = string.find(element)
+            if index != -1:
+                return index
+
+        return -1
+
+    def get_memory_scope(self, name: str) -> MemoryScope:
+        """
+        Get MemoryScope by name.
+        :param name:
+        :return: A memory scope.
+        """
+        if not name:
+            raise TypeError(f"Expecting: {str.__name__}, but received None")
+
+        return next(
+            (
+                memory_scope
+                for memory_scope in self.configuration.memory_scopes
+                if memory_scope.name.lower() == name.lower()
+            ),
+            None,
+        )
+
+    def version(self) -> str:
+        """
+        Version help caller to identify the updates and decide cache or not.
+        :return: Current version.
+        """
+        return str(self._version)
+
+    def resolve_memory_scope(self, path: str) -> Tuple[MemoryScope, str]:
+        """
+        Will find the MemoryScope for and return the remaining path.
+        :param path:
+        :return: The memory scope and remaining subpath in scope.
+        """
+        scope = path
+        sep_index = -1
+        dot = path.find(".")
+        open_square_bracket = path.find("[")
+
+        if dot > 0 and open_square_bracket > 0:
+            sep_index = min(dot, open_square_bracket)
+
+        elif dot > 0:
+            sep_index = dot
+
+        elif open_square_bracket > 0:
+            sep_index = open_square_bracket
+
+        if sep_index > 0:
+            scope = path[0:sep_index]
+            memory_scope = self.get_memory_scope(scope)
+            if memory_scope:
+                remaining_path = path[sep_index + 1 :]
+                return memory_scope, remaining_path
+
+        memory_scope = self.get_memory_scope(scope)
+        if not scope:
+            raise IndexError(self._get_bad_scope_message(scope))
+        return memory_scope, ""
+
+    def transform_path(self, path: str) -> str:
+        """
+        Transform the path using the registered PathTransformers.
+        :param path: Path to transform.
+        :return: The transformed path.
+        """
+        for path_resolver in self.configuration.path_resolvers:
+            path = path_resolver.transform_path(path)
+
+        return path
+
+    @staticmethod
+    def _is_primitive(type_to_check: Type) -> bool:
+        return type_to_check.__name__ in BUILTIN_TYPES
+
+    def try_get_value(
+        self, path: str, class_type: Type = object
+    ) -> Tuple[bool, object]:
+        """
+        Get the value from memory using path expression (NOTE: This always returns clone of value).
+        :param class_type: The value type to return.
+        :param path: Path expression to use.
+        :return: True if found, false if not and the value.
+        """
+        if not path:
+            raise TypeError(f"Expecting: {str.__name__}, but received None")
+        return_value = (
+            class_type() if DialogStateManager._is_primitive(class_type) else None
+        )
+        path = self.transform_path(path)
+
+        try:
+            memory_scope, remaining_path = self.resolve_memory_scope(path)
+        except Exception as error:
+            print_tb(error.__traceback__)
+            return False, return_value
+
+        if not memory_scope:
+            return False, return_value
+
+        if not remaining_path:
+            memory = memory_scope.get_memory(self._dialog_context)
+            if not memory:
+                return False, return_value
+
+            return True, memory
+
+        # TODO: HACK to support .First() retrieval on turn.recognized.entities.foo, replace with Expressions once
+        #  expressions ship
+        first = ".FIRST()"
+        i_first = path.upper().rindex(first)
+        if i_first >= 0:
+            remaining_path = path[i_first + len(first) :]
+            path = path[0:i_first]
+            success, first_value = self._try_get_first_nested_value(path, self)
+            if success:
+                if not remaining_path:
+                    return True, first_value
+
+                path_value = self._object_path_cls.try_get_path_value(
+                    first_value, remaining_path
+                )
+                return bool(path_value), path_value
+
+            return False, return_value
+
+        path_value = self._object_path_cls.try_get_path_value(self, path)
+        return bool(path_value), path_value
+
+    def get_value(
+        self,
+        class_type: Type,
+        path_expression: str,
+        default_value: Callable[[], T] = None,
+    ) -> T:
+        """
+        Get the value from memory using path expression (NOTE: This always returns clone of value).
+        :param class_type: The value type to return.
+        :param path_expression: Path expression to use.
+        :param default_value: Function to give default value if there is none (OPTIONAL).
+        :return: Result or null if the path is not valid.
+        """
+        if not path_expression:
+            raise TypeError(f"Expecting: {str.__name__}, but received None")
+
+        success, value = self.try_get_value(path_expression, class_type)
+        if success:
+            return value
+
+        return default_value() if default_value else None
+
+    def get_int_value(self, path_expression: str, default_value: int = 0) -> int:
+        """
+        Get an int value from memory using a path expression.
+        :param path_expression: Path expression to use.
+        :param default_value: Default value if there is none (OPTIONAL).
+        :return:
+        """
+        if not path_expression:
+            raise TypeError(f"Expecting: {str.__name__}, but received None")
+        success, value = self.try_get_value(path_expression, int)
+        if success:
+            return value
+
+        return default_value
+
+    def get_bool_value(self, path_expression: str, default_value: bool = False) -> bool:
+        """
+        Get a bool value from memory using a path expression.
+        :param path_expression: Path expression to use.
+        :param default_value: Default value if there is none (OPTIONAL).
+        :return:
+        """
+        if not path_expression:
+            raise TypeError(f"Expecting: {str.__name__}, but received None")
+        success, value = self.try_get_value(path_expression, bool)
+        if success:
+            return value
+
+        return default_value
+
+    def get_string_value(self, path_expression: str, default_value: str = "") -> str:
+        """
+        Get a string value from memory using a path expression.
+        :param path_expression: Path expression to use.
+        :param default_value: Default value if there is none (OPTIONAL).
+        :return:
+        """
+        if not path_expression:
+            raise TypeError(f"Expecting: {str.__name__}, but received None")
+        success, value = self.try_get_value(path_expression, str)
+        if success:
+            return value
+
+        return default_value
+
+    def set_value(self, path: str, value: object):
+        """
+        Set memory to value.
+        :param path: Path to memory.
+        :param value: Object to set.
+        :return:
+        """
+        if isawaitable(value):
+            raise Exception(f"{path} = You can't pass an awaitable to set_value")
+
+        if not path:
+            raise TypeError(f"Expecting: {str.__name__}, but received None")
+
+        path = self.transform_path(path)
+        if self._track_change(path, value):
+            self._object_path_cls.set_path_value(self, path, value)
+
+        # Every set will increase version
+        self._version += 1
+
+    def remove_value(self, path: str):
+        """
+        Set memory to value.
+        :param path: Path to memory.
+        :param value: Object to set.
+        :return:
+        """
+        if not path:
+            raise TypeError(f"Expecting: {str.__name__}, but received None")
+
+        path = self.transform_path(path)
+        if self._track_change(path, None):
+            self._object_path_cls.remove_path_value(self, path)
+
+    def get_memory_snapshot(self) -> Dict[str, object]:
+        """
+        Gets all memoryscopes suitable for logging.
+        :return: object which represents all memory scopes.
+        """
+        result = {}
+
+        for scope in [
+            ms for ms in self.configuration.memory_scopes if ms.include_in_snapshot
+        ]:
+            memory = scope.get_memory(self._dialog_context)
+            if memory:
+                result[scope.name] = memory
+
+        return result
+
+    async def load_all_scopes(self):
+        """
+        Load all of the scopes.
+        :return:
+        """
+        for scope in self.configuration.memory_scopes:
+            await scope.load(self._dialog_context)
+
+    async def save_all_changes(self):
+        """
+        Save all changes for all scopes.
+        :return:
+        """
+        for scope in self.configuration.memory_scopes:
+            await scope.save_changes(self._dialog_context)
+
+    async def delete_scopes_memory_async(self, name: str):
+        """
+        Delete the memory for a scope.
+        :param name: name of the scope.
+        :return:
+        """
+        name = name.upper()
+        scope_list = [
+            ms for ms in self.configuration.memory_scopes if ms.name.upper == name
+        ]
+        if len(scope_list) > 1:
+            raise RuntimeError(f"More than 1 scopes found with the name '{name}'")
+        scope = scope_list[0] if scope_list else None
+        if scope:
+            await scope.delete(self._dialog_context)
+
+    def add(self, key: str, value: object):
+        """
+        Adds an element to the dialog state manager.
+        :param key: Key of the element to add.
+        :param value: Value of the element to add.
+        :return:
+        """
+        raise RuntimeError("Not supported")
+
+    def contains_key(self, key: str) -> bool:
+        """
+        Determines whether the dialog state manager contains an element with the specified key.
+        :param key: The key to locate in the dialog state manager.
+        :return: True if the dialog state manager contains an element with the key otherwise, False.
+        """
+        scopes_with_key = [
+            ms
+            for ms in self.configuration.memory_scopes
+            if ms.name.upper == key.upper()
+        ]
+        return bool(scopes_with_key)
+
+    def remove(self, key: str):
+        """
+        Removes the element with the specified key from the dialog state manager.
+        :param key: Key of the element to remove.
+        :return:
+        """
+        raise RuntimeError("Not supported")
+
+    # 
+    # Removes all items from the dialog state manager.
+    # 
+    # This method is not supported.
+    def clear(self, key: str):
+        """
+        Removes all items from the dialog state manager.
+        :param key: Key of the element to remove.
+        :return:
+        """
+        raise RuntimeError("Not supported")
+
+    def contains(self, item: Tuple[str, object]) -> bool:
+        """
+        Determines whether the dialog state manager contains a specific value (should use __contains__).
+        :param item: The tuple of the item to locate.
+        :return bool: True if item is found in the dialog state manager otherwise, False
+        """
+        raise RuntimeError("Not supported")
+
+    def __contains__(self, item: Tuple[str, object]) -> bool:
+        """
+        Determines whether the dialog state manager contains a specific value.
+        :param item: The tuple of the item to locate.
+        :return bool: True if item is found in the dialog state manager otherwise, False
+        """
+        raise RuntimeError("Not supported")
+
+    def copy_to(self, array: List[Tuple[str, object]], array_index: int):
+        """
+        Copies the elements of the dialog state manager to an array starting at a particular index.
+        :param array: The one-dimensional array that is the destination of the elements copied
+         from the dialog state manager. The array must have zero-based indexing.
+        :param array_index:
+        :return:
+        """
+        for memory_scope in self.configuration.memory_scopes:
+            array[array_index] = (
+                memory_scope.name,
+                memory_scope.get_memory(self._dialog_context),
+            )
+            array_index += 1
+
+    def remove_item(self, item: Tuple[str, object]) -> bool:
+        """
+        Determines whether the dialog state manager contains a specific value (should use __contains__).
+        :param item: The tuple of the item to locate.
+        :return bool: True if item is found in the dialog state manager otherwise, False
+        """
+        raise RuntimeError("Not supported")
+
+    # 
+    # Returns an enumerator that iterates through the collection.
+    # 
+    # An enumerator that can be used to iterate through the collection.
+    def get_enumerator(self) -> Iterator[Tuple[str, object]]:
+        """
+        Returns an enumerator that iterates through the collection.
+        :return: An enumerator that can be used to iterate through the collection.
+        """
+        for memory_scope in self.configuration.memory_scopes:
+            yield (memory_scope.name, memory_scope.get_memory(self._dialog_context))
+
+    def track_paths(self, paths: Iterable[str]) -> List[str]:
+        """
+        Track when specific paths are changed.
+        :param paths: Paths to track.
+        :return: Normalized paths to pass to any_path_changed.
+        """
+        all_paths = []
+        for path in paths:
+            t_path = self.transform_path(path)
+
+            # Track any path that resolves to a constant path
+            segments = self._object_path_cls.try_resolve_path(self, t_path)
+            if segments:
+                n_path = "_".join(segments)
+                self.set_value(self.path_tracker + "." + n_path, 0)
+                all_paths.append(n_path)
+
+        return all_paths
+
+    def any_path_changed(self, counter: int, paths: Iterable[str]) -> bool:
+        """
+        Check to see if any path has changed since watermark.
+        :param counter: Time counter to compare to.
+        :param paths: Paths from track_paths to check.
+        :return: True if any path has changed since counter.
+        """
+        found = False
+        if paths:
+            for path in paths:
+                if self.get_value(int, self.path_tracker + "." + path) > counter:
+                    found = True
+                    break
+
+        return found
+
+    def __iter__(self):
+        for memory_scope in self.configuration.memory_scopes:
+            yield (memory_scope.name, memory_scope.get_memory(self._dialog_context))
+
+    @staticmethod
+    def _try_get_first_nested_value(
+        remaining_path: str, memory: object
+    ) -> Tuple[bool, object]:
+        # These modules are imported at static level to avoid circular dependency problems
+        # pylint: disable=import-outside-toplevel
+
+        from botbuilder.dialogs import ObjectPath
+
+        array = ObjectPath.try_get_path_value(memory, remaining_path)
+        if array:
+            if isinstance(array[0], list):
+                first = array[0]
+                if first:
+                    second = first[0]
+                    return True, second
+
+                return False, None
+
+            return True, array[0]
+
+        return False, None
+
+    def _track_change(self, path: str, value: object) -> bool:
+        has_path = False
+        segments = self._object_path_cls.try_resolve_path(self, path)
+        if segments:
+            root = segments[1] if len(segments) > 1 else ""
+
+            # Skip _* as first scope, i.e. _adaptive, _tracker, ...
+            if not root.startswith("_"):
+                # Convert to a simple path with _ between segments
+                path_name = "_".join(segments)
+                tracked_path = f"{self.path_tracker}.{path_name}"
+                counter = None
+
+                def update():
+                    nonlocal counter
+                    last_changed = self.try_get_value(tracked_path, int)
+                    if last_changed:
+                        if counter is not None:
+                            counter = self.get_value(int, DialogPath.EVENT_COUNTER)
+
+                        self.set_value(tracked_path, counter)
+
+                update()
+                if not self._is_primitive(type(value)):
+                    # For an object we need to see if any children path are being tracked
+                    def check_children(property: str, instance: object):
+                        nonlocal tracked_path
+                        # Add new child segment
+                        tracked_path += "_" + property.lower()
+                        update()
+                        if not self._is_primitive(type(instance)):
+                            self._object_path_cls.for_each_property(
+                                property, check_children
+                            )
+
+                        # Remove added child segment
+                        tracked_path = tracked_path.Substring(
+                            0, tracked_path.LastIndexOf("_")
+                        )
+
+                    self._object_path_cls.for_each_property(value, check_children)
+
+            has_path = True
+
+        return has_path
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/dialog_state_manager_configuration.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/dialog_state_manager_configuration.py
new file mode 100644
index 000000000..b1565a53d
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/dialog_state_manager_configuration.py
@@ -0,0 +1,10 @@
+from typing import List
+
+from botbuilder.dialogs.memory.scopes import MemoryScope
+from .path_resolver_base import PathResolverBase
+
+
+class DialogStateManagerConfiguration:
+    def __init__(self):
+        self.path_resolvers: List[PathResolverBase] = list()
+        self.memory_scopes: List[MemoryScope] = list()
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/path_resolver_base.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/path_resolver_base.py
new file mode 100644
index 000000000..42b80c93f
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/path_resolver_base.py
@@ -0,0 +1,7 @@
+from abc import ABC, abstractmethod
+
+
+class PathResolverBase(ABC):
+    @abstractmethod
+    def transform_path(self, path: str):
+        raise NotImplementedError()
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/path_resolvers/__init__.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/path_resolvers/__init__.py
new file mode 100644
index 000000000..b22ac063a
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/path_resolvers/__init__.py
@@ -0,0 +1,19 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+
+from .alias_path_resolver import AliasPathResolver
+from .at_at_path_resolver import AtAtPathResolver
+from .at_path_resolver import AtPathResolver
+from .dollar_path_resolver import DollarPathResolver
+from .hash_path_resolver import HashPathResolver
+from .percent_path_resolver import PercentPathResolver
+
+__all__ = [
+    "AliasPathResolver",
+    "AtAtPathResolver",
+    "AtPathResolver",
+    "DollarPathResolver",
+    "HashPathResolver",
+    "PercentPathResolver",
+]
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/path_resolvers/alias_path_resolver.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/path_resolvers/alias_path_resolver.py
new file mode 100644
index 000000000..b16930284
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/path_resolvers/alias_path_resolver.py
@@ -0,0 +1,53 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.dialogs.memory import PathResolverBase
+
+
+class AliasPathResolver(PathResolverBase):
+    def __init__(self, alias: str, prefix: str, postfix: str = None):
+        """
+        Initializes a new instance of the  class.
+        Alias name.
+        Prefix name.
+        Postfix name.
+        """
+        if alias is None:
+            raise TypeError(f"Expecting: alias, but received None")
+        if prefix is None:
+            raise TypeError(f"Expecting: prefix, but received None")
+
+        # Gets the alias name.
+        self.alias = alias.strip()
+        self._prefix = prefix.strip()
+        self._postfix = postfix.strip() if postfix else ""
+
+    def transform_path(self, path: str):
+        """
+        Transforms the path.
+        Path to inspect.
+        Transformed path.
+        """
+        if not path:
+            raise TypeError(f"Expecting: path, but received None")
+
+        path = path.strip()
+        if (
+            path.startswith(self.alias)
+            and len(path) > len(self.alias)
+            and AliasPathResolver._is_path_char(path[len(self.alias)])
+        ):
+            # here we only deals with trailing alias, alias in middle be handled in further breakdown
+            # $xxx -> path.xxx
+            return f"{self._prefix}{path[len(self.alias):]}{self._postfix}".rstrip(".")
+
+        return path
+
+    @staticmethod
+    def _is_path_char(char: str) -> bool:
+        """
+        Verifies if a character is valid for a path.
+        Character to verify.
+        true if the character is valid for a path otherwise, false.
+        """
+        return len(char) == 1 and (char.isalpha() or char == "_")
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/path_resolvers/at_at_path_resolver.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/path_resolvers/at_at_path_resolver.py
new file mode 100644
index 000000000..d440c040a
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/path_resolvers/at_at_path_resolver.py
@@ -0,0 +1,9 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from .alias_path_resolver import AliasPathResolver
+
+
+class AtAtPathResolver(AliasPathResolver):
+    def __init__(self):
+        super().__init__(alias="@@", prefix="turn.recognized.entities.")
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/path_resolvers/at_path_resolver.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/path_resolvers/at_path_resolver.py
new file mode 100644
index 000000000..91bbb6564
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/path_resolvers/at_path_resolver.py
@@ -0,0 +1,43 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from .alias_path_resolver import AliasPathResolver
+
+
+class AtPathResolver(AliasPathResolver):
+
+    _DELIMITERS = [".", "["]
+
+    def __init__(self):
+        super().__init__(alias="@", prefix="")
+
+        self._PREFIX = "turn.recognized.entities."  # pylint: disable=invalid-name
+
+    def transform_path(self, path: str):
+        if not path:
+            raise TypeError(f"Expecting: path, but received None")
+
+        path = path.strip()
+        if (
+            path.startswith("@")
+            and len(path) > 1
+            and AtPathResolver._is_path_char(path[1])
+        ):
+            end = any(delimiter in path for delimiter in AtPathResolver._DELIMITERS)
+            if end == -1:
+                end = len(path)
+
+            prop = path[1:end]
+            suffix = path[end:]
+            path = f"{self._PREFIX}{prop}.first(){suffix}"
+
+        return path
+
+    @staticmethod
+    def _index_of_any(string: str, elements_to_search_for) -> int:
+        for element in elements_to_search_for:
+            index = string.find(element)
+            if index != -1:
+                return index
+
+        return -1
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/path_resolvers/dollar_path_resolver.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/path_resolvers/dollar_path_resolver.py
new file mode 100644
index 000000000..8152d23c5
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/path_resolvers/dollar_path_resolver.py
@@ -0,0 +1,9 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from .alias_path_resolver import AliasPathResolver
+
+
+class DollarPathResolver(AliasPathResolver):
+    def __init__(self):
+        super().__init__(alias="$", prefix="dialog.")
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/path_resolvers/hash_path_resolver.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/path_resolvers/hash_path_resolver.py
new file mode 100644
index 000000000..b00376e59
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/path_resolvers/hash_path_resolver.py
@@ -0,0 +1,9 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from .alias_path_resolver import AliasPathResolver
+
+
+class HashPathResolver(AliasPathResolver):
+    def __init__(self):
+        super().__init__(alias="#", prefix="turn.recognized.intents.")
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/path_resolvers/percent_path_resolver.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/path_resolvers/percent_path_resolver.py
new file mode 100644
index 000000000..dd0fa2e17
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/path_resolvers/percent_path_resolver.py
@@ -0,0 +1,9 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from .alias_path_resolver import AliasPathResolver
+
+
+class PercentPathResolver(AliasPathResolver):
+    def __init__(self):
+        super().__init__(alias="%", prefix="class.")
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scope_path.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scope_path.py
new file mode 100644
index 000000000..faf906699
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scope_path.py
@@ -0,0 +1,35 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+# User memory scope root path.
+# This property is deprecated, use ScopePath.User instead.
+USER = "user"
+
+# Conversation memory scope root path.
+# This property is deprecated, use ScopePath.Conversation instead.This property is deprecated, use ScopePath.Dialog instead.This property is deprecated, use ScopePath.DialogClass instead.This property is deprecated, use ScopePath.This instead.This property is deprecated, use ScopePath.Class instead.
+CLASS = "class"
+
+# Settings memory scope root path.
+# This property is deprecated, use ScopePath.Settings instead.
+
+SETTINGS = "settings"
+
+# Turn memory scope root path.
+# This property is deprecated, use ScopePath.Turn instead.
+TURN = "turn"
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/__init__.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/__init__.py
new file mode 100644
index 000000000..ec2e2b61c
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/__init__.py
@@ -0,0 +1,32 @@
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+# --------------------------------------------------------------------------
+from .bot_state_memory_scope import BotStateMemoryScope
+from .class_memory_scope import ClassMemoryScope
+from .conversation_memory_scope import ConversationMemoryScope
+from .dialog_class_memory_scope import DialogClassMemoryScope
+from .dialog_context_memory_scope import DialogContextMemoryScope
+from .dialog_memory_scope import DialogMemoryScope
+from .memory_scope import MemoryScope
+from .settings_memory_scope import SettingsMemoryScope
+from .this_memory_scope import ThisMemoryScope
+from .turn_memory_scope import TurnMemoryScope
+from .user_memory_scope import UserMemoryScope
+
+
+__all__ = [
+    "BotStateMemoryScope",
+    "ClassMemoryScope",
+    "ConversationMemoryScope",
+    "DialogClassMemoryScope",
+    "DialogContextMemoryScope",
+    "DialogMemoryScope",
+    "MemoryScope",
+    "SettingsMemoryScope",
+    "ThisMemoryScope",
+    "TurnMemoryScope",
+    "UserMemoryScope",
+]
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/bot_state_memory_scope.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/bot_state_memory_scope.py
new file mode 100644
index 000000000..088c7a0fb
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/bot_state_memory_scope.py
@@ -0,0 +1,43 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from typing import Type
+
+from botbuilder.core import BotState
+
+from .memory_scope import MemoryScope
+
+
+class BotStateMemoryScope(MemoryScope):
+    def __init__(self, bot_state_type: Type[BotState], name: str):
+        super().__init__(name, include_in_snapshot=True)
+        self.bot_state_type = bot_state_type
+
+    def get_memory(self, dialog_context: "DialogContext") -> object:
+        if not dialog_context:
+            raise TypeError(f"Expecting: DialogContext, but received None")
+
+        bot_state: BotState = self._get_bot_state(dialog_context)
+        cached_state = (
+            bot_state.get_cached_state(dialog_context.context) if bot_state else None
+        )
+
+        return cached_state.state if cached_state else None
+
+    def set_memory(self, dialog_context: "DialogContext", memory: object):
+        raise RuntimeError("You cannot replace the root BotState object")
+
+    async def load(self, dialog_context: "DialogContext", force: bool = False):
+        bot_state: BotState = self._get_bot_state(dialog_context)
+
+        if bot_state:
+            await bot_state.load(dialog_context.context, force)
+
+    async def save_changes(self, dialog_context: "DialogContext", force: bool = False):
+        bot_state: BotState = self._get_bot_state(dialog_context)
+
+        if bot_state:
+            await bot_state.save_changes(dialog_context.context, force)
+
+    def _get_bot_state(self, dialog_context: "DialogContext") -> BotState:
+        return dialog_context.context.turn_state.get(self.bot_state_type.__name__, None)
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/class_memory_scope.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/class_memory_scope.py
new file mode 100644
index 000000000..1589ac152
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/class_memory_scope.py
@@ -0,0 +1,57 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from collections import namedtuple
+
+from botbuilder.dialogs.memory import scope_path
+
+from .memory_scope import MemoryScope
+
+
+class ClassMemoryScope(MemoryScope):
+    def __init__(self):
+        super().__init__(scope_path.SETTINGS, include_in_snapshot=False)
+
+    def get_memory(self, dialog_context: "DialogContext") -> object:
+        if not dialog_context:
+            raise TypeError(f"Expecting: DialogContext, but received None")
+
+        # if active dialog is a container dialog then "dialogclass" binds to it.
+        if dialog_context.active_dialog:
+            dialog = dialog_context.find_dialog_sync(dialog_context.active_dialog.id)
+            if dialog:
+                return ClassMemoryScope._bind_to_dialog_context(dialog, dialog_context)
+
+        return None
+
+    def set_memory(self, dialog_context: "DialogContext", memory: object):
+        raise Exception(
+            f"{self.__class__.__name__}.set_memory not supported (read only)"
+        )
+
+    @staticmethod
+    def _bind_to_dialog_context(obj, dialog_context: "DialogContext") -> object:
+        clone = {}
+        for prop in dir(obj):
+            # don't process double underscore attributes
+            if prop[:1] != "_":
+                prop_value = getattr(obj, prop)
+                if not callable(prop_value):
+                    # the only objects
+                    if hasattr(prop_value, "try_get_value"):
+                        clone[prop] = prop_value.try_get_value(dialog_context.state)
+                    elif hasattr(prop_value, "__dict__") and not isinstance(
+                        prop_value, type
+                    ):
+                        clone[prop] = ClassMemoryScope._bind_to_dialog_context(
+                            prop_value, dialog_context
+                        )
+                    else:
+                        clone[prop] = prop_value
+        if clone:
+            ReadOnlyObject = namedtuple(  # pylint: disable=invalid-name
+                "ReadOnlyObject", clone
+            )
+            return ReadOnlyObject(**clone)
+
+        return None
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/conversation_memory_scope.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/conversation_memory_scope.py
new file mode 100644
index 000000000..2f88dd57a
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/conversation_memory_scope.py
@@ -0,0 +1,12 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.core import ConversationState
+from botbuilder.dialogs.memory import scope_path
+
+from .bot_state_memory_scope import BotStateMemoryScope
+
+
+class ConversationMemoryScope(BotStateMemoryScope):
+    def __init__(self):
+        super().__init__(ConversationState, scope_path.CONVERSATION)
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/dialog_class_memory_scope.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/dialog_class_memory_scope.py
new file mode 100644
index 000000000..b363d1065
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/dialog_class_memory_scope.py
@@ -0,0 +1,45 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from copy import deepcopy
+
+from botbuilder.dialogs.memory import scope_path
+
+from .memory_scope import MemoryScope
+
+
+class DialogClassMemoryScope(MemoryScope):
+    def __init__(self):
+        # pylint: disable=import-outside-toplevel
+        super().__init__(scope_path.DIALOG_CLASS, include_in_snapshot=False)
+
+        # This import is to avoid circular dependency issues
+        from botbuilder.dialogs import DialogContainer
+
+        self._dialog_container_cls = DialogContainer
+
+    def get_memory(self, dialog_context: "DialogContext") -> object:
+        if not dialog_context:
+            raise TypeError(f"Expecting: DialogContext, but received None")
+
+        # if active dialog is a container dialog then "dialogclass" binds to it.
+        if dialog_context.active_dialog:
+            dialog = dialog_context.find_dialog_sync(dialog_context.active_dialog.id)
+            if isinstance(dialog, self._dialog_container_cls):
+                return deepcopy(dialog)
+
+        # Otherwise we always bind to parent, or if there is no parent the active dialog
+        parent_id = (
+            dialog_context.parent.active_dialog.id
+            if dialog_context.parent and dialog_context.parent.active_dialog
+            else None
+        )
+        active_id = (
+            dialog_context.active_dialog.id if dialog_context.active_dialog else None
+        )
+        return deepcopy(dialog_context.find_dialog_sync(parent_id or active_id))
+
+    def set_memory(self, dialog_context: "DialogContext", memory: object):
+        raise Exception(
+            f"{self.__class__.__name__}.set_memory not supported (read only)"
+        )
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/dialog_context_memory_scope.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/dialog_context_memory_scope.py
new file mode 100644
index 000000000..200f71b8c
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/dialog_context_memory_scope.py
@@ -0,0 +1,65 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.dialogs.memory import scope_path
+
+from .memory_scope import MemoryScope
+
+
+class DialogContextMemoryScope(MemoryScope):
+    def __init__(self):
+        # pylint: disable=invalid-name
+
+        super().__init__(scope_path.SETTINGS, include_in_snapshot=False)
+        # Stack name.
+        self.STACK = "stack"
+
+        # Active dialog name.
+        self.ACTIVE_DIALOG = "activeDialog"
+
+        # Parent name.
+        self.PARENT = "parent"
+
+    def get_memory(self, dialog_context: "DialogContext") -> object:
+        """
+        Gets the backing memory for this scope.
+        The  object for this turn.
+        Memory for the scope.
+        """
+        if not dialog_context:
+            raise TypeError(f"Expecting: DialogContext, but received None")
+
+        # TODO: make sure that every object in the dict is serializable
+        memory = {}
+        stack = list([])
+        current_dc = dialog_context
+
+        # go to leaf node
+        while current_dc.child:
+            current_dc = current_dc.child
+
+        while current_dc:
+            # (PORTERS NOTE: javascript stack is reversed with top of stack on end)
+            for item in current_dc.stack:
+                # filter out ActionScope items because they are internal bookkeeping.
+                if not item.id.startswith("ActionScope["):
+                    stack.append(item.id)
+
+            current_dc = current_dc.parent
+
+        # top of stack is stack[0].
+        memory[self.STACK] = stack
+        memory[self.ACTIVE_DIALOG] = (
+            dialog_context.active_dialog.id if dialog_context.active_dialog else None
+        )
+        memory[self.PARENT] = (
+            dialog_context.parent.active_dialog.id
+            if dialog_context.parent and dialog_context.parent.active_dialog
+            else None
+        )
+        return memory
+
+    def set_memory(self, dialog_context: "DialogContext", memory: object):
+        raise Exception(
+            f"{self.__class__.__name__}.set_memory not supported (read only)"
+        )
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/dialog_memory_scope.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/dialog_memory_scope.py
new file mode 100644
index 000000000..490ad23a1
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/dialog_memory_scope.py
@@ -0,0 +1,68 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.dialogs.memory import scope_path
+
+from .memory_scope import MemoryScope
+
+
+class DialogMemoryScope(MemoryScope):
+    def __init__(self):
+        # pylint: disable=import-outside-toplevel
+        super().__init__(scope_path.DIALOG)
+
+        # This import is to avoid circular dependency issues
+        from botbuilder.dialogs import DialogContainer
+
+        self._dialog_container_cls = DialogContainer
+
+    def get_memory(self, dialog_context: "DialogContext") -> object:
+        if not dialog_context:
+            raise TypeError(f"Expecting: DialogContext, but received None")
+
+        # if active dialog is a container dialog then "dialog" binds to it.
+        if dialog_context.active_dialog:
+            dialog = dialog_context.find_dialog_sync(dialog_context.active_dialog.id)
+            if isinstance(dialog, self._dialog_container_cls):
+                return dialog_context.active_dialog.state
+
+        # Otherwise we always bind to parent, or if there is no parent the active dialog
+        parent_state = (
+            dialog_context.parent.active_dialog.state
+            if dialog_context.parent and dialog_context.parent.active_dialog
+            else None
+        )
+        dc_state = (
+            dialog_context.active_dialog.state if dialog_context.active_dialog else None
+        )
+        return parent_state or dc_state
+
+    def set_memory(self, dialog_context: "DialogContext", memory: object):
+        if not dialog_context:
+            raise TypeError(f"Expecting: DialogContext, but received None")
+
+        if not memory:
+            raise TypeError(f"Expecting: memory object, but received None")
+
+        # If active dialog is a container dialog then "dialog" binds to it.
+        # Otherwise the "dialog" will bind to the dialogs parent assuming it
+        # is a container.
+        parent = dialog_context
+        if not self.is_container(parent) and self.is_container(parent.parent):
+            parent = parent.parent
+
+        # If there's no active dialog then throw an error.
+        if not parent.active_dialog:
+            raise Exception(
+                "Cannot set DialogMemoryScope. There is no active dialog dialog or parent dialog in the context"
+            )
+
+        parent.active_dialog.state = memory
+
+    def is_container(self, dialog_context: "DialogContext"):
+        if dialog_context and dialog_context.active_dialog:
+            dialog = dialog_context.find_dialog_sync(dialog_context.active_dialog.id)
+            if isinstance(dialog, self._dialog_container_cls):
+                return True
+
+        return False
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/memory_scope.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/memory_scope.py
new file mode 100644
index 000000000..3b00401fc
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/memory_scope.py
@@ -0,0 +1,84 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from abc import ABC, abstractmethod
+
+
+class MemoryScope(ABC):
+    def __init__(self, name: str, include_in_snapshot: bool = True):
+        # 
+        # Gets or sets name of the scope.
+        # 
+        # 
+        # Name of the scope.
+        # 
+        self.include_in_snapshot = include_in_snapshot
+        # 
+        # Gets or sets a value indicating whether this memory should be included in snapshot.
+        # 
+        # 
+        # True or false.
+        # 
+        self.name = name
+
+    # 
+    # Get the backing memory for this scope.
+    # 
+    # dc.
+    # memory for the scope.
+    @abstractmethod
+    def get_memory(
+        self, dialog_context: "DialogContext"
+    ) -> object:  # pylint: disable=unused-argument
+        raise NotImplementedError()
+
+    # 
+    # Changes the backing object for the memory scope.
+    # 
+    # dc.
+    # memory.
+    @abstractmethod
+    def set_memory(
+        self, dialog_context: "DialogContext", memory: object
+    ):  # pylint: disable=unused-argument
+        raise NotImplementedError()
+
+    # 
+    # Populates the state cache for this  from the storage layer.
+    # 
+    # The dialog context object for this turn.
+    # Optional, true to overwrite any existing state cache
+    # or false to load state from storage only if the cache doesn't already exist.
+    # A cancellation token that can be used by other objects
+    # or threads to receive notice of cancellation.
+    # A task that represents the work queued to execute.
+    async def load(
+        self, dialog_context: "DialogContext", force: bool = False
+    ):  # pylint: disable=unused-argument
+        return
+
+    # 
+    # Writes the state cache for this  to the storage layer.
+    # 
+    # The dialog context object for this turn.
+    # Optional, true to save the state cache to storage
+    # or false to save state to storage only if a property in the cache has changed.
+    # A cancellation token that can be used by other objects
+    # or threads to receive notice of cancellation.
+    # A task that represents the work queued to execute.
+    async def save_changes(
+        self, dialog_context: "DialogContext", force: bool = False
+    ):  # pylint: disable=unused-argument
+        return
+
+    # 
+    # Deletes any state in storage and the cache for this .
+    # 
+    # The dialog context object for this turn.
+    # A cancellation token that can be used by other objects
+    # or threads to receive notice of cancellation.
+    # A task that represents the work queued to execute.
+    async def delete(
+        self, dialog_context: "DialogContext"
+    ):  # pylint: disable=unused-argument
+        return
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/settings_memory_scope.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/settings_memory_scope.py
new file mode 100644
index 000000000..790137aea
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/settings_memory_scope.py
@@ -0,0 +1,31 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.dialogs.memory import scope_path
+
+from .memory_scope import MemoryScope
+
+
+class SettingsMemoryScope(MemoryScope):
+    def __init__(self):
+        super().__init__(scope_path.SETTINGS)
+        self._empty_settings = {}
+        self.include_in_snapshot = False
+
+    def get_memory(self, dialog_context: "DialogContext") -> object:
+        if not dialog_context:
+            raise TypeError(f"Expecting: DialogContext, but received None")
+
+        settings: dict = dialog_context.context.turn_state.get(
+            scope_path.SETTINGS, None
+        )
+
+        if not settings:
+            settings = self._empty_settings
+
+        return settings
+
+    def set_memory(self, dialog_context: "DialogContext", memory: object):
+        raise Exception(
+            f"{self.__class__.__name__}.set_memory not supported (read only)"
+        )
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/this_memory_scope.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/this_memory_scope.py
new file mode 100644
index 000000000..3de53bab3
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/this_memory_scope.py
@@ -0,0 +1,28 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.dialogs.memory import scope_path
+
+from .memory_scope import MemoryScope
+
+
+class ThisMemoryScope(MemoryScope):
+    def __init__(self):
+        super().__init__(scope_path.THIS)
+
+    def get_memory(self, dialog_context: "DialogContext") -> object:
+        if not dialog_context:
+            raise TypeError(f"Expecting: DialogContext, but received None")
+
+        return (
+            dialog_context.active_dialog.state if dialog_context.active_dialog else None
+        )
+
+    def set_memory(self, dialog_context: "DialogContext", memory: object):
+        if not dialog_context:
+            raise TypeError(f"Expecting: DialogContext, but received None")
+
+        if not memory:
+            raise TypeError(f"Expecting: object, but received None")
+
+        dialog_context.active_dialog.state = memory
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/turn_memory_scope.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/turn_memory_scope.py
new file mode 100644
index 000000000..3773edf6b
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/turn_memory_scope.py
@@ -0,0 +1,79 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.dialogs.memory import scope_path
+
+from .memory_scope import MemoryScope
+
+
+class CaseInsensitiveDict(dict):
+    # pylint: disable=protected-access
+
+    @classmethod
+    def _k(cls, key):
+        return key.lower() if isinstance(key, str) else key
+
+    def __init__(self, *args, **kwargs):
+        super(CaseInsensitiveDict, self).__init__(*args, **kwargs)
+        self._convert_keys()
+
+    def __getitem__(self, key):
+        return super(CaseInsensitiveDict, self).__getitem__(self.__class__._k(key))
+
+    def __setitem__(self, key, value):
+        super(CaseInsensitiveDict, self).__setitem__(self.__class__._k(key), value)
+
+    def __delitem__(self, key):
+        return super(CaseInsensitiveDict, self).__delitem__(self.__class__._k(key))
+
+    def __contains__(self, key):
+        return super(CaseInsensitiveDict, self).__contains__(self.__class__._k(key))
+
+    def pop(self, key, *args, **kwargs):
+        return super(CaseInsensitiveDict, self).pop(
+            self.__class__._k(key), *args, **kwargs
+        )
+
+    def get(self, key, *args, **kwargs):
+        return super(CaseInsensitiveDict, self).get(
+            self.__class__._k(key), *args, **kwargs
+        )
+
+    def setdefault(self, key, *args, **kwargs):
+        return super(CaseInsensitiveDict, self).setdefault(
+            self.__class__._k(key), *args, **kwargs
+        )
+
+    def update(self, e=None, **f):
+        if e is None:
+            e = {}
+        super(CaseInsensitiveDict, self).update(self.__class__(e))
+        super(CaseInsensitiveDict, self).update(self.__class__(**f))
+
+    def _convert_keys(self):
+        for k in list(self.keys()):
+            val = super(CaseInsensitiveDict, self).pop(k)
+            self.__setitem__(k, val)
+
+
+class TurnMemoryScope(MemoryScope):
+    def __init__(self):
+        super().__init__(scope_path.TURN)
+
+    def get_memory(self, dialog_context: "DialogContext") -> object:
+        if not dialog_context:
+            raise TypeError(f"Expecting: DialogContext, but received None")
+
+        turn_value = dialog_context.context.turn_state.get(scope_path.TURN, None)
+
+        if not turn_value:
+            turn_value = CaseInsensitiveDict()
+            dialog_context.context.turn_state[scope_path.TURN] = turn_value
+
+        return turn_value
+
+    def set_memory(self, dialog_context: "DialogContext", memory: object):
+        if not dialog_context:
+            raise TypeError(f"Expecting: DialogContext, but received None")
+
+        dialog_context.context.turn_state[scope_path.TURN] = memory
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/user_memory_scope.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/user_memory_scope.py
new file mode 100644
index 000000000..b1bc6351d
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/memory/scopes/user_memory_scope.py
@@ -0,0 +1,12 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.core import UserState
+from botbuilder.dialogs.memory import scope_path
+
+from .bot_state_memory_scope import BotStateMemoryScope
+
+
+class UserMemoryScope(BotStateMemoryScope):
+    def __init__(self):
+        super().__init__(UserState, scope_path.USER)
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/object_path.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/object_path.py
new file mode 100644
index 000000000..80f722519
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/object_path.py
@@ -0,0 +1,313 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import copy
+from typing import Union, Callable
+
+
+class ObjectPath:
+    """
+    Helper methods for working with json objects.
+    """
+
+    @staticmethod
+    def assign(start_object, overlay_object, default: Union[Callable, object] = None):
+        """
+        Creates a new object by overlaying values in start_object with non-null values from overlay_object.
+
+        :param start_object: dict or typed object, the target object to set values on
+        :param overlay_object: dict or typed object, the item to overlay values form
+        :param default: Provides a default object if both source and overlay are None
+        :return: A copy of start_object, with values from overlay_object
+        """
+        if start_object and overlay_object:
+            merged = copy.deepcopy(start_object)
+
+            def merge(target: dict, source: dict):
+                key_set = set(target).union(set(source))
+
+                for key in key_set:
+                    target_value = target.get(key)
+                    source_value = source.get(key)
+
+                    # skip empty overlay items
+                    if source_value:
+                        if isinstance(source_value, dict):
+                            # merge dictionaries
+                            if not target_value:
+                                target[key] = copy.deepcopy(source_value)
+                            else:
+                                merge(target_value, source_value)
+                        elif not hasattr(source_value, "__dict__"):
+                            # simple type.  just copy it.
+                            target[key] = copy.copy(source_value)
+                        elif not target_value:
+                            # the target doesn't have the value, but
+                            # the overlay does.  just copy it.
+                            target[key] = copy.deepcopy(source_value)
+                        else:
+                            # recursive class copy
+                            merge(target_value.__dict__, source_value.__dict__)
+
+            target_dict = merged if isinstance(merged, dict) else merged.__dict__
+            overlay_dict = (
+                overlay_object
+                if isinstance(overlay_object, dict)
+                else overlay_object.__dict__
+            )
+            merge(target_dict, overlay_dict)
+
+            return merged
+
+        if overlay_object:
+            return copy.deepcopy(overlay_object)
+
+        if start_object:
+            return start_object
+        if default:
+            return default() if callable(default) else copy.deepcopy(default)
+        return None
+
+    @staticmethod
+    def set_path_value(obj, path: str, value: object):
+        """
+        Given an object evaluate a path to set the value.
+        """
+
+        segments = ObjectPath.try_resolve_path(obj, path)
+        if not segments:
+            return
+
+        current = obj
+        for i in range(len(segments) - 1):
+            segment = segments[i]
+            if ObjectPath.is_int(segment):
+                index = int(segment)
+                next_obj = current[index]
+                if not next_obj and len(current) <= index:
+                    # Expand list to index
+                    current += [None] * ((index + 1) - len(current))
+                    next_obj = current[index]
+            else:
+                next_obj = ObjectPath.__get_object_property(current, segment)
+                if not next_obj:
+                    # Create object or list based on next segment
+                    next_segment = segments[i + 1]
+                    if not ObjectPath.is_int(next_segment):
+                        ObjectPath.__set_object_segment(current, segment, {})
+                    else:
+                        ObjectPath.__set_object_segment(current, segment, [])
+
+                    next_obj = ObjectPath.__get_object_property(current, segment)
+
+            current = next_obj
+
+        last_segment = segments[-1]
+        ObjectPath.__set_object_segment(current, last_segment, value)
+
+    @staticmethod
+    def get_path_value(
+        obj, path: str, default: Union[Callable, object] = None
+    ) -> object:
+        """
+        Get the value for a path relative to an object.
+        """
+
+        value = ObjectPath.try_get_path_value(obj, path)
+        if value:
+            return value
+
+        if default is None:
+            raise KeyError(f"Key {path} not found")
+        return default() if callable(default) else copy.deepcopy(default)
+
+    @staticmethod
+    def has_value(obj, path: str) -> bool:
+        """
+        Does an object have a subpath.
+        """
+        return ObjectPath.try_get_path_value(obj, path) is not None
+
+    @staticmethod
+    def remove_path_value(obj, path: str):
+        """
+        Remove path from object.
+        """
+
+        segments = ObjectPath.try_resolve_path(obj, path)
+        if not segments:
+            return
+
+        current = obj
+        for i in range(len(segments) - 1):
+            segment = segments[i]
+            current = ObjectPath.__resolve_segment(current, segment)
+            if not current:
+                return
+
+        if current:
+            last_segment = segments[-1]
+            if ObjectPath.is_int(last_segment):
+                current[int(last_segment)] = None
+            else:
+                current.pop(last_segment)
+
+    @staticmethod
+    def try_get_path_value(obj, path: str) -> object:
+        """
+        Get the value for a path relative to an object.
+        """
+
+        if not obj:
+            return None
+
+        if path is None:
+            return None
+
+        if not path:
+            return obj
+
+        segments = ObjectPath.try_resolve_path(obj, path)
+        if not segments:
+            return None
+
+        result = ObjectPath.__resolve_segments(obj, segments)
+        if not result:
+            return None
+
+        return result
+
+    @staticmethod
+    def __set_object_segment(obj, segment, value):
+        val = ObjectPath.__get_normalized_value(value)
+
+        if ObjectPath.is_int(segment):
+            # the target is an list
+            index = int(segment)
+
+            # size the list if needed
+            obj += [None] * ((index + 1) - len(obj))
+
+            obj[index] = val
+            return
+
+        # the target is a dictionary
+        obj[segment] = val
+
+    @staticmethod
+    def __get_normalized_value(value):
+        return value
+
+    @staticmethod
+    def try_resolve_path(obj, property_path: str, evaluate: bool = False) -> []:
+        so_far = []
+        first = property_path[0] if property_path else " "
+        if first in ("'", '"'):
+            if not property_path.endswith(first):
+                return None
+
+            so_far.append(property_path[1 : len(property_path) - 2])
+        elif ObjectPath.is_int(property_path):
+            so_far.append(int(property_path))
+        else:
+            start = 0
+            i = 0
+
+            def emit():
+                nonlocal start, i
+                segment = property_path[start:i]
+                if segment:
+                    so_far.append(segment)
+                start = i + 1
+
+            while i < len(property_path):
+                char = property_path[i]
+                if char in (".", "["):
+                    emit()
+
+                if char == "[":
+                    nesting = 1
+                    i += 1
+                    while i < len(property_path):
+                        char = property_path[i]
+                        if char == "[":
+                            nesting += 1
+                        elif char == "]":
+                            nesting -= 1
+                            if nesting == 0:
+                                break
+                        i += 1
+
+                    if nesting > 0:
+                        return None
+
+                    expr = property_path[start:i]
+                    start = i + 1
+                    indexer = ObjectPath.try_resolve_path(obj, expr, True)
+                    if not indexer:
+                        return None
+
+                    result = indexer[0]
+                    if ObjectPath.is_int(result):
+                        so_far.append(int(result))
+                    else:
+                        so_far.append(result)
+
+                i += 1
+
+            emit()
+
+            if evaluate:
+                result = ObjectPath.__resolve_segments(obj, so_far)
+                if not result:
+                    return None
+
+                so_far.clear()
+                so_far.append(result)
+
+        return so_far
+
+    @staticmethod
+    def for_each_property(obj: object, action: Callable[[str, object], None]):
+        if isinstance(obj, dict):
+            for key, value in obj.items():
+                action(key, value)
+        elif hasattr(obj, "__dict__"):
+            for key, value in vars(obj).items():
+                action(key, value)
+
+    @staticmethod
+    def __resolve_segments(current, segments: []) -> object:
+        result = current
+
+        for segment in segments:
+            result = ObjectPath.__resolve_segment(result, segment)
+            if not result:
+                return None
+
+        return result
+
+    @staticmethod
+    def __resolve_segment(current, segment) -> object:
+        if current:
+            if ObjectPath.is_int(segment):
+                current = current[int(segment)]
+            else:
+                current = ObjectPath.__get_object_property(current, segment)
+
+        return current
+
+    @staticmethod
+    def __get_object_property(obj, property_name: str):
+        # doing a case insensitive search
+        property_name_lower = property_name.lower()
+        matching = [obj[key] for key in obj if key.lower() == property_name_lower]
+        return matching[0] if matching else None
+
+    @staticmethod
+    def is_int(value: str) -> bool:
+        try:
+            int(value)
+            return True
+        except ValueError:
+            return False
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/persisted_state.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/persisted_state.py
new file mode 100644
index 000000000..e4fc016e8
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/persisted_state.py
@@ -0,0 +1,20 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from typing import Dict
+
+from .persisted_state_keys import PersistedStateKeys
+
+
+class PersistedState:
+    def __init__(self, keys: PersistedStateKeys = None, data: Dict[str, object] = None):
+        if keys and data:
+            self.user_state: Dict[str, object] = data[
+                keys.user_state
+            ] if keys.user_state in data else {}
+            self.conversation_state: Dict[str, object] = data[
+                keys.conversation_state
+            ] if keys.conversation_state in data else {}
+        else:
+            self.user_state: Dict[str, object] = {}
+            self.conversation_state: Dict[str, object] = {}
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/persisted_state_keys.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/persisted_state_keys.py
new file mode 100644
index 000000000..59f7c34cd
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/persisted_state_keys.py
@@ -0,0 +1,8 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+
+class PersistedStateKeys:
+    def __init__(self):
+        self.user_state: str = None
+        self.conversation_state: str = None
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/activity_prompt.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/activity_prompt.py
index 5930441e1..a8f2f944e 100644
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/activity_prompt.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/activity_prompt.py
@@ -1,7 +1,6 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License.
 
-from abc import ABC
 from typing import Callable, Dict
 
 from botbuilder.core import TurnContext
@@ -20,13 +19,19 @@
 from .prompt_validator_context import PromptValidatorContext
 
 
-class ActivityPrompt(Dialog, ABC):
+class ActivityPrompt(Dialog):
     """
     Waits for an activity to be received.
 
-    This prompt requires a validator be passed in and is useful when waiting for non-message
-    activities like an event to be received. The validator can ignore received events until the
-    expected activity is received.
+        .. remarks::
+            This prompt requires a validator be passed in and is useful when waiting for non-message
+            activities like an event to be received. The validator can ignore received events until the
+            expected activity is received.
+
+    :var persisted_options:
+    :typevar persisted_options: str
+    :var persisted_state:
+    :vartype persisted_state: str
     """
 
     persisted_options = "options"
@@ -36,13 +41,12 @@ def __init__(
         self, dialog_id: str, validator: Callable[[PromptValidatorContext], bool]
     ):
         """
-        Initializes a new instance of the ActivityPrompt class.
-
-        Parameters:
-        ----------
-        dialog_id (str): Unique ID of the dialog within its parent DialogSet or ComponentDialog.
+        Initializes a new instance of the :class:`ActivityPrompt` class.
 
-        validator: Validator that will be called each time a new activity is received.
+        :param dialog_id: Unique ID of the dialog within its parent :class:`DialogSet` or :class:`ComponentDialog`.
+        :type dialog_id: str
+        :param validator: Validator that will be called each time a new activity is received.
+        :type validator: :class:`typing.Callable[[:class:`PromptValidatorContext`], bool]`
         """
         Dialog.__init__(self, dialog_id)
 
@@ -53,6 +57,16 @@ def __init__(
     async def begin_dialog(
         self, dialog_context: DialogContext, options: PromptOptions = None
     ) -> DialogTurnResult:
+        """
+        Called when a prompt dialog is pushed onto the dialog stack and is being activated.
+
+        :param dialog_context: The dialog context for the current turn of the conversation.
+        :type dialog_context: :class:`DialogContext`
+        :param options: Optional, additional information to pass to the prompt being started.
+        :type options: :class:`PromptOptions`
+        :return Dialog.end_of_turn:
+        :rtype Dialog.end_of_turn: :class:`Dialog.DialogTurnResult`
+        """
         if not dialog_context:
             raise TypeError("ActivityPrompt.begin_dialog(): dc cannot be None.")
         if not isinstance(options, PromptOptions):
@@ -83,6 +97,14 @@ async def begin_dialog(
         return Dialog.end_of_turn
 
     async def continue_dialog(self, dialog_context: DialogContext) -> DialogTurnResult:
+        """
+        Called when a prompt dialog is the active dialog and the user replied with a new activity.
+
+        :param dialog_context: The dialog context for the current turn of the conversation.
+        :type dialog_context: :class:`DialogContext`
+        :return Dialog.end_of_turn:
+        :rtype Dialog.end_of_turn: :class:`Dialog.DialogTurnResult`
+        """
         if not dialog_context:
             raise TypeError(
                 "ActivityPrompt.continue_dialog(): DialogContext cannot be None."
@@ -130,11 +152,22 @@ async def resume_dialog(  # pylint: disable=unused-argument
         self, dialog_context: DialogContext, reason: DialogReason, result: object = None
     ):
         """
-        Prompts are typically leaf nodes on the stack but the dev is free to push other dialogs
-        on top of the stack which will result in the prompt receiving an unexpected call to
-        resume_dialog() when the pushed on dialog ends.
-        To avoid the prompt prematurely ending, we need to implement this method and
-        simply re-prompt the user
+        Called when a prompt dialog resumes being the active dialog on the dialog stack, such
+        as when the previous active dialog on the stack completes.
+
+        .. remarks::
+            Prompts are typically leaf nodes on the stack but the dev is free to push other dialogs
+            on top of the stack which will result in the prompt receiving an unexpected call to
+            :meth:`ActivityPrompt.resume_dialog()` when the pushed on dialog ends.
+            To avoid the prompt prematurely ending, we need to implement this method and
+            simply re-prompt the user.
+
+        :param dialog_context: The dialog context for the current turn of the conversation
+        :type dialog_context: :class:`DialogContext`
+        :param reason: An enum indicating why the dialog resumed.
+        :type reason: :class:`DialogReason`
+        :param result: Optional, value returned from the previous dialog on the stack.
+        :type result: object
         """
         await self.reprompt_dialog(dialog_context.context, dialog_context.active_dialog)
 
@@ -155,15 +188,14 @@ async def on_prompt(
         """
         Called anytime the derived class should send the user a prompt.
 
-        Parameters:
-        ----------
-        context: Context for the current turn of conversation with the user.
-
-        state: Additional state being persisted for the prompt.
-
-        options: Options that the prompt started with in the call to `DialogContext.prompt()`.
-
-        isRetry: If `true` the users response wasn't recognized and the re-prompt should be sent.
+        :param dialog_context: The dialog context for the current turn of the conversation
+        :type dialog_context: :class:`DialogContext`
+        :param state: Additional state being persisted for the prompt.
+        :type state: :class:`typing.Dict[str, dict]`
+        :param options: Options that the prompt started with in the call to :meth:`DialogContext.prompt()`.
+        :type options: :class:`PromptOptions`
+        :param isRetry: If `true` the users response wasn't recognized and the re-prompt should be sent.
+        :type isRetry: bool
         """
         if is_retry and options.retry_prompt:
             options.retry_prompt.input_hint = InputHints.expecting_input
@@ -175,7 +207,18 @@ async def on_prompt(
     async def on_recognize(  # pylint: disable=unused-argument
         self, context: TurnContext, state: Dict[str, object], options: PromptOptions
     ) -> PromptRecognizerResult:
-
+        """
+        When overridden in a derived class, attempts to recognize the incoming activity.
+
+        :param context: Context for the current turn of conversation with the user.
+        :type context: :class:`botbuilder.core.TurnContext`
+        :param state: Contains state for the current instance of the prompt on the dialog stack.
+        :type state: :class:`typing.Dict[str, dict]`
+        :param options: A prompt options object
+        :type options: :class:`PromptOptions`
+        :return result: constructed from the options initially provided in the call to :meth:`async def on_prompt()`
+        :rtype result: :class:`PromptRecognizerResult`
+        """
         result = PromptRecognizerResult()
         result.succeeded = (True,)
         result.value = context.activity
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/attachment_prompt.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/attachment_prompt.py
index efac79c82..ab2cf1736 100644
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/attachment_prompt.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/attachment_prompt.py
@@ -3,10 +3,10 @@
 
 from typing import Callable, Dict
 
-from botbuilder.schema import ActivityTypes, Attachment, InputHints
+from botbuilder.schema import ActivityTypes, InputHints
 from botbuilder.core import TurnContext
 
-from .prompt import Prompt
+from .prompt import Prompt, PromptValidatorContext
 from .prompt_options import PromptOptions
 from .prompt_recognizer_result import PromptRecognizerResult
 
@@ -18,7 +18,9 @@ class AttachmentPrompt(Prompt):
     By default the prompt will return to the calling dialog an `[Attachment]`
     """
 
-    def __init__(self, dialog_id: str, validator: Callable[[Attachment], bool] = None):
+    def __init__(
+        self, dialog_id: str, validator: Callable[[PromptValidatorContext], bool] = None
+    ):
         super().__init__(dialog_id, validator)
 
     async def on_prompt(
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/choice_prompt.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/choice_prompt.py
index fdcd77bbc..93bf929dd 100644
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/choice_prompt.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/choice_prompt.py
@@ -121,7 +121,9 @@ async def on_prompt(
             if self.choice_options
             else ChoicePrompt._default_choice_options[culture]
         )
-        choice_style = options.style if options.style else self.style
+        choice_style = (
+            0 if options.style == 0 else options.style if options.style else self.style
+        )
 
         if is_retry and options.retry_prompt is not None:
             prompt = self.append_choices(
@@ -150,6 +152,8 @@ async def on_recognize(
         if turn_context.activity.type == ActivityTypes.message:
             activity: Activity = turn_context.activity
             utterance: str = activity.text
+            if not utterance:
+                return result
             opt: FindChoicesOptions = self.recognizer_options if self.recognizer_options else FindChoicesOptions()
             opt.locale = (
                 activity.locale
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/confirm_prompt.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/confirm_prompt.py
index f307e1f49..b5f902c50 100644
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/confirm_prompt.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/confirm_prompt.py
@@ -122,9 +122,11 @@ async def on_recognize(
         result = PromptRecognizerResult()
         if turn_context.activity.type == ActivityTypes.message:
             # Recognize utterance
-            message = turn_context.activity
+            utterance = turn_context.activity.text
+            if not utterance:
+                return result
             culture = self.determine_culture(turn_context.activity)
-            results = recognize_boolean(message.text, culture)
+            results = recognize_boolean(utterance, culture)
             if results:
                 first = results[0]
                 if "value" in first.resolution:
@@ -151,7 +153,7 @@ async def on_recognize(
                     )
                     choices = {confirm_choices[0], confirm_choices[1]}
                     second_attempt_results = ChoiceRecognizers.recognize_choices(
-                        message.text, choices
+                        utterance, choices
                     )
                     if second_attempt_results:
                         result.succeeded = True
@@ -165,6 +167,6 @@ def determine_culture(self, activity: Activity) -> str:
         )
         if not culture or culture not in self.choice_defaults:
             culture = (
-                "English"
-            )  # TODO: Fix to reference recognizer to use proper constants
+                "English"  # TODO: Fix to reference recognizer to use proper constants
+            )
         return culture
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/datetime_prompt.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/datetime_prompt.py
index 3eceeb184..907d81f7d 100644
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/datetime_prompt.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/datetime_prompt.py
@@ -50,11 +50,17 @@ async def on_recognize(
         result = PromptRecognizerResult()
         if turn_context.activity.type == ActivityTypes.message:
             # Recognize utterance
-            message = turn_context.activity
+            utterance = turn_context.activity.text
+            if not utterance:
+                return result
             # TODO: English constant needs to be ported.
-            culture = message.locale if message.locale is not None else "English"
+            culture = (
+                turn_context.activity.locale
+                if turn_context.activity.locale is not None
+                else "English"
+            )
 
-            results = recognize_datetime(message.text, culture)
+            results = recognize_datetime(utterance, culture)
             if results:
                 result.succeeded = True
                 result.value = []
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/number_prompt.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/number_prompt.py
index ed757c391..519ba39c9 100644
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/number_prompt.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/number_prompt.py
@@ -55,9 +55,11 @@ async def on_recognize(
 
         result = PromptRecognizerResult()
         if turn_context.activity.type == ActivityTypes.message:
-            message = turn_context.activity
+            utterance = turn_context.activity.text
+            if not utterance:
+                return result
             culture = self._get_culture(turn_context)
-            results: [ModelResult] = recognize_number(message.text, culture)
+            results: [ModelResult] = recognize_number(utterance, culture)
 
             if results:
                 result.succeeded = True
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py
index da709136b..c9d8bb5a9 100644
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py
@@ -3,15 +3,29 @@
 
 import re
 from datetime import datetime, timedelta
+from http import HTTPStatus
 from typing import Union, Awaitable, Callable
 
+from botframework.connector import Channels
+from botframework.connector.auth import (
+    ClaimsIdentity,
+    SkillValidation,
+    JwtTokenValidation,
+)
+from botframework.connector.token_api.models import SignInUrlResponse
 from botbuilder.core import (
     CardFactory,
+    ExtendedUserTokenProvider,
     MessageFactory,
     InvokeResponse,
     TurnContext,
+    BotAdapter,
+)
+from botbuilder.core.oauth import (
+    ConnectorClientBuilder,
     UserTokenProvider,
 )
+from botbuilder.core.bot_framework_adapter import TokenExchangeRequest
 from botbuilder.dialogs import Dialog, DialogContext, DialogTurnResult
 from botbuilder.schema import (
     Activity,
@@ -20,42 +34,58 @@
     CardAction,
     InputHints,
     SigninCard,
+    SignInConstants,
     OAuthCard,
     TokenResponse,
+    TokenExchangeInvokeRequest,
+    TokenExchangeInvokeResponse,
 )
-from botframework.connector import Channels
+
 from .prompt_options import PromptOptions
 from .oauth_prompt_settings import OAuthPromptSettings
 from .prompt_validator_context import PromptValidatorContext
 from .prompt_recognizer_result import PromptRecognizerResult
 
 
+class CallerInfo:
+    def __init__(self, caller_service_url: str = None, scope: str = None):
+        self.caller_service_url = caller_service_url
+        self.scope = scope
+
+
 class OAuthPrompt(Dialog):
+    PERSISTED_OPTIONS = "options"
+    PERSISTED_STATE = "state"
+    PERSISTED_EXPIRES = "expires"
+    PERSISTED_CALLER = "caller"
+
     """
-    Creates a new prompt that asks the user to sign in using the Bot Framework Single Sign On (SSO) service.
-    The prompt will attempt to retrieve the users current token and if the user isn't signed in, it
-    will send them an `OAuthCard` containing a button they can press to sign in. Depending on the channel,
-    the user will be sent through one of two possible sign-in flows:
-    - The automatic sign-in flow where once the user signs in, the SSO service will forward
-    the bot the users access token using either an `event` or `invoke` activity.
-    - The "magic code" flow where once the user signs in, they will be prompted by the SSO service
-    to send the bot a six digit code confirming their identity. This code will be sent as a
-    standard `message` activity.
-    Both flows are automatically supported by the `OAuthPrompt` and they only thing you need to be careful of
-    is that you don't block the `event` and `invoke` activities that the prompt might be waiting on.
-    Note:
-    You should avaoid persisting the access token with your bots other state. The Bot Frameworks SSO service
-    will securely store the token on your behalf. If you store it in your bots state,
-    it could expire or be revoked in between turns.
-    When calling the prompt from within a waterfall step, you should use the token within the step
-    following the prompt and then let the token go out of scope at the end of your function
-    Prompt Usage
-    When used with your bots `DialogSet`, you can simply add a new instance of the prompt as a named dialog using
-     `DialogSet.add()`.
-    You can then start the prompt from a waterfall step using either
-     `DialogContext.begin()` or `DialogContext.prompt()`.
-    The user will be prompted to sign in as needed and their access token will be passed as an argument to the callers
-     next waterfall step.
+    Creates a new prompt that asks the user to sign in, using the Bot Framework Single Sign On (SSO) service.
+
+    .. remarks::
+        The prompt will attempt to retrieve the users current token and if the user isn't signed in, it
+        will send them an `OAuthCard` containing a button they can press to sign in. Depending on the channel,
+        the user will be sent through one of two possible sign-in flows:
+        - The automatic sign-in flow where once the user signs in, the SSO service will forward
+        the bot the users access token using either an `event` or `invoke` activity.
+        - The "magic code" flow where once the user signs in, they will be prompted by the SSO service
+        to send the bot a six digit code confirming their identity. This code will be sent as a
+        standard `message` activity.
+        Both flows are automatically supported by the `OAuthPrompt` and they only thing you need to be careful of
+        is that you don't block the `event` and `invoke` activities that the prompt might be waiting on.
+
+        You should avoid persisting the access token with your bots other state. The Bot Frameworks SSO service
+        will securely store the token on your behalf. If you store it in your bots state,
+        it could expire or be revoked in between turns.
+        When calling the prompt from within a waterfall step, you should use the token within the step
+        following the prompt and then let the token go out of scope at the end of your function.
+
+        When used with your bots :class:`DialogSet`, you can simply add a new instance of the prompt as a named
+        dialog using :meth`DialogSet.add()`.
+        You can then start the prompt from a waterfall step using either :meth:`DialogContext.begin()` or
+        :meth:`DialogContext.prompt()`.
+        The user will be prompted to sign in as needed and their access token will be passed as an argument to
+        the callers next waterfall step.
     """
 
     def __init__(
@@ -64,6 +94,20 @@ def __init__(
         settings: OAuthPromptSettings,
         validator: Callable[[PromptValidatorContext], Awaitable[bool]] = None,
     ):
+        """
+        Creates a new instance of the :class:`OAuthPrompt` class.
+
+        :param dialog_id: The Id to assign to this prompt.
+        :type dialog_id: str
+        :param settings: Additional authentication settings to use with this instance of the prompt
+        :type settings: :class:`OAuthPromptSettings`
+        :param validator: Optional, contains additional, custom validation for this prompt
+        :type validator: :class:`PromptValidatorContext`
+
+        .. remarks::
+            The value of :param dialogId: must be unique within the :class:`DialogSet`or :class:`ComponentDialog`
+            to which the prompt is added.
+        """
         super().__init__(dialog_id)
         self._validator = validator
 
@@ -78,9 +122,26 @@ def __init__(
     async def begin_dialog(
         self, dialog_context: DialogContext, options: PromptOptions = None
     ) -> DialogTurnResult:
+        """
+        Starts an authentication prompt dialog. Called when an authentication prompt dialog is pushed onto the
+        dialog stack and is being activated.
+
+        :param dialog_context: The dialog context for the current turn of the conversation
+        :type dialog_context:  :class:`DialogContext`
+        :param options: Optional, additional information to pass to the prompt being started
+        :type options: :class:`PromptOptions`
+
+        :return: Dialog turn result
+        :rtype: :class`:`DialogTurnResult`
+
+        .. remarks::
+
+            If the task is successful, the result indicates whether the prompt is still active after the turn
+            has been processed.
+        """
         if dialog_context is None:
             raise TypeError(
-                f"OAuthPrompt.begin_dialog: Expected DialogContext but got NoneType instead"
+                f"OAuthPrompt.begin_dialog(): Expected DialogContext but got NoneType instead"
             )
 
         options = options or PromptOptions()
@@ -99,39 +160,73 @@ async def begin_dialog(
             else 900000
         )
         state = dialog_context.active_dialog.state
-        state["state"] = {}
-        state["options"] = options
-        state["expires"] = datetime.now() + timedelta(seconds=timeout / 1000)
+        state[OAuthPrompt.PERSISTED_STATE] = {}
+        state[OAuthPrompt.PERSISTED_OPTIONS] = options
+        state[OAuthPrompt.PERSISTED_EXPIRES] = datetime.now() + timedelta(
+            seconds=timeout / 1000
+        )
+        state[OAuthPrompt.PERSISTED_CALLER] = OAuthPrompt.__create_caller_info(
+            dialog_context.context
+        )
 
         if not isinstance(dialog_context.context.adapter, UserTokenProvider):
             raise TypeError(
-                "OAuthPrompt.get_user_token(): not supported by the current adapter"
+                "OAuthPrompt.begin_dialog(): not supported by the current adapter"
             )
 
         output = await dialog_context.context.adapter.get_user_token(
-            dialog_context.context, self._settings.connection_name, None
+            dialog_context.context,
+            self._settings.connection_name,
+            None,
+            self._settings.oath_app_credentials,
         )
 
         if output is not None:
             return await dialog_context.end_dialog(output)
 
-        await self.send_oauth_card(dialog_context.context, options.prompt)
+        await self._send_oauth_card(dialog_context.context, options.prompt)
         return Dialog.end_of_turn
 
     async def continue_dialog(self, dialog_context: DialogContext) -> DialogTurnResult:
-        # Recognize token
-        recognized = await self._recognize_token(dialog_context.context)
+        """
+        Continues a dialog. Called when a prompt dialog is the active dialog and the user replied with a new activity.
+
+        :param dialog_context: The dialog context for the current turn of the conversation
+        :type dialog_context:  :class:`DialogContext`
 
+        :return: Dialog turn result
+        :rtype: :class:`DialogTurnResult`
+
+        .. remarks::
+            If the task is successful, the result indicates whether the dialog is still
+            active after the turn has been processed by the dialog.
+            The prompt generally continues to receive the user's replies until it accepts the
+            user's reply as valid input for the prompt.
+        """
         # Check for timeout
         state = dialog_context.active_dialog.state
         is_message = dialog_context.context.activity.type == ActivityTypes.message
-        has_timed_out = is_message and (datetime.now() > state["expires"])
+        is_timeout_activity_type = (
+            is_message
+            or OAuthPrompt._is_token_response_event(dialog_context.context)
+            or OAuthPrompt._is_teams_verification_invoke(dialog_context.context)
+            or OAuthPrompt._is_token_exchange_request_invoke(dialog_context.context)
+        )
+
+        has_timed_out = is_timeout_activity_type and (
+            datetime.now() > state[OAuthPrompt.PERSISTED_EXPIRES]
+        )
 
         if has_timed_out:
             return await dialog_context.end_dialog(None)
 
         if state["state"].get("attemptCount") is None:
             state["state"]["attemptCount"] = 1
+        else:
+            state["state"]["attemptCount"] += 1
+
+        # Recognize token
+        recognized = await self._recognize_token(dialog_context)
 
         # Validate the return value
         is_valid = False
@@ -140,9 +235,8 @@ async def continue_dialog(self, dialog_context: DialogContext) -> DialogTurnResu
                 PromptValidatorContext(
                     dialog_context.context,
                     recognized,
-                    state["state"],
-                    state["options"],
-                    state["state"]["attemptCount"],
+                    state[OAuthPrompt.PERSISTED_STATE],
+                    state[OAuthPrompt.PERSISTED_OPTIONS],
                 )
             )
         elif recognized.succeeded:
@@ -151,20 +245,40 @@ async def continue_dialog(self, dialog_context: DialogContext) -> DialogTurnResu
         # Return recognized value or re-prompt
         if is_valid:
             return await dialog_context.end_dialog(recognized.value)
+        if is_message and self._settings.end_on_invalid_message:
+            # If EndOnInvalidMessage is set, complete the prompt with no result.
+            return await dialog_context.end_dialog(None)
 
         # Send retry prompt
         if (
             not dialog_context.context.responded
             and is_message
-            and state["options"].retry_prompt is not None
+            and state[OAuthPrompt.PERSISTED_OPTIONS].retry_prompt is not None
         ):
-            await dialog_context.context.send_activity(state["options"].retry_prompt)
+            await dialog_context.context.send_activity(
+                state[OAuthPrompt.PERSISTED_OPTIONS].retry_prompt
+            )
 
         return Dialog.end_of_turn
 
     async def get_user_token(
         self, context: TurnContext, code: str = None
     ) -> TokenResponse:
+        """
+        Gets the user's tokeN.
+
+        :param context: Context for the current turn of conversation with the user
+        :type context:  :class:`TurnContext`
+        :param code: (Optional) Optional user entered code to validate.
+        :type code: str
+
+        :return: A response that includes the user's token
+        :rtype: :class:`TokenResponse`
+
+        .. remarks::
+            If the task is successful and the user already has a token or the user successfully signs in,
+            the result contains the user's token.
+        """
         adapter = context.adapter
 
         # Validate adapter type
@@ -174,10 +288,24 @@ async def get_user_token(
             )
 
         return await adapter.get_user_token(
-            context, self._settings.connection_name, code
+            context,
+            self._settings.connection_name,
+            code,
+            self._settings.oath_app_credentials,
         )
 
     async def sign_out_user(self, context: TurnContext):
+        """
+        Signs out the user
+
+        :param context: Context for the current turn of conversation with the user
+        :type context:  :class:`TurnContext`
+        :return: A task representing the work queued to execute
+
+        .. remarks::
+            If the task is successful and the user already has a token or the user successfully signs in,
+            the result contains the user's token.
+        """
         adapter = context.adapter
 
         # Validate adapter type
@@ -186,9 +314,25 @@ async def sign_out_user(self, context: TurnContext):
                 "OAuthPrompt.sign_out_user(): not supported for the current adapter."
             )
 
-        return await adapter.sign_out_user(context, self._settings.connection_name)
+        return await adapter.sign_out_user(
+            context,
+            self._settings.connection_name,
+            None,
+            self._settings.oath_app_credentials,
+        )
 
-    async def send_oauth_card(
+    @staticmethod
+    def __create_caller_info(context: TurnContext) -> CallerInfo:
+        bot_identity = context.turn_state.get(BotAdapter.BOT_IDENTITY_KEY)
+        if bot_identity and SkillValidation.is_skill_claim(bot_identity.claims):
+            return CallerInfo(
+                caller_service_url=context.activity.service_url,
+                scope=JwtTokenValidation.get_app_id_from_claims(bot_identity.claims),
+            )
+
+        return None
+
+    async def _send_oauth_card(
         self, context: TurnContext, prompt: Union[Activity, str] = None
     ):
         if not isinstance(prompt, Activity):
@@ -198,11 +342,46 @@ async def send_oauth_card(
 
         prompt.attachments = prompt.attachments or []
 
-        if self._channel_suppports_oauth_card(context.activity.channel_id):
+        if OAuthPrompt._channel_suppports_oauth_card(context.activity.channel_id):
             if not any(
                 att.content_type == CardFactory.content_types.oauth_card
                 for att in prompt.attachments
             ):
+                adapter: ExtendedUserTokenProvider = context.adapter
+                card_action_type = ActionTypes.signin
+                sign_in_resource: SignInUrlResponse = await adapter.get_sign_in_resource_from_user_and_credentials(
+                    context,
+                    self._settings.oath_app_credentials,
+                    self._settings.connection_name,
+                    context.activity.from_property.id,
+                )
+                link = sign_in_resource.sign_in_link
+                bot_identity: ClaimsIdentity = context.turn_state.get(
+                    BotAdapter.BOT_IDENTITY_KEY
+                )
+
+                # use the SignInLink when in speech channel or bot is a skill or
+                # an extra OAuthAppCredentials is being passed in
+                if (
+                    (
+                        bot_identity
+                        and SkillValidation.is_skill_claim(bot_identity.claims)
+                    )
+                    or not context.activity.service_url.startswith("http")
+                    or self._settings.oath_app_credentials
+                ):
+                    if context.activity.channel_id == Channels.emulator:
+                        card_action_type = ActionTypes.open_url
+                elif not OAuthPrompt._channel_requires_sign_in_link(
+                    context.activity.channel_id
+                ):
+                    link = None
+
+                json_token_ex_resource = (
+                    sign_in_resource.token_exchange_resource.as_dict()
+                    if sign_in_resource.token_exchange_resource
+                    else None
+                )
                 prompt.attachments.append(
                     CardFactory.oauth_card(
                         OAuthCard(
@@ -212,9 +391,11 @@ async def send_oauth_card(
                                 CardAction(
                                     title=self._settings.title,
                                     text=self._settings.text,
-                                    type=ActionTypes.signin,
+                                    type=card_action_type,
+                                    value=link,
                                 )
                             ],
+                            token_exchange_resource=json_token_ex_resource,
                         )
                     )
                 )
@@ -225,11 +406,14 @@ async def send_oauth_card(
             ):
                 if not hasattr(context.adapter, "get_oauth_sign_in_link"):
                     raise Exception(
-                        "OAuthPrompt.send_oauth_card(): get_oauth_sign_in_link() not supported by the current adapter"
+                        "OAuthPrompt._send_oauth_card(): get_oauth_sign_in_link() not supported by the current adapter"
                     )
 
                 link = await context.adapter.get_oauth_sign_in_link(
-                    context, self._settings.connection_name
+                    context,
+                    self._settings.connection_name,
+                    None,
+                    self._settings.oath_app_credentials,
                 )
                 prompt.attachments.append(
                     CardFactory.signin_card(
@@ -249,26 +433,147 @@ async def send_oauth_card(
         # Send prompt
         await context.send_activity(prompt)
 
-    async def _recognize_token(self, context: TurnContext) -> PromptRecognizerResult:
+    async def _recognize_token(
+        self, dialog_context: DialogContext
+    ) -> PromptRecognizerResult:
+        context = dialog_context.context
         token = None
-        if self._is_token_response_event(context):
+        if OAuthPrompt._is_token_response_event(context):
             token = context.activity.value
-        elif self._is_teams_verification_invoke(context):
-            code = context.activity.value.state
+
+            # fixup the turnContext's state context if this was received from a skill host caller
+            state: CallerInfo = dialog_context.active_dialog.state[
+                OAuthPrompt.PERSISTED_CALLER
+            ]
+            if state:
+                # set the ServiceUrl to the skill host's Url
+                dialog_context.context.activity.service_url = state.caller_service_url
+
+                # recreate a ConnectorClient and set it in TurnState so replies use the correct one
+                if not isinstance(context.adapter, ConnectorClientBuilder):
+                    raise TypeError(
+                        "OAuthPrompt: IConnectorClientProvider interface not implemented by the current adapter"
+                    )
+
+                connector_client_builder: ConnectorClientBuilder = context.adapter
+                claims_identity = context.turn_state.get(BotAdapter.BOT_IDENTITY_KEY)
+                connector_client = await connector_client_builder.create_connector_client(
+                    dialog_context.context.activity.service_url,
+                    claims_identity,
+                    state.scope,
+                )
+
+                context.turn_state[
+                    BotAdapter.BOT_CONNECTOR_CLIENT_KEY
+                ] = connector_client
+
+        elif OAuthPrompt._is_teams_verification_invoke(context):
+            code = context.activity.value["state"]
             try:
                 token = await self.get_user_token(context, code)
                 if token is not None:
                     await context.send_activity(
-                        Activity(type="invokeResponse", value=InvokeResponse(200))
+                        Activity(
+                            type="invokeResponse",
+                            value=InvokeResponse(int(HTTPStatus.OK)),
+                        )
                     )
                 else:
                     await context.send_activity(
-                        Activity(type="invokeResponse", value=InvokeResponse(404))
+                        Activity(
+                            type="invokeResponse",
+                            value=InvokeResponse(int(HTTPStatus.NOT_FOUND)),
+                        )
                     )
             except Exception:
-                context.send_activity(
-                    Activity(type="invokeResponse", value=InvokeResponse(500))
+                await context.send_activity(
+                    Activity(
+                        type="invokeResponse",
+                        value=InvokeResponse(int(HTTPStatus.INTERNAL_SERVER_ERROR)),
+                    )
+                )
+        elif self._is_token_exchange_request_invoke(context):
+            if isinstance(context.activity.value, dict):
+                context.activity.value = TokenExchangeInvokeRequest().from_dict(
+                    context.activity.value
                 )
+
+            if not (
+                context.activity.value
+                and self._is_token_exchange_request(context.activity.value)
+            ):
+                # Received activity is not a token exchange request.
+                await context.send_activity(
+                    self._get_token_exchange_invoke_response(
+                        int(HTTPStatus.BAD_REQUEST),
+                        "The bot received an InvokeActivity that is missing a TokenExchangeInvokeRequest value."
+                        " This is required to be sent with the InvokeActivity.",
+                    )
+                )
+            elif (
+                context.activity.value.connection_name != self._settings.connection_name
+            ):
+                # Connection name on activity does not match that of setting.
+                await context.send_activity(
+                    self._get_token_exchange_invoke_response(
+                        int(HTTPStatus.BAD_REQUEST),
+                        "The bot received an InvokeActivity with a TokenExchangeInvokeRequest containing a"
+                        " ConnectionName that does not match the ConnectionName expected by the bots active"
+                        " OAuthPrompt. Ensure these names match when sending the InvokeActivityInvalid"
+                        " ConnectionName in the TokenExchangeInvokeRequest",
+                    )
+                )
+            elif not getattr(context.adapter, "exchange_token"):
+                # Token Exchange not supported in the adapter.
+                await context.send_activity(
+                    self._get_token_exchange_invoke_response(
+                        int(HTTPStatus.BAD_GATEWAY),
+                        "The bot's BotAdapter does not support token exchange operations."
+                        " Ensure the bot's Adapter supports the ExtendedUserTokenProvider interface.",
+                    )
+                )
+
+                raise AttributeError(
+                    "OAuthPrompt._recognize_token(): not supported by the current adapter."
+                )
+            else:
+                # No errors. Proceed with token exchange.
+                extended_user_token_provider: ExtendedUserTokenProvider = context.adapter
+
+                token_exchange_response = None
+                try:
+                    token_exchange_response = await extended_user_token_provider.exchange_token_from_credentials(
+                        context,
+                        self._settings.oath_app_credentials,
+                        self._settings.connection_name,
+                        context.activity.from_property.id,
+                        TokenExchangeRequest(token=context.activity.value.token),
+                    )
+                except:
+                    # Ignore Exceptions
+                    # If token exchange failed for any reason, tokenExchangeResponse above stays null, and
+                    # hence we send back a failure invoke response to the caller.
+                    pass
+
+                if not token_exchange_response or not token_exchange_response.token:
+                    await context.send_activity(
+                        self._get_token_exchange_invoke_response(
+                            int(HTTPStatus.PRECONDITION_FAILED),
+                            "The bot is unable to exchange token. Proceed with regular login.",
+                        )
+                    )
+                else:
+                    await context.send_activity(
+                        self._get_token_exchange_invoke_response(
+                            int(HTTPStatus.OK), None, context.activity.value.id
+                        )
+                    )
+                    token = TokenResponse(
+                        channel_id=token_exchange_response.channel_id,
+                        connection_name=token_exchange_response.connection_name,
+                        token=token_exchange_response.token,
+                        expiration=None,
+                    )
         elif context.activity.type == ActivityTypes.message and context.activity.text:
             match = re.match(r"(? PromptRecognizerResult
             else PromptRecognizerResult()
         )
 
-    def _is_token_response_event(self, context: TurnContext) -> bool:
+    def _get_token_exchange_invoke_response(
+        self, status: int, failure_detail: str, identifier: str = None
+    ) -> Activity:
+        return Activity(
+            type=ActivityTypes.invoke_response,
+            value=InvokeResponse(
+                status=status,
+                body=TokenExchangeInvokeResponse(
+                    id=identifier,
+                    connection_name=self._settings.connection_name,
+                    failure_detail=failure_detail,
+                ),
+            ),
+        )
+
+    @staticmethod
+    def _is_token_response_event(context: TurnContext) -> bool:
         activity = context.activity
 
         return (
-            activity.type == ActivityTypes.event and activity.name == "tokens/response"
+            activity.type == ActivityTypes.event
+            and activity.name == SignInConstants.token_response_event_name
         )
 
-    def _is_teams_verification_invoke(self, context: TurnContext) -> bool:
+    @staticmethod
+    def _is_teams_verification_invoke(context: TurnContext) -> bool:
         activity = context.activity
 
         return (
             activity.type == ActivityTypes.invoke
-            and activity.name == "signin/verifyState"
+            and activity.name == SignInConstants.verify_state_operation_name
         )
 
-    def _channel_suppports_oauth_card(self, channel_id: str) -> bool:
+    @staticmethod
+    def _channel_suppports_oauth_card(channel_id: str) -> bool:
         if channel_id in [
-            Channels.ms_teams,
             Channels.cortana,
             Channels.skype,
             Channels.skype_for_business,
@@ -305,3 +628,23 @@ def _channel_suppports_oauth_card(self, channel_id: str) -> bool:
             return False
 
         return True
+
+    @staticmethod
+    def _channel_requires_sign_in_link(channel_id: str) -> bool:
+        if channel_id in [Channels.ms_teams]:
+            return True
+
+        return False
+
+    @staticmethod
+    def _is_token_exchange_request_invoke(context: TurnContext) -> bool:
+        activity = context.activity
+
+        return (
+            activity.type == ActivityTypes.invoke
+            and activity.name == SignInConstants.token_exchange_operation_name
+        )
+
+    @staticmethod
+    def _is_token_exchange_request(obj: TokenExchangeInvokeRequest) -> bool:
+        return bool(obj.connection_name) and bool(obj.token)
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt_settings.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt_settings.py
index 4eec4881a..c071c590e 100644
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt_settings.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt_settings.py
@@ -1,21 +1,37 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-
-class OAuthPromptSettings:
-    def __init__(
-        self, connection_name: str, title: str, text: str = None, timeout: int = None
-    ):
-        """
-        Settings used to configure an `OAuthPrompt` instance.
-         Parameters:
-            connection_name (str): Name of the OAuth connection being used.
-            title (str): The title of the cards signin button.
-            text (str): (Optional) additional text included on the signin card.
-            timeout (int): (Optional) number of milliseconds the prompt will wait for the user to authenticate.
-                `OAuthPrompt` defaults value to `900,000` ms (15 minutes).
-        """
-        self.connection_name = connection_name
-        self.title = title
-        self.text = text
-        self.timeout = timeout
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+from botframework.connector.auth import AppCredentials
+
+
+class OAuthPromptSettings:
+    def __init__(
+        self,
+        connection_name: str,
+        title: str,
+        text: str = None,
+        timeout: int = None,
+        oauth_app_credentials: AppCredentials = None,
+        end_on_invalid_message: bool = False,
+    ):
+        """
+        Settings used to configure an `OAuthPrompt` instance.
+         Parameters:
+            connection_name (str): Name of the OAuth connection being used.
+            title (str): The title of the cards signin button.
+            text (str): (Optional) additional text included on the signin card.
+            timeout (int): (Optional) number of milliseconds the prompt will wait for the user to authenticate.
+                `OAuthPrompt` defaults value to `900,000` ms (15 minutes).
+            oauth_app_credentials (AppCredentials): (Optional) AppCredentials to use for OAuth.  If None,
+            the Bots credentials are used.
+            end_on_invalid_message (bool): (Optional) value indicating whether the OAuthPrompt should end upon
+            receiving an invalid message.  Generally the OAuthPrompt will ignore incoming messages from the
+            user during the auth flow, if they are not related to the auth flow.  This flag enables ending the
+            OAuthPrompt rather than ignoring the user's message.  Typically, this flag will be set to 'true',
+            but is 'false' by default for backwards compatibility.
+        """
+        self.connection_name = connection_name
+        self.title = title
+        self.text = text
+        self.timeout = timeout
+        self.oath_app_credentials = oauth_app_credentials
+        self.end_on_invalid_message = end_on_invalid_message
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/prompt.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/prompt.py
index 0ab60ba17..cf0a4123d 100644
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/prompt.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/prompt.py
@@ -23,22 +23,33 @@
 
 
 class Prompt(Dialog):
-    """ Base class for all prompts."""
+    """
+
+    Defines the core behavior of prompt dialogs. Extends the :class:`Dialog` base class.
+
+    .. remarks::
+        When the prompt ends, it returns an object that represents the value it was prompted for.
+        Use :meth:`DialogSet.add()` or :meth:`ComponentDialog.add_dialog()` to add a prompt to
+        a dialog set or component dialog, respectively.
+
+        Use :meth:`DialogContext.prompt()` or :meth:`DialogContext.begin_dialog()` to start the prompt.
+        If you start a prompt from a :class:`WaterfallStep` in a :class:`WaterfallDialog`, then the
+        prompt result will be available in the next step of the waterfall.
+    """
 
     ATTEMPT_COUNT_KEY = "AttemptCount"
     persisted_options = "options"
     persisted_state = "state"
 
     def __init__(self, dialog_id: str, validator: object = None):
-        """Creates a new Prompt instance.
-        Parameters
-        ----------
-        dialog_id
-            Unique ID of the prompt within its parent `DialogSet` or
-            `ComponentDialog`.
-        validator
-            (Optional) custom validator used to provide additional validation and
-            re-prompting logic for the prompt.
+        """
+        Creates a new :class:`Prompt` instance.
+
+        :param dialog_id: Unique Id of the prompt within its parent :class:`DialogSet`
+        :class:`ComponentDialog`
+        :type dialog_id: str
+        :param validator: Optionally provide additional validation and re-prompting logic
+        :type validator: Object
         """
         super(Prompt, self).__init__(dialog_id)
 
@@ -47,6 +58,19 @@ def __init__(self, dialog_id: str, validator: object = None):
     async def begin_dialog(
         self, dialog_context: DialogContext, options: object = None
     ) -> DialogTurnResult:
+        """
+        Starts a prompt dialog. Called when a prompt dialog is pushed onto the dialog stack and is being activated.
+
+        :param dialog_context: The dialog context for the current turn of the conversation
+        :type dialog_context:  :class:`DialogContext`
+        :param options: Optional, additional information to pass to the prompt being started
+        :type options: Object
+        :return: The dialog turn result
+        :rtype: :class:`DialogTurnResult`
+
+        .. note::
+            The result indicates whether the prompt is still active after the turn has been processed.
+        """
         if not dialog_context:
             raise TypeError("Prompt(): dc cannot be None.")
         if not isinstance(options, PromptOptions):
@@ -74,6 +98,23 @@ async def begin_dialog(
         return Dialog.end_of_turn
 
     async def continue_dialog(self, dialog_context: DialogContext):
+        """
+        Continues a dialog.
+
+        :param dialog_context: The dialog context for the current turn of the conversation
+        :type dialog_context:  :class:`DialogContext`
+        :return: The dialog turn result
+        :rtype: :class:`DialogTurnResult`
+
+        .. remarks::
+            Called when a prompt dialog is the active dialog and the user replied with a new activity.
+
+            If the task is successful, the result indicates whether the dialog is still active after
+            the turn has been processed by the dialog.
+
+            The prompt generally continues to receive the user's replies until it accepts the
+            user's reply as valid input for the prompt.
+        """
         if not dialog_context:
             raise TypeError("Prompt(): dc cannot be None.")
 
@@ -111,15 +152,46 @@ async def continue_dialog(self, dialog_context: DialogContext):
     async def resume_dialog(
         self, dialog_context: DialogContext, reason: DialogReason, result: object
     ) -> DialogTurnResult:
-        # Prompts are typically leaf nodes on the stack but the dev is free to push other dialogs
-        # on top of the stack which will result in the prompt receiving an unexpected call to
-        # dialog_resume() when the pushed on dialog ends.
-        # To avoid the prompt prematurely ending we need to implement this method and
-        # simply re-prompt the user.
+        """
+        Resumes a dialog.
+
+        :param dialog_context: The dialog context for the current turn of the conversation.
+        :type dialog_context:  :class:`DialogContext`
+        :param reason: An enum indicating why the dialog resumed.
+        :type reason:  :class:`DialogReason`
+        :param result: Optional, value returned from the previous dialog on the stack.
+        :type result:  object
+        :return: The dialog turn result
+        :rtype: :class:`DialogTurnResult`
+
+        .. remarks::
+            Called when a prompt dialog resumes being the active dialog on the dialog stack,
+            such as when the previous active dialog on the stack completes.
+
+            If the task is successful, the result indicates whether the dialog is still
+            active after the turn has been processed by the dialog.
+
+            Prompts are typically leaf nodes on the stack but the dev is free to push other dialogs
+            on top of the stack which will result in the prompt receiving an unexpected call to
+            :meth:resume_dialog() when the pushed on dialog ends.
+
+            Simply re-prompt the user to avoid that the prompt ends prematurely.
+
+        """
         await self.reprompt_dialog(dialog_context.context, dialog_context.active_dialog)
         return Dialog.end_of_turn
 
     async def reprompt_dialog(self, context: TurnContext, instance: DialogInstance):
+        """
+        Reprompts user for input.
+
+        :param context: Context for the current turn of conversation with the user
+        :type context:  :class:`botbuilder.core.TurnContext`
+        :param instance: The instance of the dialog on the stack
+        :type instance:  :class:`DialogInstance`
+        :return: A task representing the asynchronous operation
+
+        """
         state = instance.state[self.persisted_state]
         options = instance.state[self.persisted_options]
         await self.on_prompt(context, state, options, False)
@@ -132,7 +204,21 @@ async def on_prompt(
         options: PromptOptions,
         is_retry: bool,
     ):
-        pass
+        """
+        Prompts user for input. When overridden in a derived class, prompts the user for input.
+
+        :param turn_context: Context for the current turn of conversation with the user
+        :type turn_context:  :class:`botbuilder.core.TurnContext`
+        :param state: Contains state for the current instance of the prompt on the dialog stack
+        :type state:  :class:`Dict`
+        :param options: A prompt options object constructed from:meth:`DialogContext.prompt()`
+        :type options:  :class:`PromptOptions`
+        :param is_retry: Determines whether `prompt` or `retry_prompt` should be used
+        :type is_retry:  bool
+
+        :return: A task representing the asynchronous operation.
+
+        """
 
     @abstractmethod
     async def on_recognize(
@@ -141,7 +227,21 @@ async def on_recognize(
         state: Dict[str, object],
         options: PromptOptions,
     ):
-        pass
+        """
+        Recognizes the user's input.
+
+        :param turn_context: Context for the current turn of conversation with the user
+        :type turn_context:  :class:`botbuilder.core.TurnContext`
+        :param state: Contains state for the current instance of the prompt on the dialog stack
+        :type state:  :class:`Dict`
+        :param options: A prompt options object constructed from :meth:`DialogContext.prompt()`
+        :type options:  :class:`PromptOptions`
+
+        :return: A task representing the asynchronous operation.
+
+        .. note::
+            When overridden in a derived class, attempts to recognize the user's input.
+        """
 
     def append_choices(
         self,
@@ -152,19 +252,27 @@ def append_choices(
         options: ChoiceFactoryOptions = None,
     ) -> Activity:
         """
-        Helper function to compose an output activity containing a set of choices.
-
-        Parameters:
-        -----------
-        prompt: The prompt to append the user's choice to.
-
-        channel_id: ID of the channel the prompt is being sent to.
-
-        choices: List of choices to append.
-
-        style: Configured style for the list of choices.
+        Composes an output activity containing a set of choices.
+
+        :param prompt: The prompt to append the user's choice to
+        :type prompt:
+        :param channel_id: Id of the channel the prompt is being sent to
+        :type channel_id: str
+        :param: choices: List of choices to append
+        :type choices:  :class:`List`
+        :param: style: Configured style for the list of choices
+        :type style:  :class:`ListStyle`
+        :param: options: Optional formatting options to use when presenting the choices
+        :type style: :class:`ChoiceFactoryOptions`
+
+        :return: A task representing the asynchronous operation
+
+        .. remarks::
+            If the task is successful, the result contains the updated activity.
+            When overridden in a derived class, appends choices to the activity when the user
+            is prompted for input. This is an helper function to compose an output activity
+            containing a set of choices.
 
-        options: (Optional) options to configure the underlying `ChoiceFactory` call.
         """
         # Get base prompt text (if any)
         text = prompt.text if prompt is not None and prompt.text else ""
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/prompt_options.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/prompt_options.py
index 8d9801424..c341a4b52 100644
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/prompt_options.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/prompt_options.py
@@ -8,6 +8,10 @@
 
 
 class PromptOptions:
+    """
+    Contains settings to pass to a :class:`Prompt` object when the prompt is started.
+    """
+
     def __init__(
         self,
         prompt: Activity = None,
@@ -17,6 +21,23 @@ def __init__(
         validations: object = None,
         number_of_attempts: int = 0,
     ):
+        """
+        Sets the initial prompt to send to the user as an :class:`botbuilder.schema.Activity`.
+
+        :param prompt: The initial prompt to send to the user
+        :type prompt: :class:`botbuilder.schema.Activity`
+        :param retry_prompt: The retry prompt to send to the user
+        :type retry_prompt: :class:`botbuilder.schema.Activity`
+        :param choices: The choices to send to the user
+        :type choices: :class:`List`
+        :param style: The style of the list of choices to send to the user
+        :type style: :class:`ListStyle`
+        :param validations: The prompt validations
+        :type validations: :class:`Object`
+        :param number_of_attempts: The number of attempts allowed
+        :type number_of_attempts: :class:`int`
+
+        """
         self.prompt = prompt
         self.retry_prompt = retry_prompt
         self.choices = choices
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/__init__.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/__init__.py
new file mode 100644
index 000000000..9a804f378
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/__init__.py
@@ -0,0 +1,17 @@
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+# --------------------------------------------------------------------------
+
+from .begin_skill_dialog_options import BeginSkillDialogOptions
+from .skill_dialog_options import SkillDialogOptions
+from .skill_dialog import SkillDialog
+
+
+__all__ = [
+    "BeginSkillDialogOptions",
+    "SkillDialogOptions",
+    "SkillDialog",
+]
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/begin_skill_dialog_options.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/begin_skill_dialog_options.py
new file mode 100644
index 000000000..a9d21ca3f
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/begin_skill_dialog_options.py
@@ -0,0 +1,17 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.schema import Activity
+
+
+class BeginSkillDialogOptions:
+    def __init__(self, activity: Activity):
+        self.activity = activity
+
+    @staticmethod
+    def from_object(obj: object) -> "BeginSkillDialogOptions":
+        if isinstance(obj, dict) and "activity" in obj:
+            return BeginSkillDialogOptions(obj["activity"])
+        if hasattr(obj, "activity"):
+            return BeginSkillDialogOptions(obj.activity)
+        return None
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py
new file mode 100644
index 000000000..119d1d62a
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py
@@ -0,0 +1,368 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from copy import deepcopy
+from typing import List
+
+from botframework.connector.token_api.models import TokenExchangeRequest
+from botbuilder.schema import (
+    Activity,
+    ActivityTypes,
+    ExpectedReplies,
+    DeliveryModes,
+    SignInConstants,
+    TokenExchangeInvokeRequest,
+)
+from botbuilder.core import BotAdapter, TurnContext, ExtendedUserTokenProvider
+from botbuilder.core.card_factory import ContentTypes
+from botbuilder.core.skills import SkillConversationIdFactoryOptions
+from botbuilder.dialogs import (
+    Dialog,
+    DialogContext,
+    DialogEvents,
+    DialogReason,
+    DialogInstance,
+)
+
+from .begin_skill_dialog_options import BeginSkillDialogOptions
+from .skill_dialog_options import SkillDialogOptions
+
+
+class SkillDialog(Dialog):
+    SKILLCONVERSATIONIDSTATEKEY = (
+        "Microsoft.Bot.Builder.Dialogs.SkillDialog.SkillConversationId"
+    )
+
+    def __init__(self, dialog_options: SkillDialogOptions, dialog_id: str):
+        super().__init__(dialog_id)
+        if not dialog_options:
+            raise TypeError("SkillDialog.__init__(): dialog_options cannot be None.")
+
+        self.dialog_options = dialog_options
+        self._deliver_mode_state_key = "deliverymode"
+
+    async def begin_dialog(self, dialog_context: DialogContext, options: object = None):
+        """
+        Method called when a new dialog has been pushed onto the stack and is being activated.
+        :param dialog_context: The dialog context for the current turn of conversation.
+        :param options: (Optional) additional argument(s) to pass to the dialog being started.
+        """
+        dialog_args = self._validate_begin_dialog_args(options)
+
+        await dialog_context.context.send_trace_activity(
+            f"{SkillDialog.__name__}.BeginDialogAsync()",
+            label=f"Using activity of type: {dialog_args.activity.type}",
+        )
+
+        # Create deep clone of the original activity to avoid altering it before forwarding it.
+        skill_activity: Activity = deepcopy(dialog_args.activity)
+
+        # Apply conversation reference and common properties from incoming activity before sending.
+        TurnContext.apply_conversation_reference(
+            skill_activity,
+            TurnContext.get_conversation_reference(dialog_context.context.activity),
+            is_incoming=True,
+        )
+
+        # Store delivery mode in dialog state for later use.
+        dialog_context.active_dialog.state[
+            self._deliver_mode_state_key
+        ] = dialog_args.activity.delivery_mode
+
+        # Create the conversationId and store it in the dialog context state so we can use it later
+        skill_conversation_id = await self._create_skill_conversation_id(
+            dialog_context.context, dialog_context.context.activity
+        )
+        dialog_context.active_dialog.state[
+            SkillDialog.SKILLCONVERSATIONIDSTATEKEY
+        ] = skill_conversation_id
+
+        # Send the activity to the skill.
+        eoc_activity = await self._send_to_skill(
+            dialog_context.context, skill_activity, skill_conversation_id
+        )
+        if eoc_activity:
+            return await dialog_context.end_dialog(eoc_activity.value)
+
+        return self.end_of_turn
+
+    async def continue_dialog(self, dialog_context: DialogContext):
+        if not self._on_validate_activity(dialog_context.context.activity):
+            return self.end_of_turn
+
+        await dialog_context.context.send_trace_activity(
+            f"{SkillDialog.__name__}.continue_dialog()",
+            label=f"ActivityType: {dialog_context.context.activity.type}",
+        )
+
+        # Handle EndOfConversation from the skill (this will be sent to the this dialog by the SkillHandler if
+        # received from the Skill)
+        if dialog_context.context.activity.type == ActivityTypes.end_of_conversation:
+            await dialog_context.context.send_trace_activity(
+                f"{SkillDialog.__name__}.continue_dialog()",
+                label=f"Got {ActivityTypes.end_of_conversation}",
+            )
+
+            return await dialog_context.end_dialog(
+                dialog_context.context.activity.value
+            )
+
+        # Create deep clone of the original activity to avoid altering it before forwarding it.
+        skill_activity = deepcopy(dialog_context.context.activity)
+
+        skill_activity.delivery_mode = dialog_context.active_dialog.state[
+            self._deliver_mode_state_key
+        ]
+
+        # Just forward to the remote skill
+        skill_conversation_id = dialog_context.active_dialog.state[
+            SkillDialog.SKILLCONVERSATIONIDSTATEKEY
+        ]
+        eoc_activity = await self._send_to_skill(
+            dialog_context.context, skill_activity, skill_conversation_id
+        )
+        if eoc_activity:
+            return await dialog_context.end_dialog(eoc_activity.value)
+
+        return self.end_of_turn
+
+    async def reprompt_dialog(  # pylint: disable=unused-argument
+        self, context: TurnContext, instance: DialogInstance
+    ):
+        # Create and send an event to the skill so it can resume the dialog.
+        reprompt_event = Activity(
+            type=ActivityTypes.event, name=DialogEvents.reprompt_dialog
+        )
+
+        # Apply conversation reference and common properties from incoming activity before sending.
+        TurnContext.apply_conversation_reference(
+            reprompt_event,
+            TurnContext.get_conversation_reference(context.activity),
+            is_incoming=True,
+        )
+
+        # connection Name is not applicable for a RePrompt, as we don't expect as OAuthCard in response.
+        skill_conversation_id = instance.state[SkillDialog.SKILLCONVERSATIONIDSTATEKEY]
+        await self._send_to_skill(context, reprompt_event, skill_conversation_id)
+
+    async def resume_dialog(  # pylint: disable=unused-argument
+        self, dialog_context: "DialogContext", reason: DialogReason, result: object
+    ):
+        await self.reprompt_dialog(dialog_context.context, dialog_context.active_dialog)
+        return self.end_of_turn
+
+    async def end_dialog(
+        self, context: TurnContext, instance: DialogInstance, reason: DialogReason
+    ):
+        # Send of of conversation to the skill if the dialog has been cancelled.
+        if reason in (DialogReason.CancelCalled, DialogReason.ReplaceCalled):
+            await context.send_trace_activity(
+                f"{SkillDialog.__name__}.end_dialog()",
+                label=f"ActivityType: {context.activity.type}",
+            )
+            activity = Activity(type=ActivityTypes.end_of_conversation)
+
+            # Apply conversation reference and common properties from incoming activity before sending.
+            TurnContext.apply_conversation_reference(
+                activity,
+                TurnContext.get_conversation_reference(context.activity),
+                is_incoming=True,
+            )
+            activity.channel_data = context.activity.channel_data
+            activity.additional_properties = context.activity.additional_properties
+
+            # connection Name is not applicable for an EndDialog, as we don't expect as OAuthCard in response.
+            skill_conversation_id = instance.state[
+                SkillDialog.SKILLCONVERSATIONIDSTATEKEY
+            ]
+            await self._send_to_skill(context, activity, skill_conversation_id)
+
+        await super().end_dialog(context, instance, reason)
+
+    def _validate_begin_dialog_args(self, options: object) -> BeginSkillDialogOptions:
+        if not options:
+            raise TypeError("options cannot be None.")
+
+        dialog_args = BeginSkillDialogOptions.from_object(options)
+
+        if not dialog_args:
+            raise TypeError(
+                "SkillDialog: options object not valid as BeginSkillDialogOptions."
+            )
+
+        if not dialog_args.activity:
+            raise TypeError(
+                "SkillDialog: activity object in options as BeginSkillDialogOptions cannot be None."
+            )
+
+        return dialog_args
+
+    def _on_validate_activity(
+        self, activity: Activity  # pylint: disable=unused-argument
+    ) -> bool:
+        """
+        Validates the activity sent during continue_dialog.
+
+        Override this method to implement a custom validator for the activity being sent during continue_dialog.
+        This method can be used to ignore activities of a certain type if needed.
+        If this method returns false, the dialog will end the turn without processing the activity.
+        """
+        return True
+
+    async def _send_to_skill(
+        self, context: TurnContext, activity: Activity, skill_conversation_id: str
+    ) -> Activity:
+        if activity.type == ActivityTypes.invoke:
+            # Force ExpectReplies for invoke activities so we can get the replies right away and send
+            # them back to the channel if needed. This makes sure that the dialog will receive the Invoke
+            # response from the skill and any other activities sent, including EoC.
+            activity.delivery_mode = DeliveryModes.expect_replies
+
+        # Always save state before forwarding
+        # (the dialog stack won't get updated with the skillDialog and things won't work if you don't)
+        await self.dialog_options.conversation_state.save_changes(context, True)
+
+        skill_info = self.dialog_options.skill
+        response = await self.dialog_options.skill_client.post_activity(
+            self.dialog_options.bot_id,
+            skill_info.app_id,
+            skill_info.skill_endpoint,
+            self.dialog_options.skill_host_endpoint,
+            skill_conversation_id,
+            activity,
+        )
+
+        # Inspect the skill response status
+        if not 200 <= response.status <= 299:
+            raise Exception(
+                f'Error invoking the skill id: "{skill_info.id}" at "{skill_info.skill_endpoint}"'
+                f" (status is {response.status}). \r\n {response.body}"
+            )
+
+        eoc_activity: Activity = None
+        if activity.delivery_mode == DeliveryModes.expect_replies and response.body:
+            # Process replies in the response.Body.
+            response.body: List[Activity]
+            response.body = ExpectedReplies().deserialize(response.body).activities
+            # Track sent invoke responses, so more than one is not sent.
+            sent_invoke_response = False
+
+            for from_skill_activity in response.body:
+                if from_skill_activity.type == ActivityTypes.end_of_conversation:
+                    # Capture the EndOfConversation activity if it was sent from skill
+                    eoc_activity = from_skill_activity
+
+                    # The conversation has ended, so cleanup the conversation id
+                    await self.dialog_options.conversation_id_factory.delete_conversation_reference(
+                        skill_conversation_id
+                    )
+                elif not sent_invoke_response and await self._intercept_oauth_cards(
+                    context, from_skill_activity, self.dialog_options.connection_name
+                ):
+                    # Token exchange succeeded, so no oauthcard needs to be shown to the user
+                    sent_invoke_response = True
+                else:
+                    # If an invoke response has already been sent we should ignore future invoke responses as this
+                    # represents a bug in the skill.
+                    if from_skill_activity.type == ActivityTypes.invoke_response:
+                        if sent_invoke_response:
+                            continue
+                        sent_invoke_response = True
+                    # Send the response back to the channel.
+                    await context.send_activity(from_skill_activity)
+
+        return eoc_activity
+
+    async def _create_skill_conversation_id(
+        self, context: TurnContext, activity: Activity
+    ) -> str:
+        # Create a conversationId to interact with the skill and send the activity
+        conversation_id_factory_options = SkillConversationIdFactoryOptions(
+            from_bot_oauth_scope=context.turn_state.get(BotAdapter.BOT_OAUTH_SCOPE_KEY),
+            from_bot_id=self.dialog_options.bot_id,
+            activity=activity,
+            bot_framework_skill=self.dialog_options.skill,
+        )
+        skill_conversation_id = await self.dialog_options.conversation_id_factory.create_skill_conversation_id(
+            conversation_id_factory_options
+        )
+        return skill_conversation_id
+
+    async def _intercept_oauth_cards(
+        self, context: TurnContext, activity: Activity, connection_name: str
+    ):
+        """
+        Tells is if we should intercept the OAuthCard message.
+        """
+        if not connection_name or not isinstance(
+            context.adapter, ExtendedUserTokenProvider
+        ):
+            # The adapter may choose not to support token exchange, in which case we fallback to
+            # showing an oauth card to the user.
+            return False
+
+        oauth_card_attachment = next(
+            attachment
+            for attachment in activity.attachments
+            if attachment.content_type == ContentTypes.oauth_card
+        )
+        if oauth_card_attachment:
+            oauth_card = oauth_card_attachment.content
+            if (
+                oauth_card
+                and oauth_card.token_exchange_resource
+                and oauth_card.token_exchange_resource.uri
+            ):
+                try:
+                    result = await context.adapter.exchange_token(
+                        turn_context=context,
+                        connection_name=connection_name,
+                        user_id=context.activity.from_property.id,
+                        exchange_request=TokenExchangeRequest(
+                            uri=oauth_card.token_exchange_resource.uri
+                        ),
+                    )
+
+                    if result and result.token:
+                        # If token above is null, then SSO has failed and hence we return false.
+                        # If not, send an invoke to the skill with the token.
+                        return await self._send_token_exchange_invoke_to_skill(
+                            activity,
+                            oauth_card.token_exchange_resource.id,
+                            oauth_card.connection_name,
+                            result.token,
+                        )
+                except:
+                    # Failures in token exchange are not fatal. They simply mean that the user needs
+                    # to be shown the OAuth card.
+                    return False
+
+        return False
+
+    async def _send_token_exchange_invoke_to_skill(
+        self,
+        incoming_activity: Activity,
+        request_id: str,
+        connection_name: str,
+        token: str,
+    ):
+        activity = incoming_activity.create_reply()
+        activity.type = ActivityTypes.invoke
+        activity.name = SignInConstants.token_exchange_operation_name
+        activity.value = TokenExchangeInvokeRequest(
+            id=request_id, token=token, connection_name=connection_name,
+        )
+
+        # route the activity to the skill
+        skill_info = self.dialog_options.skill
+        response = await self.dialog_options.skill_client.post_activity(
+            self.dialog_options.bot_id,
+            skill_info.app_id,
+            skill_info.skill_endpoint,
+            self.dialog_options.skill_host_endpoint,
+            incoming_activity.conversation.id,
+            activity,
+        )
+
+        # Check response status: true if success, false if failure
+        return response.is_successful_status_code()
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog_options.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog_options.py
new file mode 100644
index 000000000..028490a40
--- /dev/null
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog_options.py
@@ -0,0 +1,29 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.core import ConversationState
+from botbuilder.core.skills import (
+    BotFrameworkClient,
+    BotFrameworkSkill,
+    ConversationIdFactoryBase,
+)
+
+
+class SkillDialogOptions:
+    def __init__(
+        self,
+        bot_id: str = None,
+        skill_client: BotFrameworkClient = None,
+        skill_host_endpoint: str = None,
+        skill: BotFrameworkSkill = None,
+        conversation_id_factory: ConversationIdFactoryBase = None,
+        conversation_state: ConversationState = None,
+        connection_name: str = None,
+    ):
+        self.bot_id = bot_id
+        self.skill_client = skill_client
+        self.skill_host_endpoint = skill_host_endpoint
+        self.skill = skill
+        self.conversation_id_factory = conversation_id_factory
+        self.conversation_state = conversation_state
+        self.connection_name = connection_name
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/waterfall_dialog.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/waterfall_dialog.py
index 678f341f8..570b5b340 100644
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/waterfall_dialog.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/waterfall_dialog.py
@@ -59,7 +59,7 @@ async def begin_dialog(
         properties = {}
         properties["DialogId"] = self.id
         properties["InstanceId"] = instance_id
-        self.telemetry_client.track_event("WaterfallStart", properties=properties)
+        self.telemetry_client.track_event("WaterfallStart", properties)
 
         # Run first stepkinds
         return await self.run_step(dialog_context, 0, DialogReason.BeginCalled, None)
@@ -164,7 +164,7 @@ def get_step_name(self, index: int) -> str:
         """
         step_name = self._steps[index].__qualname__
 
-        if not step_name or ">" in step_name:
+        if not step_name or step_name.endswith(""):
             step_name = f"Step{index + 1}of{len(self._steps)}"
 
         return step_name
diff --git a/libraries/botbuilder-dialogs/requirements.txt b/libraries/botbuilder-dialogs/requirements.txt
index 91e19a0e6..fb56a55b1 100644
--- a/libraries/botbuilder-dialogs/requirements.txt
+++ b/libraries/botbuilder-dialogs/requirements.txt
@@ -1,8 +1,8 @@
-msrest>=0.6.6
-botframework-connector>=4.4.0b1
-botbuilder-schema>=4.4.0b1
-botbuilder-core>=4.4.0b1
-requests>=2.18.1
+msrest==0.6.10
+botframework-connector==4.12.0
+botbuilder-schema==4.12.0
+botbuilder-core==4.12.0
+requests==2.23.0
 PyJWT==1.5.3
-cryptography>=2.3.0
-aiounittest>=1.1.0
\ No newline at end of file
+cryptography==3.2
+aiounittest==1.3.0
diff --git a/libraries/botbuilder-dialogs/setup.py b/libraries/botbuilder-dialogs/setup.py
index af2cdbf80..0525ca6f5 100644
--- a/libraries/botbuilder-dialogs/setup.py
+++ b/libraries/botbuilder-dialogs/setup.py
@@ -5,18 +5,19 @@
 from setuptools import setup
 
 REQUIRES = [
+    "regex<=2019.08.19",
     "recognizers-text-date-time>=1.0.2a1",
     "recognizers-text-number-with-unit>=1.0.2a1",
     "recognizers-text-number>=1.0.2a1",
     "recognizers-text>=1.0.2a1",
     "recognizers-text-choice>=1.0.2a1",
-    "babel>=2.7.0",
-    "botbuilder-schema>=4.4.0b1",
-    "botframework-connector>=4.4.0b1",
-    "botbuilder-core>=4.4.0b1",
+    "babel==2.7.0",
+    "botbuilder-schema==4.12.0",
+    "botframework-connector==4.12.0",
+    "botbuilder-core==4.12.0",
 ]
 
-TEST_REQUIRES = ["aiounittest>=1.1.0"]
+TEST_REQUIRES = ["aiounittest==1.3.0"]
 
 root = os.path.abspath(os.path.dirname(__file__))
 
@@ -42,6 +43,7 @@
         "botbuilder.dialogs",
         "botbuilder.dialogs.prompts",
         "botbuilder.dialogs.choices",
+        "botbuilder.dialogs.skills",
     ],
     install_requires=REQUIRES + TEST_REQUIRES,
     tests_require=TEST_REQUIRES,
@@ -51,7 +53,7 @@
         "Intended Audience :: Developers",
         "License :: OSI Approved :: MIT License",
         "Operating System :: OS Independent",
-        "Development Status :: 3 - Alpha",
+        "Development Status :: 5 - Production/Stable",
         "Topic :: Scientific/Engineering :: Artificial Intelligence",
     ],
 )
diff --git a/libraries/botbuilder-dialogs/tests/memory/scopes/test_memory_scopes.py b/libraries/botbuilder-dialogs/tests/memory/scopes/test_memory_scopes.py
new file mode 100644
index 000000000..5101c7070
--- /dev/null
+++ b/libraries/botbuilder-dialogs/tests/memory/scopes/test_memory_scopes.py
@@ -0,0 +1,566 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+# pylint: disable=pointless-string-statement
+
+from collections import namedtuple
+
+import aiounittest
+
+from botbuilder.core import ConversationState, MemoryStorage, TurnContext, UserState
+from botbuilder.core.adapters import TestAdapter
+from botbuilder.dialogs import (
+    Dialog,
+    DialogContext,
+    DialogContainer,
+    DialogInstance,
+    DialogSet,
+    DialogState,
+    ObjectPath,
+)
+from botbuilder.dialogs.memory.scopes import (
+    ClassMemoryScope,
+    ConversationMemoryScope,
+    DialogMemoryScope,
+    UserMemoryScope,
+    SettingsMemoryScope,
+    ThisMemoryScope,
+    TurnMemoryScope,
+)
+from botbuilder.schema import (
+    Activity,
+    ActivityTypes,
+    ChannelAccount,
+    ConversationAccount,
+)
+
+
+class TestDialog(Dialog):
+    def __init__(self, id: str, message: str):
+        super().__init__(id)
+
+        def aux_try_get_value(state):  # pylint: disable=unused-argument
+            return "resolved value"
+
+        ExpressionObject = namedtuple("ExpressionObject", "try_get_value")
+        self.message = message
+        self.expression = ExpressionObject(aux_try_get_value)
+
+    async def begin_dialog(self, dialog_context: DialogContext, options: object = None):
+        dialog_context.active_dialog.state["is_dialog"] = True
+        await dialog_context.context.send_activity(self.message)
+        return Dialog.end_of_turn
+
+
+class TestContainer(DialogContainer):
+    def __init__(self, id: str, child: Dialog = None):
+        super().__init__(id)
+        self.child_id = None
+        if child:
+            self.dialogs.add(child)
+            self.child_id = child.id
+
+    async def begin_dialog(self, dialog_context: DialogContext, options: object = None):
+        state = dialog_context.active_dialog.state
+        state["is_container"] = True
+        if self.child_id:
+            state["dialog"] = DialogState()
+            child_dc = self.create_child_context(dialog_context)
+            return await child_dc.begin_dialog(self.child_id, options)
+
+        return Dialog.end_of_turn
+
+    async def continue_dialog(self, dialog_context: DialogContext):
+        child_dc = self.create_child_context(dialog_context)
+        if child_dc:
+            return await child_dc.continue_dialog()
+
+        return Dialog.end_of_turn
+
+    def create_child_context(self, dialog_context: DialogContext):
+        state = dialog_context.active_dialog.state
+        if state["dialog"] is not None:
+            child_dc = DialogContext(
+                self.dialogs, dialog_context.context, state["dialog"]
+            )
+            child_dc.parent = dialog_context
+            return child_dc
+
+        return None
+
+
+class MemoryScopesTests(aiounittest.AsyncTestCase):
+    begin_message = Activity(
+        text="begin",
+        type=ActivityTypes.message,
+        channel_id="test",
+        from_property=ChannelAccount(id="user"),
+        recipient=ChannelAccount(id="bot"),
+        conversation=ConversationAccount(id="convo1"),
+    )
+
+    async def test_class_memory_scope_should_find_registered_dialog(self):
+        # Create ConversationState with MemoryStorage and register the state as middleware.
+        conversation_state = ConversationState(MemoryStorage())
+
+        # Create a DialogState property, DialogSet and register the dialogs.
+        dialog_state = conversation_state.create_property("dialogs")
+        dialogs = DialogSet(dialog_state)
+        dialog = TestDialog("test", "test message")
+        dialogs.add(dialog)
+
+        # Create test context
+        context = TurnContext(TestAdapter(), MemoryScopesTests.begin_message)
+        await dialog_state.set(
+            context, DialogState(stack=[DialogInstance(id="test", state={})])
+        )
+
+        dialog_context = await dialogs.create_context(context)
+
+        # Run test
+        scope = ClassMemoryScope()
+        memory = scope.get_memory(dialog_context)
+        self.assertTrue(memory, "memory not returned")
+        self.assertEqual("test message", memory.message)
+        self.assertEqual("resolved value", memory.expression)
+
+    async def test_class_memory_scope_should_not_allow_set_memory_call(self):
+        # Create ConversationState with MemoryStorage and register the state as middleware.
+        conversation_state = ConversationState(MemoryStorage())
+
+        # Create a DialogState property, DialogSet and register the dialogs.
+        dialog_state = conversation_state.create_property("dialogs")
+        dialogs = DialogSet(dialog_state)
+        dialog = TestDialog("test", "test message")
+        dialogs.add(dialog)
+
+        # Create test context
+        context = TurnContext(TestAdapter(), MemoryScopesTests.begin_message)
+        await dialog_state.set(
+            context, DialogState(stack=[DialogInstance(id="test", state={})])
+        )
+
+        dialog_context = await dialogs.create_context(context)
+
+        # Run test
+        scope = ClassMemoryScope()
+        with self.assertRaises(Exception) as context:
+            scope.set_memory(dialog_context, {})
+
+        self.assertTrue("not supported" in str(context.exception))
+
+    async def test_class_memory_scope_should_not_allow_load_and_save_changes_calls(
+        self,
+    ):
+        # Create ConversationState with MemoryStorage and register the state as middleware.
+        conversation_state = ConversationState(MemoryStorage())
+
+        # Create a DialogState property, DialogSet and register the dialogs.
+        dialog_state = conversation_state.create_property("dialogs")
+        dialogs = DialogSet(dialog_state)
+        dialog = TestDialog("test", "test message")
+        dialogs.add(dialog)
+
+        # Create test context
+        context = TurnContext(TestAdapter(), MemoryScopesTests.begin_message)
+        await dialog_state.set(
+            context, DialogState(stack=[DialogInstance(id="test", state={})])
+        )
+
+        dialog_context = await dialogs.create_context(context)
+
+        # Run test
+        scope = ClassMemoryScope()
+        await scope.load(dialog_context)
+        memory = scope.get_memory(dialog_context)
+        with self.assertRaises(AttributeError) as context:
+            memory.message = "foo"
+
+        self.assertTrue("can't set attribute" in str(context.exception))
+        await scope.save_changes(dialog_context)
+        self.assertEqual("test message", dialog.message)
+
+    async def test_conversation_memory_scope_should_return_conversation_state(self):
+        # Create ConversationState with MemoryStorage and register the state as middleware.
+        conversation_state = ConversationState(MemoryStorage())
+
+        # Create a DialogState property, DialogSet and register the dialogs.
+        dialog_state = conversation_state.create_property("dialogs")
+        dialogs = DialogSet(dialog_state)
+        dialog = TestDialog("test", "test message")
+        dialogs.add(dialog)
+
+        # Create test context
+        context = TurnContext(TestAdapter(), MemoryScopesTests.begin_message)
+        context.turn_state["ConversationState"] = conversation_state
+
+        dialog_context = await dialogs.create_context(context)
+
+        # Initialize conversation state
+        foo_cls = namedtuple("TestObject", "foo")
+        conversation_prop = conversation_state.create_property("conversation")
+        await conversation_prop.set(context, foo_cls(foo="bar"))
+        await conversation_state.save_changes(context)
+
+        # Run test
+        scope = ConversationMemoryScope()
+        memory = scope.get_memory(dialog_context)
+        self.assertTrue(memory, "memory not returned")
+
+        # TODO: Make get_path_value take conversation.foo
+        test_obj = ObjectPath.get_path_value(memory, "conversation")
+        self.assertEqual("bar", test_obj.foo)
+
+    async def test_user_memory_scope_should_not_return_state_if_not_loaded(self):
+        # Initialize user state
+        storage = MemoryStorage()
+        context = TurnContext(TestAdapter(), MemoryScopesTests.begin_message)
+        user_state = UserState(storage)
+        context.turn_state["UserState"] = user_state
+        foo_cls = namedtuple("TestObject", "foo")
+        user_prop = user_state.create_property("conversation")
+        await user_prop.set(context, foo_cls(foo="bar"))
+        await user_state.save_changes(context)
+
+        # Replace context and user_state with new instances
+        context = TurnContext(TestAdapter(), MemoryScopesTests.begin_message)
+        user_state = UserState(storage)
+        context.turn_state["UserState"] = user_state
+
+        # Create a DialogState property, DialogSet and register the dialogs.
+        conversation_state = ConversationState(storage)
+        dialog_state = conversation_state.create_property("dialogs")
+        dialogs = DialogSet(dialog_state)
+        dialog = TestDialog("test", "test message")
+        dialogs.add(dialog)
+
+        # Create test context
+        dialog_context = await dialogs.create_context(context)
+
+        # Run test
+        scope = UserMemoryScope()
+        memory = scope.get_memory(dialog_context)
+        self.assertIsNone(memory, "state returned")
+
+    async def test_user_memory_scope_should_return_state_once_loaded(self):
+        # Initialize user state
+        storage = MemoryStorage()
+        context = TurnContext(TestAdapter(), MemoryScopesTests.begin_message)
+        user_state = UserState(storage)
+        context.turn_state["UserState"] = user_state
+        foo_cls = namedtuple("TestObject", "foo")
+        user_prop = user_state.create_property("conversation")
+        await user_prop.set(context, foo_cls(foo="bar"))
+        await user_state.save_changes(context)
+
+        # Replace context and conversation_state with instances
+        context = TurnContext(TestAdapter(), MemoryScopesTests.begin_message)
+        user_state = UserState(storage)
+        context.turn_state["UserState"] = user_state
+
+        # Create a DialogState property, DialogSet and register the dialogs.
+        conversation_state = ConversationState(storage)
+        context.turn_state["ConversationState"] = conversation_state
+        dialog_state = conversation_state.create_property("dialogs")
+        dialogs = DialogSet(dialog_state)
+        dialog = TestDialog("test", "test message")
+        dialogs.add(dialog)
+
+        # Create test context
+        dialog_context = await dialogs.create_context(context)
+
+        # Run test
+        scope = UserMemoryScope()
+        memory = scope.get_memory(dialog_context)
+        self.assertIsNone(memory, "state returned")
+
+        await scope.load(dialog_context)
+        memory = scope.get_memory(dialog_context)
+        self.assertIsNotNone(memory, "state not returned")
+
+        # TODO: Make get_path_value take conversation.foo
+        test_obj = ObjectPath.get_path_value(memory, "conversation")
+        self.assertEqual("bar", test_obj.foo)
+
+    async def test_dialog_memory_scope_should_return_containers_state(self):
+        # Create a DialogState property, DialogSet and register the dialogs.
+        storage = MemoryStorage()
+        conversation_state = ConversationState(storage)
+        dialog_state = conversation_state.create_property("dialogs")
+        dialogs = DialogSet(dialog_state)
+        container = TestContainer("container")
+        dialogs.add(container)
+
+        # Create test context
+        context = TurnContext(TestAdapter(), MemoryScopesTests.begin_message)
+        dialog_context = await dialogs.create_context(context)
+
+        # Run test
+        scope = DialogMemoryScope()
+        await dialog_context.begin_dialog("container")
+        memory = scope.get_memory(dialog_context)
+        self.assertIsNotNone(memory, "state not returned")
+        self.assertTrue(memory["is_container"])
+
+    async def test_dialog_memory_scope_should_return_parent_containers_state_for_children(
+        self,
+    ):
+        # Create a DialogState property, DialogSet and register the dialogs.
+        storage = MemoryStorage()
+        conversation_state = ConversationState(storage)
+        dialog_state = conversation_state.create_property("dialogs")
+        dialogs = DialogSet(dialog_state)
+        container = TestContainer("container", TestDialog("child", "test message"))
+        dialogs.add(container)
+
+        # Create test context
+        context = TurnContext(TestAdapter(), MemoryScopesTests.begin_message)
+        dialog_context = await dialogs.create_context(context)
+
+        # Run test
+        scope = DialogMemoryScope()
+        await dialog_context.begin_dialog("container")
+        child_dc = dialog_context.child
+        self.assertIsNotNone(child_dc, "No child DC")
+        memory = scope.get_memory(child_dc)
+        self.assertIsNotNone(memory, "state not returned")
+        self.assertTrue(memory["is_container"])
+
+    async def test_dialog_memory_scope_should_return_childs_state_when_no_parent(self):
+        # Create a DialogState property, DialogSet and register the dialogs.
+        storage = MemoryStorage()
+        conversation_state = ConversationState(storage)
+        dialog_state = conversation_state.create_property("dialogs")
+        dialogs = DialogSet(dialog_state)
+        dialog = TestDialog("test", "test message")
+        dialogs.add(dialog)
+
+        # Create test context
+        context = TurnContext(TestAdapter(), MemoryScopesTests.begin_message)
+        dialog_context = await dialogs.create_context(context)
+
+        # Run test
+        scope = DialogMemoryScope()
+        await dialog_context.begin_dialog("test")
+        memory = scope.get_memory(dialog_context)
+        self.assertIsNotNone(memory, "state not returned")
+        self.assertTrue(memory["is_dialog"])
+
+    async def test_dialog_memory_scope_should_overwrite_parents_memory(self):
+        # Create a DialogState property, DialogSet and register the dialogs.
+        storage = MemoryStorage()
+        conversation_state = ConversationState(storage)
+        dialog_state = conversation_state.create_property("dialogs")
+        dialogs = DialogSet(dialog_state)
+        container = TestContainer("container", TestDialog("child", "test message"))
+        dialogs.add(container)
+
+        # Create test context
+        context = TurnContext(TestAdapter(), MemoryScopesTests.begin_message)
+        dialog_context = await dialogs.create_context(context)
+
+        # Run test
+        scope = DialogMemoryScope()
+        await dialog_context.begin_dialog("container")
+        child_dc = dialog_context.child
+        self.assertIsNotNone(child_dc, "No child DC")
+
+        foo_cls = namedtuple("TestObject", "foo")
+        scope.set_memory(child_dc, foo_cls("bar"))
+        memory = scope.get_memory(child_dc)
+        self.assertIsNotNone(memory, "state not returned")
+        self.assertEqual(memory.foo, "bar")
+
+    async def test_dialog_memory_scope_should_overwrite_active_dialogs_memory(self):
+        # Create a DialogState property, DialogSet and register the dialogs.
+        storage = MemoryStorage()
+        conversation_state = ConversationState(storage)
+        dialog_state = conversation_state.create_property("dialogs")
+        dialogs = DialogSet(dialog_state)
+        container = TestContainer("container")
+        dialogs.add(container)
+
+        # Create test context
+        context = TurnContext(TestAdapter(), MemoryScopesTests.begin_message)
+        dialog_context = await dialogs.create_context(context)
+
+        # Run test
+        scope = DialogMemoryScope()
+        await dialog_context.begin_dialog("container")
+        foo_cls = namedtuple("TestObject", "foo")
+        scope.set_memory(dialog_context, foo_cls("bar"))
+        memory = scope.get_memory(dialog_context)
+        self.assertIsNotNone(memory, "state not returned")
+        self.assertEqual(memory.foo, "bar")
+
+    async def test_dialog_memory_scope_should_raise_error_if_set_memory_called_without_memory(
+        self,
+    ):
+        # Create a DialogState property, DialogSet and register the dialogs.
+        storage = MemoryStorage()
+        conversation_state = ConversationState(storage)
+        dialog_state = conversation_state.create_property("dialogs")
+        dialogs = DialogSet(dialog_state)
+        container = TestContainer("container")
+        dialogs.add(container)
+
+        # Create test context
+        context = TurnContext(TestAdapter(), MemoryScopesTests.begin_message)
+        dialog_context = await dialogs.create_context(context)
+
+        # Run test
+        with self.assertRaises(Exception):
+            scope = DialogMemoryScope()
+            await dialog_context.begin_dialog("container")
+            scope.set_memory(dialog_context, None)
+
+    async def test_settings_memory_scope_should_return_content_of_settings(self):
+        # pylint: disable=import-outside-toplevel
+        from test_settings import DefaultConfig
+
+        # Create a DialogState property, DialogSet and register the dialogs.
+        conversation_state = ConversationState(MemoryStorage())
+        dialog_state = conversation_state.create_property("dialogs")
+        dialogs = DialogSet(dialog_state).add(TestDialog("test", "test message"))
+
+        # Create test context
+        context = TurnContext(TestAdapter(), MemoryScopesTests.begin_message)
+        dialog_context = await dialogs.create_context(context)
+        settings = DefaultConfig()
+        dialog_context.context.turn_state["settings"] = settings
+
+        # Run test
+        scope = SettingsMemoryScope()
+        memory = scope.get_memory(dialog_context)
+        self.assertIsNotNone(memory)
+        self.assertEqual(memory.STRING, "test")
+        self.assertEqual(memory.INT, 3)
+        self.assertEqual(memory.LIST[0], "zero")
+        self.assertEqual(memory.LIST[1], "one")
+        self.assertEqual(memory.LIST[2], "two")
+        self.assertEqual(memory.LIST[3], "three")
+
+    async def test_this_memory_scope_should_return_active_dialogs_state(self):
+        # Create a DialogState property, DialogSet and register the dialogs.
+        storage = MemoryStorage()
+        conversation_state = ConversationState(storage)
+        dialog_state = conversation_state.create_property("dialogs")
+        dialogs = DialogSet(dialog_state)
+        dialog = TestDialog("test", "test message")
+        dialogs.add(dialog)
+
+        # Create test context
+        context = TurnContext(TestAdapter(), MemoryScopesTests.begin_message)
+        dialog_context = await dialogs.create_context(context)
+
+        # Run test
+        scope = ThisMemoryScope()
+        await dialog_context.begin_dialog("test")
+        memory = scope.get_memory(dialog_context)
+        self.assertIsNotNone(memory, "state not returned")
+        self.assertTrue(memory["is_dialog"])
+
+    async def test_this_memory_scope_should_overwrite_active_dialogs_memory(self):
+        # Create a DialogState property, DialogSet and register the dialogs.
+        storage = MemoryStorage()
+        conversation_state = ConversationState(storage)
+        dialog_state = conversation_state.create_property("dialogs")
+        dialogs = DialogSet(dialog_state)
+        container = TestContainer("container")
+        dialogs.add(container)
+
+        # Create test context
+        context = TurnContext(TestAdapter(), MemoryScopesTests.begin_message)
+        dialog_context = await dialogs.create_context(context)
+
+        # Run test
+        scope = ThisMemoryScope()
+        await dialog_context.begin_dialog("container")
+        foo_cls = namedtuple("TestObject", "foo")
+        scope.set_memory(dialog_context, foo_cls("bar"))
+        memory = scope.get_memory(dialog_context)
+        self.assertIsNotNone(memory, "state not returned")
+        self.assertEqual(memory.foo, "bar")
+
+    async def test_this_memory_scope_should_raise_error_if_set_memory_called_without_memory(
+        self,
+    ):
+        # Create a DialogState property, DialogSet and register the dialogs.
+        storage = MemoryStorage()
+        conversation_state = ConversationState(storage)
+        dialog_state = conversation_state.create_property("dialogs")
+        dialogs = DialogSet(dialog_state)
+        container = TestContainer("container")
+        dialogs.add(container)
+
+        # Create test context
+        context = TurnContext(TestAdapter(), MemoryScopesTests.begin_message)
+        dialog_context = await dialogs.create_context(context)
+
+        # Run test
+        with self.assertRaises(Exception):
+            scope = ThisMemoryScope()
+            await dialog_context.begin_dialog("container")
+            scope.set_memory(dialog_context, None)
+
+    async def test_this_memory_scope_should_raise_error_if_set_memory_called_without_active_dialog(
+        self,
+    ):
+        # Create a DialogState property, DialogSet and register the dialogs.
+        storage = MemoryStorage()
+        conversation_state = ConversationState(storage)
+        dialog_state = conversation_state.create_property("dialogs")
+        dialogs = DialogSet(dialog_state)
+        container = TestContainer("container")
+        dialogs.add(container)
+
+        # Create test context
+        context = TurnContext(TestAdapter(), MemoryScopesTests.begin_message)
+        dialog_context = await dialogs.create_context(context)
+
+        # Run test
+        with self.assertRaises(Exception):
+            scope = ThisMemoryScope()
+            foo_cls = namedtuple("TestObject", "foo")
+            scope.set_memory(dialog_context, foo_cls("bar"))
+
+    async def test_turn_memory_scope_should_persist_changes_to_turn_state(self):
+        # Create a DialogState property, DialogSet and register the dialogs.
+        storage = MemoryStorage()
+        conversation_state = ConversationState(storage)
+        dialog_state = conversation_state.create_property("dialogs")
+        dialogs = DialogSet(dialog_state)
+        dialog = TestDialog("test", "test message")
+        dialogs.add(dialog)
+
+        # Create test context
+        context = TurnContext(TestAdapter(), MemoryScopesTests.begin_message)
+        dialog_context = await dialogs.create_context(context)
+
+        # Run test
+        scope = TurnMemoryScope()
+        memory = scope.get_memory(dialog_context)
+        self.assertIsNotNone(memory, "state not returned")
+        memory["foo"] = "bar"
+        memory = scope.get_memory(dialog_context)
+        self.assertEqual(memory["foo"], "bar")
+
+    async def test_turn_memory_scope_should_overwrite_values_in_turn_state(self):
+        # Create a DialogState property, DialogSet and register the dialogs.
+        storage = MemoryStorage()
+        conversation_state = ConversationState(storage)
+        dialog_state = conversation_state.create_property("dialogs")
+        dialogs = DialogSet(dialog_state)
+        dialog = TestDialog("test", "test message")
+        dialogs.add(dialog)
+
+        # Create test context
+        context = TurnContext(TestAdapter(), MemoryScopesTests.begin_message)
+        dialog_context = await dialogs.create_context(context)
+
+        # Run test
+        scope = TurnMemoryScope()
+        foo_cls = namedtuple("TestObject", "foo")
+        scope.set_memory(dialog_context, foo_cls("bar"))
+        memory = scope.get_memory(dialog_context)
+        self.assertIsNotNone(memory, "state not returned")
+        self.assertEqual(memory.foo, "bar")
diff --git a/libraries/botbuilder-dialogs/tests/memory/scopes/test_settings.py b/libraries/botbuilder-dialogs/tests/memory/scopes/test_settings.py
new file mode 100644
index 000000000..ab83adef1
--- /dev/null
+++ b/libraries/botbuilder-dialogs/tests/memory/scopes/test_settings.py
@@ -0,0 +1,14 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+
+
+class DefaultConfig:
+    """ Bot Configuration """
+
+    STRING = os.environ.get("STRING", "test")
+    INT = os.environ.get("INT", 3)
+    LIST = os.environ.get("LIST", ["zero", "one", "two", "three"])
+    NOT_TO_BE_OVERRIDDEN = os.environ.get("NOT_TO_BE_OVERRIDDEN", "one")
+    TO_BE_OVERRIDDEN = os.environ.get("TO_BE_OVERRIDDEN", "one")
diff --git a/libraries/botbuilder-dialogs/tests/test_activity_prompt.py b/libraries/botbuilder-dialogs/tests/test_activity_prompt.py
index ab8fa4971..2f2019c91 100644
--- a/libraries/botbuilder-dialogs/tests/test_activity_prompt.py
+++ b/libraries/botbuilder-dialogs/tests/test_activity_prompt.py
@@ -215,3 +215,91 @@ async def aux_validator(prompt_context: PromptValidatorContext):
         step1 = await adapter.send("hello")
         step2 = await step1.assert_reply("please send an event.")
         await step2.assert_reply("please send an event.")
+
+    async def test_activity_prompt_onerror_should_return_dialogcontext(self):
+        # Create ConversationState with MemoryStorage and register the state as middleware.
+        convo_state = ConversationState(MemoryStorage())
+
+        # Create a DialogState property, DialogSet and AttachmentPrompt.
+        dialog_state = convo_state.create_property("dialog_state")
+        dialogs = DialogSet(dialog_state)
+        dialogs.add(SimpleActivityPrompt("EventActivityPrompt", validator))
+
+        async def exec_test(turn_context: TurnContext):
+            dialog_context = await dialogs.create_context(turn_context)
+
+            results = await dialog_context.continue_dialog()
+
+            if results.status == DialogTurnStatus.Empty:
+                options = PromptOptions(
+                    prompt=Activity(
+                        type=ActivityTypes.message, text="please send an event."
+                    )
+                )
+
+                try:
+                    await dialog_context.prompt("EventActivityPrompt", options)
+                    await dialog_context.prompt("Non existent id", options)
+                except Exception as err:
+                    self.assertIsNotNone(
+                        err.data["DialogContext"]  # pylint: disable=no-member
+                    )
+                    self.assertEqual(
+                        err.data["DialogContext"][  # pylint: disable=no-member
+                            "active_dialog"
+                        ],
+                        "EventActivityPrompt",
+                    )
+                else:
+                    raise Exception("Should have thrown an error.")
+
+            elif results.status == DialogTurnStatus.Complete:
+                await turn_context.send_activity(results.result)
+
+            await convo_state.save_changes(turn_context)
+
+        # Initialize TestAdapter.
+        adapter = TestAdapter(exec_test)
+
+        await adapter.send("hello")
+
+    async def test_activity_replace_dialog_onerror_should_return_dialogcontext(self):
+        # Create ConversationState with MemoryStorage and register the state as middleware.
+        convo_state = ConversationState(MemoryStorage())
+
+        # Create a DialogState property, DialogSet and AttachmentPrompt.
+        dialog_state = convo_state.create_property("dialog_state")
+        dialogs = DialogSet(dialog_state)
+        dialogs.add(SimpleActivityPrompt("EventActivityPrompt", validator))
+
+        async def exec_test(turn_context: TurnContext):
+            dialog_context = await dialogs.create_context(turn_context)
+
+            results = await dialog_context.continue_dialog()
+
+            if results.status == DialogTurnStatus.Empty:
+                options = PromptOptions(
+                    prompt=Activity(
+                        type=ActivityTypes.message, text="please send an event."
+                    )
+                )
+
+                try:
+                    await dialog_context.prompt("EventActivityPrompt", options)
+                    await dialog_context.replace_dialog("Non existent id", options)
+                except Exception as err:
+                    self.assertIsNotNone(
+                        err.data["DialogContext"]  # pylint: disable=no-member
+                    )
+                else:
+                    raise Exception("Should have thrown an error.")
+
+            elif results.status == DialogTurnStatus.Complete:
+                await turn_context.send_activity(results.result)
+
+            await convo_state.save_changes(turn_context)
+
+        # Initialize TestAdapter.
+        adapter = TestAdapter(exec_test)
+
+        await adapter.send("hello")
diff --git a/libraries/botbuilder-dialogs/tests/test_choice_prompt.py b/libraries/botbuilder-dialogs/tests/test_choice_prompt.py
index 8b4499e1d..16cb16c9e 100644
--- a/libraries/botbuilder-dialogs/tests/test_choice_prompt.py
+++ b/libraries/botbuilder-dialogs/tests/test_choice_prompt.py
@@ -1,695 +1,717 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from typing import List
-
-import aiounittest
-from recognizers_text import Culture
-
-from botbuilder.core import CardFactory, ConversationState, MemoryStorage, TurnContext
-from botbuilder.core.adapters import TestAdapter
-from botbuilder.dialogs import DialogSet, DialogTurnResult, DialogTurnStatus
-from botbuilder.dialogs.choices import Choice, ListStyle
-from botbuilder.dialogs.prompts import (
-    ChoicePrompt,
-    PromptOptions,
-    PromptValidatorContext,
-)
-from botbuilder.schema import Activity, ActivityTypes
-
-_color_choices: List[Choice] = [
-    Choice(value="red"),
-    Choice(value="green"),
-    Choice(value="blue"),
-]
-
-_answer_message: Activity = Activity(text="red", type=ActivityTypes.message)
-_invalid_message: Activity = Activity(text="purple", type=ActivityTypes.message)
-
-
-class ChoicePromptTest(aiounittest.AsyncTestCase):
-    def test_choice_prompt_with_empty_id_should_fail(self):
-        empty_id = ""
-
-        with self.assertRaises(TypeError):
-            ChoicePrompt(empty_id)
-
-    def test_choice_prompt_with_none_id_should_fail(self):
-        none_id = None
-
-        with self.assertRaises(TypeError):
-            ChoicePrompt(none_id)
-
-    async def test_should_call_choice_prompt_using_dc_prompt(self):
-        async def exec_test(turn_context: TurnContext):
-            dialog_context = await dialogs.create_context(turn_context)
-
-            results: DialogTurnResult = await dialog_context.continue_dialog()
-
-            if results.status == DialogTurnStatus.Empty:
-                options = PromptOptions(
-                    prompt=Activity(
-                        type=ActivityTypes.message, text="Please choose a color."
-                    ),
-                    choices=_color_choices,
-                )
-                await dialog_context.prompt("ChoicePrompt", options)
-            elif results.status == DialogTurnStatus.Complete:
-                selected_choice = results.result
-                await turn_context.send_activity(selected_choice.value)
-
-            await convo_state.save_changes(turn_context)
-
-        # Initialize TestAdapter.
-        adapter = TestAdapter(exec_test)
-
-        # Create new ConversationState with MemoryStorage and register the state as middleware.
-        convo_state = ConversationState(MemoryStorage())
-
-        # Create a DialogState property, DialogSet, and ChoicePrompt.
-        dialog_state = convo_state.create_property("dialogState")
-        dialogs = DialogSet(dialog_state)
-        choice_prompt = ChoicePrompt("ChoicePrompt")
-        dialogs.add(choice_prompt)
-
-        step1 = await adapter.send("hello")
-        step2 = await step1.assert_reply(
-            "Please choose a color. (1) red, (2) green, or (3) blue"
-        )
-        step3 = await step2.send(_answer_message)
-        await step3.assert_reply("red")
-
-    async def test_should_call_choice_prompt_with_custom_validator(self):
-        async def exec_test(turn_context: TurnContext):
-            dialog_context = await dialogs.create_context(turn_context)
-
-            results: DialogTurnResult = await dialog_context.continue_dialog()
-
-            if results.status == DialogTurnStatus.Empty:
-                options = PromptOptions(
-                    prompt=Activity(
-                        type=ActivityTypes.message, text="Please choose a color."
-                    ),
-                    choices=_color_choices,
-                )
-                await dialog_context.prompt("prompt", options)
-            elif results.status == DialogTurnStatus.Complete:
-                selected_choice = results.result
-                await turn_context.send_activity(selected_choice.value)
-
-            await convo_state.save_changes(turn_context)
-
-        adapter = TestAdapter(exec_test)
-
-        convo_state = ConversationState(MemoryStorage())
-        dialog_state = convo_state.create_property("dialogState")
-        dialogs = DialogSet(dialog_state)
-
-        async def validator(prompt: PromptValidatorContext) -> bool:
-            assert prompt
-
-            return prompt.recognized.succeeded
-
-        choice_prompt = ChoicePrompt("prompt", validator)
-
-        dialogs.add(choice_prompt)
-
-        step1 = await adapter.send("Hello")
-        step2 = await step1.assert_reply(
-            "Please choose a color. (1) red, (2) green, or (3) blue"
-        )
-        step3 = await step2.send(_invalid_message)
-        step4 = await step3.assert_reply(
-            "Please choose a color. (1) red, (2) green, or (3) blue"
-        )
-        step5 = await step4.send(_answer_message)
-        await step5.assert_reply("red")
-
-    async def test_should_send_custom_retry_prompt(self):
-        async def exec_test(turn_context: TurnContext):
-            dialog_context = await dialogs.create_context(turn_context)
-
-            results: DialogTurnResult = await dialog_context.continue_dialog()
-
-            if results.status == DialogTurnStatus.Empty:
-                options = PromptOptions(
-                    prompt=Activity(
-                        type=ActivityTypes.message, text="Please choose a color."
-                    ),
-                    retry_prompt=Activity(
-                        type=ActivityTypes.message,
-                        text="Please choose red, blue, or green.",
-                    ),
-                    choices=_color_choices,
-                )
-                await dialog_context.prompt("prompt", options)
-            elif results.status == DialogTurnStatus.Complete:
-                selected_choice = results.result
-                await turn_context.send_activity(selected_choice.value)
-
-            await convo_state.save_changes(turn_context)
-
-        adapter = TestAdapter(exec_test)
-
-        convo_state = ConversationState(MemoryStorage())
-        dialog_state = convo_state.create_property("dialogState")
-        dialogs = DialogSet(dialog_state)
-        choice_prompt = ChoicePrompt("prompt")
-        dialogs.add(choice_prompt)
-
-        step1 = await adapter.send("Hello")
-        step2 = await step1.assert_reply(
-            "Please choose a color. (1) red, (2) green, or (3) blue"
-        )
-        step3 = await step2.send(_invalid_message)
-        step4 = await step3.assert_reply(
-            "Please choose red, blue, or green. (1) red, (2) green, or (3) blue"
-        )
-        step5 = await step4.send(_answer_message)
-        await step5.assert_reply("red")
-
-    async def test_should_send_ignore_retry_prompt_if_validator_replies(self):
-        async def exec_test(turn_context: TurnContext):
-            dialog_context = await dialogs.create_context(turn_context)
-
-            results: DialogTurnResult = await dialog_context.continue_dialog()
-
-            if results.status == DialogTurnStatus.Empty:
-                options = PromptOptions(
-                    prompt=Activity(
-                        type=ActivityTypes.message, text="Please choose a color."
-                    ),
-                    retry_prompt=Activity(
-                        type=ActivityTypes.message,
-                        text="Please choose red, blue, or green.",
-                    ),
-                    choices=_color_choices,
-                )
-                await dialog_context.prompt("prompt", options)
-            elif results.status == DialogTurnStatus.Complete:
-                selected_choice = results.result
-                await turn_context.send_activity(selected_choice.value)
-
-            await convo_state.save_changes(turn_context)
-
-        adapter = TestAdapter(exec_test)
-
-        convo_state = ConversationState(MemoryStorage())
-        dialog_state = convo_state.create_property("dialogState")
-        dialogs = DialogSet(dialog_state)
-
-        async def validator(prompt: PromptValidatorContext) -> bool:
-            assert prompt
-
-            if not prompt.recognized.succeeded:
-                await prompt.context.send_activity("Bad input.")
-
-            return prompt.recognized.succeeded
-
-        choice_prompt = ChoicePrompt("prompt", validator)
-
-        dialogs.add(choice_prompt)
-
-        step1 = await adapter.send("Hello")
-        step2 = await step1.assert_reply(
-            "Please choose a color. (1) red, (2) green, or (3) blue"
-        )
-        step3 = await step2.send(_invalid_message)
-        step4 = await step3.assert_reply("Bad input.")
-        step5 = await step4.send(_answer_message)
-        await step5.assert_reply("red")
-
-    async def test_should_use_default_locale_when_rendering_choices(self):
-        async def exec_test(turn_context: TurnContext):
-            dialog_context = await dialogs.create_context(turn_context)
-
-            results: DialogTurnResult = await dialog_context.continue_dialog()
-
-            if results.status == DialogTurnStatus.Empty:
-                options = PromptOptions(
-                    prompt=Activity(
-                        type=ActivityTypes.message, text="Please choose a color."
-                    ),
-                    choices=_color_choices,
-                )
-                await dialog_context.prompt("prompt", options)
-            elif results.status == DialogTurnStatus.Complete:
-                selected_choice = results.result
-                await turn_context.send_activity(selected_choice.value)
-
-            await convo_state.save_changes(turn_context)
-
-        adapter = TestAdapter(exec_test)
-
-        convo_state = ConversationState(MemoryStorage())
-        dialog_state = convo_state.create_property("dialogState")
-        dialogs = DialogSet(dialog_state)
-
-        async def validator(prompt: PromptValidatorContext) -> bool:
-            assert prompt
-
-            if not prompt.recognized.succeeded:
-                await prompt.context.send_activity("Bad input.")
-
-            return prompt.recognized.succeeded
-
-        choice_prompt = ChoicePrompt(
-            "prompt", validator, default_locale=Culture.Spanish
-        )
-
-        dialogs.add(choice_prompt)
-
-        step1 = await adapter.send(Activity(type=ActivityTypes.message, text="Hello"))
-        step2 = await step1.assert_reply(
-            "Please choose a color. (1) red, (2) green, o (3) blue"
-        )
-        step3 = await step2.send(_invalid_message)
-        step4 = await step3.assert_reply("Bad input.")
-        step5 = await step4.send(Activity(type=ActivityTypes.message, text="red"))
-        await step5.assert_reply("red")
-
-    async def test_should_use_context_activity_locale_when_rendering_choices(self):
-        async def exec_test(turn_context: TurnContext):
-            dialog_context = await dialogs.create_context(turn_context)
-
-            results: DialogTurnResult = await dialog_context.continue_dialog()
-
-            if results.status == DialogTurnStatus.Empty:
-                options = PromptOptions(
-                    prompt=Activity(
-                        type=ActivityTypes.message, text="Please choose a color."
-                    ),
-                    choices=_color_choices,
-                )
-                await dialog_context.prompt("prompt", options)
-            elif results.status == DialogTurnStatus.Complete:
-                selected_choice = results.result
-                await turn_context.send_activity(selected_choice.value)
-
-            await convo_state.save_changes(turn_context)
-
-        adapter = TestAdapter(exec_test)
-
-        convo_state = ConversationState(MemoryStorage())
-        dialog_state = convo_state.create_property("dialogState")
-        dialogs = DialogSet(dialog_state)
-
-        async def validator(prompt: PromptValidatorContext) -> bool:
-            assert prompt
-
-            if not prompt.recognized.succeeded:
-                await prompt.context.send_activity("Bad input.")
-
-            return prompt.recognized.succeeded
-
-        choice_prompt = ChoicePrompt("prompt", validator)
-        dialogs.add(choice_prompt)
-
-        step1 = await adapter.send(
-            Activity(type=ActivityTypes.message, text="Hello", locale=Culture.Spanish)
-        )
-        step2 = await step1.assert_reply(
-            "Please choose a color. (1) red, (2) green, o (3) blue"
-        )
-        step3 = await step2.send(_answer_message)
-        await step3.assert_reply("red")
-
-    async def test_should_use_context_activity_locale_over_default_locale_when_rendering_choices(
-        self
-    ):
-        async def exec_test(turn_context: TurnContext):
-            dialog_context = await dialogs.create_context(turn_context)
-
-            results: DialogTurnResult = await dialog_context.continue_dialog()
-
-            if results.status == DialogTurnStatus.Empty:
-                options = PromptOptions(
-                    prompt=Activity(
-                        type=ActivityTypes.message, text="Please choose a color."
-                    ),
-                    choices=_color_choices,
-                )
-                await dialog_context.prompt("prompt", options)
-            elif results.status == DialogTurnStatus.Complete:
-                selected_choice = results.result
-                await turn_context.send_activity(selected_choice.value)
-
-            await convo_state.save_changes(turn_context)
-
-        adapter = TestAdapter(exec_test)
-
-        convo_state = ConversationState(MemoryStorage())
-        dialog_state = convo_state.create_property("dialogState")
-        dialogs = DialogSet(dialog_state)
-
-        async def validator(prompt: PromptValidatorContext) -> bool:
-            assert prompt
-
-            if not prompt.recognized.succeeded:
-                await prompt.context.send_activity("Bad input.")
-
-            return prompt.recognized.succeeded
-
-        choice_prompt = ChoicePrompt(
-            "prompt", validator, default_locale=Culture.Spanish
-        )
-        dialogs.add(choice_prompt)
-
-        step1 = await adapter.send(
-            Activity(type=ActivityTypes.message, text="Hello", locale=Culture.English)
-        )
-        step2 = await step1.assert_reply(
-            "Please choose a color. (1) red, (2) green, or (3) blue"
-        )
-        step3 = await step2.send(_answer_message)
-        await step3.assert_reply("red")
-
-    async def test_should_not_render_choices_if_list_style_none_is_specified(self):
-        async def exec_test(turn_context: TurnContext):
-            dialog_context = await dialogs.create_context(turn_context)
-
-            results: DialogTurnResult = await dialog_context.continue_dialog()
-
-            if results.status == DialogTurnStatus.Empty:
-                options = PromptOptions(
-                    prompt=Activity(
-                        type=ActivityTypes.message, text="Please choose a color."
-                    ),
-                    choices=_color_choices,
-                    style=ListStyle.none,
-                )
-                await dialog_context.prompt("prompt", options)
-            elif results.status == DialogTurnStatus.Complete:
-                selected_choice = results.result
-                await turn_context.send_activity(selected_choice.value)
-
-            await convo_state.save_changes(turn_context)
-
-        adapter = TestAdapter(exec_test)
-
-        convo_state = ConversationState(MemoryStorage())
-        dialog_state = convo_state.create_property("dialogState")
-        dialogs = DialogSet(dialog_state)
-
-        choice_prompt = ChoicePrompt("prompt")
-
-        dialogs.add(choice_prompt)
-
-        step1 = await adapter.send("Hello")
-        step2 = await step1.assert_reply("Please choose a color.")
-        step3 = await step2.send(_answer_message)
-        await step3.assert_reply("red")
-
-    async def test_should_create_prompt_with_inline_choices_when_specified(self):
-        async def exec_test(turn_context: TurnContext):
-            dialog_context = await dialogs.create_context(turn_context)
-
-            results: DialogTurnResult = await dialog_context.continue_dialog()
-
-            if results.status == DialogTurnStatus.Empty:
-                options = PromptOptions(
-                    prompt=Activity(
-                        type=ActivityTypes.message, text="Please choose a color."
-                    ),
-                    choices=_color_choices,
-                )
-                await dialog_context.prompt("prompt", options)
-            elif results.status == DialogTurnStatus.Complete:
-                selected_choice = results.result
-                await turn_context.send_activity(selected_choice.value)
-
-            await convo_state.save_changes(turn_context)
-
-        adapter = TestAdapter(exec_test)
-
-        convo_state = ConversationState(MemoryStorage())
-        dialog_state = convo_state.create_property("dialogState")
-        dialogs = DialogSet(dialog_state)
-
-        choice_prompt = ChoicePrompt("prompt")
-        choice_prompt.style = ListStyle.in_line
-
-        dialogs.add(choice_prompt)
-
-        step1 = await adapter.send("Hello")
-        step2 = await step1.assert_reply(
-            "Please choose a color. (1) red, (2) green, or (3) blue"
-        )
-        step3 = await step2.send(_answer_message)
-        await step3.assert_reply("red")
-
-    async def test_should_create_prompt_with_list_choices_when_specified(self):
-        async def exec_test(turn_context: TurnContext):
-            dialog_context = await dialogs.create_context(turn_context)
-
-            results: DialogTurnResult = await dialog_context.continue_dialog()
-
-            if results.status == DialogTurnStatus.Empty:
-                options = PromptOptions(
-                    prompt=Activity(
-                        type=ActivityTypes.message, text="Please choose a color."
-                    ),
-                    choices=_color_choices,
-                )
-                await dialog_context.prompt("prompt", options)
-            elif results.status == DialogTurnStatus.Complete:
-                selected_choice = results.result
-                await turn_context.send_activity(selected_choice.value)
-
-            await convo_state.save_changes(turn_context)
-
-        adapter = TestAdapter(exec_test)
-
-        convo_state = ConversationState(MemoryStorage())
-        dialog_state = convo_state.create_property("dialogState")
-        dialogs = DialogSet(dialog_state)
-
-        choice_prompt = ChoicePrompt("prompt")
-        choice_prompt.style = ListStyle.list_style
-
-        dialogs.add(choice_prompt)
-
-        step1 = await adapter.send("Hello")
-        step2 = await step1.assert_reply(
-            "Please choose a color.\n\n   1. red\n   2. green\n   3. blue"
-        )
-        step3 = await step2.send(_answer_message)
-        await step3.assert_reply("red")
-
-    async def test_should_create_prompt_with_suggested_action_style_when_specified(
-        self
-    ):
-        async def exec_test(turn_context: TurnContext):
-            dialog_context = await dialogs.create_context(turn_context)
-
-            results: DialogTurnResult = await dialog_context.continue_dialog()
-
-            if results.status == DialogTurnStatus.Empty:
-                options = PromptOptions(
-                    prompt=Activity(
-                        type=ActivityTypes.message, text="Please choose a color."
-                    ),
-                    choices=_color_choices,
-                    style=ListStyle.suggested_action,
-                )
-                await dialog_context.prompt("prompt", options)
-            elif results.status == DialogTurnStatus.Complete:
-                selected_choice = results.result
-                await turn_context.send_activity(selected_choice.value)
-
-            await convo_state.save_changes(turn_context)
-
-        adapter = TestAdapter(exec_test)
-
-        convo_state = ConversationState(MemoryStorage())
-        dialog_state = convo_state.create_property("dialogState")
-        dialogs = DialogSet(dialog_state)
-
-        choice_prompt = ChoicePrompt("prompt")
-
-        dialogs.add(choice_prompt)
-
-        step1 = await adapter.send("Hello")
-        step2 = await step1.assert_reply("Please choose a color.")
-        step3 = await step2.send(_answer_message)
-        await step3.assert_reply("red")
-
-    async def test_should_create_prompt_with_auto_style_when_specified(self):
-        async def exec_test(turn_context: TurnContext):
-            dialog_context = await dialogs.create_context(turn_context)
-
-            results: DialogTurnResult = await dialog_context.continue_dialog()
-
-            if results.status == DialogTurnStatus.Empty:
-                options = PromptOptions(
-                    prompt=Activity(
-                        type=ActivityTypes.message, text="Please choose a color."
-                    ),
-                    choices=_color_choices,
-                    style=ListStyle.auto,
-                )
-                await dialog_context.prompt("prompt", options)
-            elif results.status == DialogTurnStatus.Complete:
-                selected_choice = results.result
-                await turn_context.send_activity(selected_choice.value)
-
-            await convo_state.save_changes(turn_context)
-
-        adapter = TestAdapter(exec_test)
-
-        convo_state = ConversationState(MemoryStorage())
-        dialog_state = convo_state.create_property("dialogState")
-        dialogs = DialogSet(dialog_state)
-
-        choice_prompt = ChoicePrompt("prompt")
-
-        dialogs.add(choice_prompt)
-
-        step1 = await adapter.send("Hello")
-        step2 = await step1.assert_reply(
-            "Please choose a color. (1) red, (2) green, or (3) blue"
-        )
-        step3 = await step2.send(_answer_message)
-        await step3.assert_reply("red")
-
-    async def test_should_recognize_valid_number_choice(self):
-        async def exec_test(turn_context: TurnContext):
-            dialog_context = await dialogs.create_context(turn_context)
-
-            results: DialogTurnResult = await dialog_context.continue_dialog()
-
-            if results.status == DialogTurnStatus.Empty:
-                options = PromptOptions(
-                    prompt=Activity(
-                        type=ActivityTypes.message, text="Please choose a color."
-                    ),
-                    choices=_color_choices,
-                )
-                await dialog_context.prompt("prompt", options)
-            elif results.status == DialogTurnStatus.Complete:
-                selected_choice = results.result
-                await turn_context.send_activity(selected_choice.value)
-
-            await convo_state.save_changes(turn_context)
-
-        adapter = TestAdapter(exec_test)
-
-        convo_state = ConversationState(MemoryStorage())
-        dialog_state = convo_state.create_property("dialogState")
-        dialogs = DialogSet(dialog_state)
-
-        choice_prompt = ChoicePrompt("prompt")
-
-        dialogs.add(choice_prompt)
-
-        step1 = await adapter.send("Hello")
-        step2 = await step1.assert_reply(
-            "Please choose a color. (1) red, (2) green, or (3) blue"
-        )
-        step3 = await step2.send("1")
-        await step3.assert_reply("red")
-
-    async def test_should_display_choices_on_hero_card(self):
-        size_choices = ["large", "medium", "small"]
-
-        async def exec_test(turn_context: TurnContext):
-            dialog_context = await dialogs.create_context(turn_context)
-
-            results: DialogTurnResult = await dialog_context.continue_dialog()
-
-            if results.status == DialogTurnStatus.Empty:
-                options = PromptOptions(
-                    prompt=Activity(
-                        type=ActivityTypes.message, text="Please choose a size."
-                    ),
-                    choices=size_choices,
-                )
-                await dialog_context.prompt("prompt", options)
-            elif results.status == DialogTurnStatus.Complete:
-                selected_choice = results.result
-                await turn_context.send_activity(selected_choice.value)
-
-            await convo_state.save_changes(turn_context)
-
-        def assert_expected_activity(
-            activity: Activity, description
-        ):  # pylint: disable=unused-argument
-            assert len(activity.attachments) == 1
-            assert (
-                activity.attachments[0].content_type
-                == CardFactory.content_types.hero_card
-            )
-            assert activity.attachments[0].content.text == "Please choose a size."
-
-        adapter = TestAdapter(exec_test)
-
-        convo_state = ConversationState(MemoryStorage())
-        dialog_state = convo_state.create_property("dialogState")
-        dialogs = DialogSet(dialog_state)
-
-        choice_prompt = ChoicePrompt("prompt")
-
-        # Change the ListStyle of the prompt to ListStyle.none.
-        choice_prompt.style = ListStyle.hero_card
-
-        dialogs.add(choice_prompt)
-
-        step1 = await adapter.send("Hello")
-        step2 = await step1.assert_reply(assert_expected_activity)
-        step3 = await step2.send("1")
-        await step3.assert_reply(size_choices[0])
-
-    async def test_should_display_choices_on_hero_card_with_additional_attachment(self):
-        size_choices = ["large", "medium", "small"]
-        card = CardFactory.adaptive_card(
-            {
-                "type": "AdaptiveCard",
-                "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
-                "version": "1.2",
-                "body": [],
-            }
-        )
-        card_activity = Activity(attachments=[card])
-
-        async def exec_test(turn_context: TurnContext):
-            dialog_context = await dialogs.create_context(turn_context)
-
-            results: DialogTurnResult = await dialog_context.continue_dialog()
-
-            if results.status == DialogTurnStatus.Empty:
-                options = PromptOptions(prompt=card_activity, choices=size_choices)
-                await dialog_context.prompt("prompt", options)
-            elif results.status == DialogTurnStatus.Complete:
-                selected_choice = results.result
-                await turn_context.send_activity(selected_choice.value)
-
-            await convo_state.save_changes(turn_context)
-
-        def assert_expected_activity(
-            activity: Activity, description
-        ):  # pylint: disable=unused-argument
-            assert len(activity.attachments) == 2
-            assert (
-                activity.attachments[0].content_type
-                == CardFactory.content_types.adaptive_card
-            )
-            assert (
-                activity.attachments[1].content_type
-                == CardFactory.content_types.hero_card
-            )
-
-        adapter = TestAdapter(exec_test)
-
-        convo_state = ConversationState(MemoryStorage())
-        dialog_state = convo_state.create_property("dialogState")
-        dialogs = DialogSet(dialog_state)
-
-        choice_prompt = ChoicePrompt("prompt")
-
-        # Change the ListStyle of the prompt to ListStyle.none.
-        choice_prompt.style = ListStyle.hero_card
-
-        dialogs.add(choice_prompt)
-
-        step1 = await adapter.send("Hello")
-        await step1.assert_reply(assert_expected_activity)
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from typing import List
+
+import aiounittest
+from recognizers_text import Culture
+
+from botbuilder.core import CardFactory, ConversationState, MemoryStorage, TurnContext
+from botbuilder.core.adapters import TestAdapter
+from botbuilder.dialogs import (
+    DialogSet,
+    DialogTurnResult,
+    DialogTurnStatus,
+    ChoiceRecognizers,
+    FindChoicesOptions,
+)
+from botbuilder.dialogs.choices import Choice, ListStyle
+from botbuilder.dialogs.prompts import (
+    ChoicePrompt,
+    PromptOptions,
+    PromptValidatorContext,
+)
+from botbuilder.schema import Activity, ActivityTypes
+
+_color_choices: List[Choice] = [
+    Choice(value="red"),
+    Choice(value="green"),
+    Choice(value="blue"),
+]
+
+_answer_message: Activity = Activity(text="red", type=ActivityTypes.message)
+_invalid_message: Activity = Activity(text="purple", type=ActivityTypes.message)
+
+
+class ChoicePromptTest(aiounittest.AsyncTestCase):
+    def test_choice_prompt_with_empty_id_should_fail(self):
+        empty_id = ""
+
+        with self.assertRaises(TypeError):
+            ChoicePrompt(empty_id)
+
+    def test_choice_prompt_with_none_id_should_fail(self):
+        none_id = None
+
+        with self.assertRaises(TypeError):
+            ChoicePrompt(none_id)
+
+    async def test_should_call_choice_prompt_using_dc_prompt(self):
+        async def exec_test(turn_context: TurnContext):
+            dialog_context = await dialogs.create_context(turn_context)
+
+            results: DialogTurnResult = await dialog_context.continue_dialog()
+
+            if results.status == DialogTurnStatus.Empty:
+                options = PromptOptions(
+                    prompt=Activity(
+                        type=ActivityTypes.message, text="Please choose a color."
+                    ),
+                    choices=_color_choices,
+                )
+                await dialog_context.prompt("ChoicePrompt", options)
+            elif results.status == DialogTurnStatus.Complete:
+                selected_choice = results.result
+                await turn_context.send_activity(selected_choice.value)
+
+            await convo_state.save_changes(turn_context)
+
+        # Initialize TestAdapter.
+        adapter = TestAdapter(exec_test)
+
+        # Create new ConversationState with MemoryStorage and register the state as middleware.
+        convo_state = ConversationState(MemoryStorage())
+
+        # Create a DialogState property, DialogSet, and ChoicePrompt.
+        dialog_state = convo_state.create_property("dialogState")
+        dialogs = DialogSet(dialog_state)
+        choice_prompt = ChoicePrompt("ChoicePrompt")
+        dialogs.add(choice_prompt)
+
+        step1 = await adapter.send("hello")
+        step2 = await step1.assert_reply(
+            "Please choose a color. (1) red, (2) green, or (3) blue"
+        )
+        step3 = await step2.send(_answer_message)
+        await step3.assert_reply("red")
+
+    async def test_should_call_choice_prompt_with_custom_validator(self):
+        async def exec_test(turn_context: TurnContext):
+            dialog_context = await dialogs.create_context(turn_context)
+
+            results: DialogTurnResult = await dialog_context.continue_dialog()
+
+            if results.status == DialogTurnStatus.Empty:
+                options = PromptOptions(
+                    prompt=Activity(
+                        type=ActivityTypes.message, text="Please choose a color."
+                    ),
+                    choices=_color_choices,
+                )
+                await dialog_context.prompt("prompt", options)
+            elif results.status == DialogTurnStatus.Complete:
+                selected_choice = results.result
+                await turn_context.send_activity(selected_choice.value)
+
+            await convo_state.save_changes(turn_context)
+
+        adapter = TestAdapter(exec_test)
+
+        convo_state = ConversationState(MemoryStorage())
+        dialog_state = convo_state.create_property("dialogState")
+        dialogs = DialogSet(dialog_state)
+
+        async def validator(prompt: PromptValidatorContext) -> bool:
+            assert prompt
+
+            return prompt.recognized.succeeded
+
+        choice_prompt = ChoicePrompt("prompt", validator)
+
+        dialogs.add(choice_prompt)
+
+        step1 = await adapter.send("Hello")
+        step2 = await step1.assert_reply(
+            "Please choose a color. (1) red, (2) green, or (3) blue"
+        )
+        step3 = await step2.send(_invalid_message)
+        step4 = await step3.assert_reply(
+            "Please choose a color. (1) red, (2) green, or (3) blue"
+        )
+        step5 = await step4.send(_answer_message)
+        await step5.assert_reply("red")
+
+    async def test_should_send_custom_retry_prompt(self):
+        async def exec_test(turn_context: TurnContext):
+            dialog_context = await dialogs.create_context(turn_context)
+
+            results: DialogTurnResult = await dialog_context.continue_dialog()
+
+            if results.status == DialogTurnStatus.Empty:
+                options = PromptOptions(
+                    prompt=Activity(
+                        type=ActivityTypes.message, text="Please choose a color."
+                    ),
+                    retry_prompt=Activity(
+                        type=ActivityTypes.message,
+                        text="Please choose red, blue, or green.",
+                    ),
+                    choices=_color_choices,
+                )
+                await dialog_context.prompt("prompt", options)
+            elif results.status == DialogTurnStatus.Complete:
+                selected_choice = results.result
+                await turn_context.send_activity(selected_choice.value)
+
+            await convo_state.save_changes(turn_context)
+
+        adapter = TestAdapter(exec_test)
+
+        convo_state = ConversationState(MemoryStorage())
+        dialog_state = convo_state.create_property("dialogState")
+        dialogs = DialogSet(dialog_state)
+        choice_prompt = ChoicePrompt("prompt")
+        dialogs.add(choice_prompt)
+
+        step1 = await adapter.send("Hello")
+        step2 = await step1.assert_reply(
+            "Please choose a color. (1) red, (2) green, or (3) blue"
+        )
+        step3 = await step2.send(_invalid_message)
+        step4 = await step3.assert_reply(
+            "Please choose red, blue, or green. (1) red, (2) green, or (3) blue"
+        )
+        step5 = await step4.send(_answer_message)
+        await step5.assert_reply("red")
+
+    async def test_should_send_ignore_retry_prompt_if_validator_replies(self):
+        async def exec_test(turn_context: TurnContext):
+            dialog_context = await dialogs.create_context(turn_context)
+
+            results: DialogTurnResult = await dialog_context.continue_dialog()
+
+            if results.status == DialogTurnStatus.Empty:
+                options = PromptOptions(
+                    prompt=Activity(
+                        type=ActivityTypes.message, text="Please choose a color."
+                    ),
+                    retry_prompt=Activity(
+                        type=ActivityTypes.message,
+                        text="Please choose red, blue, or green.",
+                    ),
+                    choices=_color_choices,
+                )
+                await dialog_context.prompt("prompt", options)
+            elif results.status == DialogTurnStatus.Complete:
+                selected_choice = results.result
+                await turn_context.send_activity(selected_choice.value)
+
+            await convo_state.save_changes(turn_context)
+
+        adapter = TestAdapter(exec_test)
+
+        convo_state = ConversationState(MemoryStorage())
+        dialog_state = convo_state.create_property("dialogState")
+        dialogs = DialogSet(dialog_state)
+
+        async def validator(prompt: PromptValidatorContext) -> bool:
+            assert prompt
+
+            if not prompt.recognized.succeeded:
+                await prompt.context.send_activity("Bad input.")
+
+            return prompt.recognized.succeeded
+
+        choice_prompt = ChoicePrompt("prompt", validator)
+
+        dialogs.add(choice_prompt)
+
+        step1 = await adapter.send("Hello")
+        step2 = await step1.assert_reply(
+            "Please choose a color. (1) red, (2) green, or (3) blue"
+        )
+        step3 = await step2.send(_invalid_message)
+        step4 = await step3.assert_reply("Bad input.")
+        step5 = await step4.send(_answer_message)
+        await step5.assert_reply("red")
+
+    async def test_should_use_default_locale_when_rendering_choices(self):
+        async def exec_test(turn_context: TurnContext):
+            dialog_context = await dialogs.create_context(turn_context)
+
+            results: DialogTurnResult = await dialog_context.continue_dialog()
+
+            if results.status == DialogTurnStatus.Empty:
+                options = PromptOptions(
+                    prompt=Activity(
+                        type=ActivityTypes.message, text="Please choose a color."
+                    ),
+                    choices=_color_choices,
+                )
+                await dialog_context.prompt("prompt", options)
+            elif results.status == DialogTurnStatus.Complete:
+                selected_choice = results.result
+                await turn_context.send_activity(selected_choice.value)
+
+            await convo_state.save_changes(turn_context)
+
+        adapter = TestAdapter(exec_test)
+
+        convo_state = ConversationState(MemoryStorage())
+        dialog_state = convo_state.create_property("dialogState")
+        dialogs = DialogSet(dialog_state)
+
+        async def validator(prompt: PromptValidatorContext) -> bool:
+            assert prompt
+
+            if not prompt.recognized.succeeded:
+                await prompt.context.send_activity("Bad input.")
+
+            return prompt.recognized.succeeded
+
+        choice_prompt = ChoicePrompt(
+            "prompt", validator, default_locale=Culture.Spanish
+        )
+
+        dialogs.add(choice_prompt)
+
+        step1 = await adapter.send(Activity(type=ActivityTypes.message, text="Hello"))
+        step2 = await step1.assert_reply(
+            "Please choose a color. (1) red, (2) green, o (3) blue"
+        )
+        step3 = await step2.send(_invalid_message)
+        step4 = await step3.assert_reply("Bad input.")
+        step5 = await step4.send(Activity(type=ActivityTypes.message, text="red"))
+        await step5.assert_reply("red")
+
+    async def test_should_use_context_activity_locale_when_rendering_choices(self):
+        async def exec_test(turn_context: TurnContext):
+            dialog_context = await dialogs.create_context(turn_context)
+
+            results: DialogTurnResult = await dialog_context.continue_dialog()
+
+            if results.status == DialogTurnStatus.Empty:
+                options = PromptOptions(
+                    prompt=Activity(
+                        type=ActivityTypes.message, text="Please choose a color."
+                    ),
+                    choices=_color_choices,
+                )
+                await dialog_context.prompt("prompt", options)
+            elif results.status == DialogTurnStatus.Complete:
+                selected_choice = results.result
+                await turn_context.send_activity(selected_choice.value)
+
+            await convo_state.save_changes(turn_context)
+
+        adapter = TestAdapter(exec_test)
+
+        convo_state = ConversationState(MemoryStorage())
+        dialog_state = convo_state.create_property("dialogState")
+        dialogs = DialogSet(dialog_state)
+
+        async def validator(prompt: PromptValidatorContext) -> bool:
+            assert prompt
+
+            if not prompt.recognized.succeeded:
+                await prompt.context.send_activity("Bad input.")
+
+            return prompt.recognized.succeeded
+
+        choice_prompt = ChoicePrompt("prompt", validator)
+        dialogs.add(choice_prompt)
+
+        step1 = await adapter.send(
+            Activity(type=ActivityTypes.message, text="Hello", locale=Culture.Spanish)
+        )
+        step2 = await step1.assert_reply(
+            "Please choose a color. (1) red, (2) green, o (3) blue"
+        )
+        step3 = await step2.send(_answer_message)
+        await step3.assert_reply("red")
+
+    async def test_should_use_context_activity_locale_over_default_locale_when_rendering_choices(
+        self,
+    ):
+        async def exec_test(turn_context: TurnContext):
+            dialog_context = await dialogs.create_context(turn_context)
+
+            results: DialogTurnResult = await dialog_context.continue_dialog()
+
+            if results.status == DialogTurnStatus.Empty:
+                options = PromptOptions(
+                    prompt=Activity(
+                        type=ActivityTypes.message, text="Please choose a color."
+                    ),
+                    choices=_color_choices,
+                )
+                await dialog_context.prompt("prompt", options)
+            elif results.status == DialogTurnStatus.Complete:
+                selected_choice = results.result
+                await turn_context.send_activity(selected_choice.value)
+
+            await convo_state.save_changes(turn_context)
+
+        adapter = TestAdapter(exec_test)
+
+        convo_state = ConversationState(MemoryStorage())
+        dialog_state = convo_state.create_property("dialogState")
+        dialogs = DialogSet(dialog_state)
+
+        async def validator(prompt: PromptValidatorContext) -> bool:
+            assert prompt
+
+            if not prompt.recognized.succeeded:
+                await prompt.context.send_activity("Bad input.")
+
+            return prompt.recognized.succeeded
+
+        choice_prompt = ChoicePrompt(
+            "prompt", validator, default_locale=Culture.Spanish
+        )
+        dialogs.add(choice_prompt)
+
+        step1 = await adapter.send(
+            Activity(type=ActivityTypes.message, text="Hello", locale=Culture.English)
+        )
+        step2 = await step1.assert_reply(
+            "Please choose a color. (1) red, (2) green, or (3) blue"
+        )
+        step3 = await step2.send(_answer_message)
+        await step3.assert_reply("red")
+
+    async def test_should_not_render_choices_if_list_style_none_is_specified(self):
+        async def exec_test(turn_context: TurnContext):
+            dialog_context = await dialogs.create_context(turn_context)
+
+            results: DialogTurnResult = await dialog_context.continue_dialog()
+
+            if results.status == DialogTurnStatus.Empty:
+                options = PromptOptions(
+                    prompt=Activity(
+                        type=ActivityTypes.message, text="Please choose a color."
+                    ),
+                    choices=_color_choices,
+                    style=ListStyle.none,
+                )
+                await dialog_context.prompt("prompt", options)
+            elif results.status == DialogTurnStatus.Complete:
+                selected_choice = results.result
+                await turn_context.send_activity(selected_choice.value)
+
+            await convo_state.save_changes(turn_context)
+
+        adapter = TestAdapter(exec_test)
+
+        convo_state = ConversationState(MemoryStorage())
+        dialog_state = convo_state.create_property("dialogState")
+        dialogs = DialogSet(dialog_state)
+
+        choice_prompt = ChoicePrompt("prompt")
+
+        dialogs.add(choice_prompt)
+
+        step1 = await adapter.send("Hello")
+        step2 = await step1.assert_reply("Please choose a color.")
+        step3 = await step2.send(_answer_message)
+        await step3.assert_reply("red")
+
+    async def test_should_create_prompt_with_inline_choices_when_specified(self):
+        async def exec_test(turn_context: TurnContext):
+            dialog_context = await dialogs.create_context(turn_context)
+
+            results: DialogTurnResult = await dialog_context.continue_dialog()
+
+            if results.status == DialogTurnStatus.Empty:
+                options = PromptOptions(
+                    prompt=Activity(
+                        type=ActivityTypes.message, text="Please choose a color."
+                    ),
+                    choices=_color_choices,
+                )
+                await dialog_context.prompt("prompt", options)
+            elif results.status == DialogTurnStatus.Complete:
+                selected_choice = results.result
+                await turn_context.send_activity(selected_choice.value)
+
+            await convo_state.save_changes(turn_context)
+
+        adapter = TestAdapter(exec_test)
+
+        convo_state = ConversationState(MemoryStorage())
+        dialog_state = convo_state.create_property("dialogState")
+        dialogs = DialogSet(dialog_state)
+
+        choice_prompt = ChoicePrompt("prompt")
+        choice_prompt.style = ListStyle.in_line
+
+        dialogs.add(choice_prompt)
+
+        step1 = await adapter.send("Hello")
+        step2 = await step1.assert_reply(
+            "Please choose a color. (1) red, (2) green, or (3) blue"
+        )
+        step3 = await step2.send(_answer_message)
+        await step3.assert_reply("red")
+
+    async def test_should_create_prompt_with_list_choices_when_specified(self):
+        async def exec_test(turn_context: TurnContext):
+            dialog_context = await dialogs.create_context(turn_context)
+
+            results: DialogTurnResult = await dialog_context.continue_dialog()
+
+            if results.status == DialogTurnStatus.Empty:
+                options = PromptOptions(
+                    prompt=Activity(
+                        type=ActivityTypes.message, text="Please choose a color."
+                    ),
+                    choices=_color_choices,
+                )
+                await dialog_context.prompt("prompt", options)
+            elif results.status == DialogTurnStatus.Complete:
+                selected_choice = results.result
+                await turn_context.send_activity(selected_choice.value)
+
+            await convo_state.save_changes(turn_context)
+
+        adapter = TestAdapter(exec_test)
+
+        convo_state = ConversationState(MemoryStorage())
+        dialog_state = convo_state.create_property("dialogState")
+        dialogs = DialogSet(dialog_state)
+
+        choice_prompt = ChoicePrompt("prompt")
+        choice_prompt.style = ListStyle.list_style
+
+        dialogs.add(choice_prompt)
+
+        step1 = await adapter.send("Hello")
+        step2 = await step1.assert_reply(
+            "Please choose a color.\n\n   1. red\n   2. green\n   3. blue"
+        )
+        step3 = await step2.send(_answer_message)
+        await step3.assert_reply("red")
+
+    async def test_should_create_prompt_with_suggested_action_style_when_specified(
+        self,
+    ):
+        async def exec_test(turn_context: TurnContext):
+            dialog_context = await dialogs.create_context(turn_context)
+
+            results: DialogTurnResult = await dialog_context.continue_dialog()
+
+            if results.status == DialogTurnStatus.Empty:
+                options = PromptOptions(
+                    prompt=Activity(
+                        type=ActivityTypes.message, text="Please choose a color."
+                    ),
+                    choices=_color_choices,
+                    style=ListStyle.suggested_action,
+                )
+                await dialog_context.prompt("prompt", options)
+            elif results.status == DialogTurnStatus.Complete:
+                selected_choice = results.result
+                await turn_context.send_activity(selected_choice.value)
+
+            await convo_state.save_changes(turn_context)
+
+        adapter = TestAdapter(exec_test)
+
+        convo_state = ConversationState(MemoryStorage())
+        dialog_state = convo_state.create_property("dialogState")
+        dialogs = DialogSet(dialog_state)
+
+        choice_prompt = ChoicePrompt("prompt")
+
+        dialogs.add(choice_prompt)
+
+        step1 = await adapter.send("Hello")
+        step2 = await step1.assert_reply("Please choose a color.")
+        step3 = await step2.send(_answer_message)
+        await step3.assert_reply("red")
+
+    async def test_should_create_prompt_with_auto_style_when_specified(self):
+        async def exec_test(turn_context: TurnContext):
+            dialog_context = await dialogs.create_context(turn_context)
+
+            results: DialogTurnResult = await dialog_context.continue_dialog()
+
+            if results.status == DialogTurnStatus.Empty:
+                options = PromptOptions(
+                    prompt=Activity(
+                        type=ActivityTypes.message, text="Please choose a color."
+                    ),
+                    choices=_color_choices,
+                    style=ListStyle.auto,
+                )
+                await dialog_context.prompt("prompt", options)
+            elif results.status == DialogTurnStatus.Complete:
+                selected_choice = results.result
+                await turn_context.send_activity(selected_choice.value)
+
+            await convo_state.save_changes(turn_context)
+
+        adapter = TestAdapter(exec_test)
+
+        convo_state = ConversationState(MemoryStorage())
+        dialog_state = convo_state.create_property("dialogState")
+        dialogs = DialogSet(dialog_state)
+
+        choice_prompt = ChoicePrompt("prompt")
+
+        dialogs.add(choice_prompt)
+
+        step1 = await adapter.send("Hello")
+        step2 = await step1.assert_reply(
+            "Please choose a color. (1) red, (2) green, or (3) blue"
+        )
+        step3 = await step2.send(_answer_message)
+        await step3.assert_reply("red")
+
+    async def test_should_recognize_valid_number_choice(self):
+        async def exec_test(turn_context: TurnContext):
+            dialog_context = await dialogs.create_context(turn_context)
+
+            results: DialogTurnResult = await dialog_context.continue_dialog()
+
+            if results.status == DialogTurnStatus.Empty:
+                options = PromptOptions(
+                    prompt=Activity(
+                        type=ActivityTypes.message, text="Please choose a color."
+                    ),
+                    choices=_color_choices,
+                )
+                await dialog_context.prompt("prompt", options)
+            elif results.status == DialogTurnStatus.Complete:
+                selected_choice = results.result
+                await turn_context.send_activity(selected_choice.value)
+
+            await convo_state.save_changes(turn_context)
+
+        adapter = TestAdapter(exec_test)
+
+        convo_state = ConversationState(MemoryStorage())
+        dialog_state = convo_state.create_property("dialogState")
+        dialogs = DialogSet(dialog_state)
+
+        choice_prompt = ChoicePrompt("prompt")
+
+        dialogs.add(choice_prompt)
+
+        step1 = await adapter.send("Hello")
+        step2 = await step1.assert_reply(
+            "Please choose a color. (1) red, (2) green, or (3) blue"
+        )
+        step3 = await step2.send("1")
+        await step3.assert_reply("red")
+
+    async def test_should_display_choices_on_hero_card(self):
+        size_choices = ["large", "medium", "small"]
+
+        async def exec_test(turn_context: TurnContext):
+            dialog_context = await dialogs.create_context(turn_context)
+
+            results: DialogTurnResult = await dialog_context.continue_dialog()
+
+            if results.status == DialogTurnStatus.Empty:
+                options = PromptOptions(
+                    prompt=Activity(
+                        type=ActivityTypes.message, text="Please choose a size."
+                    ),
+                    choices=size_choices,
+                )
+                await dialog_context.prompt("prompt", options)
+            elif results.status == DialogTurnStatus.Complete:
+                selected_choice = results.result
+                await turn_context.send_activity(selected_choice.value)
+
+            await convo_state.save_changes(turn_context)
+
+        def assert_expected_activity(
+            activity: Activity, description
+        ):  # pylint: disable=unused-argument
+            assert len(activity.attachments) == 1
+            assert (
+                activity.attachments[0].content_type
+                == CardFactory.content_types.hero_card
+            )
+            assert activity.attachments[0].content.text == "Please choose a size."
+
+        adapter = TestAdapter(exec_test)
+
+        convo_state = ConversationState(MemoryStorage())
+        dialog_state = convo_state.create_property("dialogState")
+        dialogs = DialogSet(dialog_state)
+
+        choice_prompt = ChoicePrompt("prompt")
+
+        # Change the ListStyle of the prompt to ListStyle.none.
+        choice_prompt.style = ListStyle.hero_card
+
+        dialogs.add(choice_prompt)
+
+        step1 = await adapter.send("Hello")
+        step2 = await step1.assert_reply(assert_expected_activity)
+        step3 = await step2.send("1")
+        await step3.assert_reply(size_choices[0])
+
+    async def test_should_display_choices_on_hero_card_with_additional_attachment(self):
+        size_choices = ["large", "medium", "small"]
+        card = CardFactory.adaptive_card(
+            {
+                "type": "AdaptiveCard",
+                "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
+                "version": "1.2",
+                "body": [],
+            }
+        )
+        card_activity = Activity(attachments=[card])
+
+        async def exec_test(turn_context: TurnContext):
+            dialog_context = await dialogs.create_context(turn_context)
+
+            results: DialogTurnResult = await dialog_context.continue_dialog()
+
+            if results.status == DialogTurnStatus.Empty:
+                options = PromptOptions(prompt=card_activity, choices=size_choices)
+                await dialog_context.prompt("prompt", options)
+            elif results.status == DialogTurnStatus.Complete:
+                selected_choice = results.result
+                await turn_context.send_activity(selected_choice.value)
+
+            await convo_state.save_changes(turn_context)
+
+        def assert_expected_activity(
+            activity: Activity, description
+        ):  # pylint: disable=unused-argument
+            assert len(activity.attachments) == 2
+            assert (
+                activity.attachments[0].content_type
+                == CardFactory.content_types.adaptive_card
+            )
+            assert (
+                activity.attachments[1].content_type
+                == CardFactory.content_types.hero_card
+            )
+
+        adapter = TestAdapter(exec_test)
+
+        convo_state = ConversationState(MemoryStorage())
+        dialog_state = convo_state.create_property("dialogState")
+        dialogs = DialogSet(dialog_state)
+
+        choice_prompt = ChoicePrompt("prompt")
+
+        # Change the ListStyle of the prompt to ListStyle.none.
+        choice_prompt.style = ListStyle.hero_card
+
+        dialogs.add(choice_prompt)
+
+        step1 = await adapter.send("Hello")
+        await step1.assert_reply(assert_expected_activity)
+
+    async def test_should_not_find_a_choice_in_an_utterance_by_ordinal(self):
+        found = ChoiceRecognizers.recognize_choices(
+            "the first one please",
+            _color_choices,
+            FindChoicesOptions(recognize_numbers=False, recognize_ordinals=False),
+        )
+        assert not found
+
+    async def test_should_not_find_a_choice_in_an_utterance_by_numerical_index(self):
+        found = ChoiceRecognizers.recognize_choices(
+            "one",
+            _color_choices,
+            FindChoicesOptions(recognize_numbers=False, recognize_ordinals=False),
+        )
+        assert not found
diff --git a/libraries/botbuilder-dialogs/tests/test_dialog_manager.py b/libraries/botbuilder-dialogs/tests/test_dialog_manager.py
new file mode 100644
index 000000000..75f5b91a3
--- /dev/null
+++ b/libraries/botbuilder-dialogs/tests/test_dialog_manager.py
@@ -0,0 +1,357 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+# pylint: disable=pointless-string-statement
+
+from enum import Enum
+from typing import Callable, List, Tuple
+
+import aiounittest
+
+from botbuilder.core import (
+    AutoSaveStateMiddleware,
+    BotAdapter,
+    ConversationState,
+    MemoryStorage,
+    MessageFactory,
+    UserState,
+    TurnContext,
+)
+from botbuilder.core.adapters import TestAdapter
+from botbuilder.core.skills import SkillHandler, SkillConversationReference
+from botbuilder.dialogs import (
+    ComponentDialog,
+    Dialog,
+    DialogContext,
+    DialogEvents,
+    DialogInstance,
+    DialogReason,
+    TextPrompt,
+    WaterfallDialog,
+    DialogManager,
+    DialogManagerResult,
+    DialogTurnStatus,
+    WaterfallStepContext,
+)
+from botbuilder.dialogs.prompts import PromptOptions
+from botbuilder.schema import (
+    Activity,
+    ActivityTypes,
+    ChannelAccount,
+    ConversationAccount,
+    EndOfConversationCodes,
+    InputHints,
+)
+from botframework.connector.auth import AuthenticationConstants, ClaimsIdentity
+
+
+class SkillFlowTestCase(str, Enum):
+    # DialogManager is executing on a root bot with no skills (typical standalone bot).
+    root_bot_only = "RootBotOnly"
+
+    # DialogManager is executing on a root bot handling replies from a skill.
+    root_bot_consuming_skill = "RootBotConsumingSkill"
+
+    # DialogManager is executing in a skill that is called from a root and calling another skill.
+    middle_skill = "MiddleSkill"
+
+    # DialogManager is executing in a skill that is called from a parent (a root or another skill) but doesn"t call
+    # another skill.
+    leaf_skill = "LeafSkill"
+
+
+class SimpleComponentDialog(ComponentDialog):
+    # An App ID for a parent bot.
+    parent_bot_id = "00000000-0000-0000-0000-0000000000PARENT"
+
+    # An App ID for a skill bot.
+    skill_bot_id = "00000000-0000-0000-0000-00000000000SKILL"
+
+    # Captures an EndOfConversation if it was sent to help with assertions.
+    eoc_sent: Activity = None
+
+    # Property to capture the DialogManager turn results and do assertions.
+    dm_turn_result: DialogManagerResult = None
+
+    def __init__(
+        self, id: str = None, prop: str = None
+    ):  # pylint: disable=unused-argument
+        super().__init__(id or "SimpleComponentDialog")
+        self.text_prompt = "TextPrompt"
+        self.waterfall_dialog = "WaterfallDialog"
+        self.add_dialog(TextPrompt(self.text_prompt))
+        self.add_dialog(
+            WaterfallDialog(
+                self.waterfall_dialog, [self.prompt_for_name, self.final_step,]
+            )
+        )
+        self.initial_dialog_id = self.waterfall_dialog
+        self.end_reason = None
+
+    @staticmethod
+    async def create_test_flow(
+        dialog: Dialog,
+        test_case: SkillFlowTestCase = SkillFlowTestCase.root_bot_only,
+        enabled_trace=False,
+    ) -> TestAdapter:
+        conversation_id = "testFlowConversationId"
+        storage = MemoryStorage()
+        conversation_state = ConversationState(storage)
+        user_state = UserState(storage)
+
+        activity = Activity(
+            channel_id="test",
+            service_url="https://test.com",
+            from_property=ChannelAccount(id="user1", name="User1"),
+            recipient=ChannelAccount(id="bot", name="Bot"),
+            conversation=ConversationAccount(
+                is_group=False, conversation_type=conversation_id, id=conversation_id
+            ),
+        )
+
+        dialog_manager = DialogManager(dialog)
+        dialog_manager.user_state = user_state
+        dialog_manager.conversation_state = conversation_state
+
+        async def logic(context: TurnContext):
+            if test_case != SkillFlowTestCase.root_bot_only:
+                # Create a skill ClaimsIdentity and put it in turn_state so isSkillClaim() returns True.
+                claims_identity = ClaimsIdentity({}, False)
+                claims_identity.claims[
+                    "ver"
+                ] = "2.0"  # AuthenticationConstants.VersionClaim
+                claims_identity.claims[
+                    "aud"
+                ] = (
+                    SimpleComponentDialog.skill_bot_id
+                )  # AuthenticationConstants.AudienceClaim
+                claims_identity.claims[
+                    "azp"
+                ] = (
+                    SimpleComponentDialog.parent_bot_id
+                )  # AuthenticationConstants.AuthorizedParty
+                context.turn_state[BotAdapter.BOT_IDENTITY_KEY] = claims_identity
+
+                if test_case == SkillFlowTestCase.root_bot_consuming_skill:
+                    # Simulate the SkillConversationReference with a channel OAuthScope stored in turn_state.
+                    # This emulates a response coming to a root bot through SkillHandler.
+                    context.turn_state[
+                        SkillHandler.SKILL_CONVERSATION_REFERENCE_KEY
+                    ] = SkillConversationReference(
+                        None, AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
+                    )
+
+                if test_case == SkillFlowTestCase.middle_skill:
+                    # Simulate the SkillConversationReference with a parent Bot ID stored in turn_state.
+                    # This emulates a response coming to a skill from another skill through SkillHandler.
+                    context.turn_state[
+                        SkillHandler.SKILL_CONVERSATION_REFERENCE_KEY
+                    ] = SkillConversationReference(
+                        None, SimpleComponentDialog.parent_bot_id
+                    )
+
+            async def aux(
+                turn_context: TurnContext,  # pylint: disable=unused-argument
+                activities: List[Activity],
+                next: Callable,
+            ):
+                for activity in activities:
+                    if activity.type == ActivityTypes.end_of_conversation:
+                        SimpleComponentDialog.eoc_sent = activity
+                        break
+
+                return await next()
+
+            # Interceptor to capture the EoC activity if it was sent so we can assert it in the tests.
+            context.on_send_activities(aux)
+
+            SimpleComponentDialog.dm_turn_result = await dialog_manager.on_turn(context)
+
+        adapter = TestAdapter(logic, activity, enabled_trace)
+        adapter.use(AutoSaveStateMiddleware([user_state, conversation_state]))
+
+        return adapter
+
+    async def on_end_dialog(
+        self, context: DialogContext, instance: DialogInstance, reason: DialogReason
+    ):
+        self.end_reason = reason
+        return await super().on_end_dialog(context, instance, reason)
+
+    async def prompt_for_name(self, step: WaterfallStepContext):
+        return await step.prompt(
+            self.text_prompt,
+            PromptOptions(
+                prompt=MessageFactory.text(
+                    "Hello, what is your name?", None, InputHints.expecting_input
+                ),
+                retry_prompt=MessageFactory.text(
+                    "Hello, what is your name again?", None, InputHints.expecting_input
+                ),
+            ),
+        )
+
+    async def final_step(self, step: WaterfallStepContext):
+        await step.context.send_activity(f"Hello { step.result }, nice to meet you!")
+        return await step.end_dialog(step.result)
+
+
+class DialogManagerTests(aiounittest.AsyncTestCase):
+    """
+    self.beforeEach(() => {
+        _dmTurnResult = undefined
+    })
+    """
+
+    async def test_handles_bot_and_skills(self):
+        construction_data: List[Tuple[SkillFlowTestCase, bool]] = [
+            (SkillFlowTestCase.root_bot_only, False),
+            (SkillFlowTestCase.root_bot_consuming_skill, False),
+            (SkillFlowTestCase.middle_skill, True),
+            (SkillFlowTestCase.leaf_skill, True),
+        ]
+
+        for test_case, should_send_eoc in construction_data:
+            with self.subTest(test_case=test_case, should_send_eoc=should_send_eoc):
+                SimpleComponentDialog.dm_turn_result = None
+                SimpleComponentDialog.eoc_sent = None
+                dialog = SimpleComponentDialog()
+                test_flow = await SimpleComponentDialog.create_test_flow(
+                    dialog, test_case
+                )
+                step1 = await test_flow.send("Hi")
+                step2 = await step1.assert_reply("Hello, what is your name?")
+                step3 = await step2.send("SomeName")
+                await step3.assert_reply("Hello SomeName, nice to meet you!")
+
+                self.assertEqual(
+                    SimpleComponentDialog.dm_turn_result.turn_result.status,
+                    DialogTurnStatus.Complete,
+                )
+
+                self.assertEqual(dialog.end_reason, DialogReason.EndCalled)
+                if should_send_eoc:
+                    self.assertTrue(
+                        bool(SimpleComponentDialog.eoc_sent),
+                        "Skills should send EndConversation to channel",
+                    )
+                    self.assertEqual(
+                        SimpleComponentDialog.eoc_sent.type,
+                        ActivityTypes.end_of_conversation,
+                    )
+                    self.assertEqual(
+                        SimpleComponentDialog.eoc_sent.code,
+                        EndOfConversationCodes.completed_successfully,
+                    )
+                    self.assertEqual(SimpleComponentDialog.eoc_sent.value, "SomeName")
+                else:
+                    self.assertIsNone(
+                        SimpleComponentDialog.eoc_sent,
+                        "Root bot should not send EndConversation to channel",
+                    )
+
+    async def test_skill_handles_eoc_from_parent(self):
+        SimpleComponentDialog.dm_turn_result = None
+        dialog = SimpleComponentDialog()
+        test_flow = await SimpleComponentDialog.create_test_flow(
+            dialog, SkillFlowTestCase.leaf_skill
+        )
+
+        step1 = await test_flow.send("Hi")
+        step2 = await step1.assert_reply("Hello, what is your name?")
+        await step2.send(Activity(type=ActivityTypes.end_of_conversation))
+
+        self.assertEqual(
+            SimpleComponentDialog.dm_turn_result.turn_result.status,
+            DialogTurnStatus.Cancelled,
+        )
+
+    async def test_skill_handles_reprompt_from_parent(self):
+        SimpleComponentDialog.dm_turn_result = None
+        dialog = SimpleComponentDialog()
+        test_flow = await SimpleComponentDialog.create_test_flow(
+            dialog, SkillFlowTestCase.leaf_skill
+        )
+
+        step1 = await test_flow.send("Hi")
+        step2 = await step1.assert_reply("Hello, what is your name?")
+        step3 = await step2.send(
+            Activity(type=ActivityTypes.event, name=DialogEvents.reprompt_dialog)
+        )
+        await step3.assert_reply("Hello, what is your name?")
+
+        self.assertEqual(
+            SimpleComponentDialog.dm_turn_result.turn_result.status,
+            DialogTurnStatus.Waiting,
+        )
+
+    async def test_skill_should_return_empty_on_reprompt_with_no_dialog(self):
+        SimpleComponentDialog.dm_turn_result = None
+        dialog = SimpleComponentDialog()
+        test_flow = await SimpleComponentDialog.create_test_flow(
+            dialog, SkillFlowTestCase.leaf_skill
+        )
+
+        await test_flow.send(
+            Activity(type=ActivityTypes.event, name=DialogEvents.reprompt_dialog)
+        )
+
+        self.assertEqual(
+            SimpleComponentDialog.dm_turn_result.turn_result.status,
+            DialogTurnStatus.Empty,
+        )
+
+    async def test_trace_skill_state(self):
+        SimpleComponentDialog.dm_turn_result = None
+        dialog = SimpleComponentDialog()
+
+        def assert_is_trace(activity, description):  # pylint: disable=unused-argument
+            assert activity.type == ActivityTypes.trace
+
+        def assert_is_trace_and_label(activity, description):
+            assert_is_trace(activity, description)
+            assert activity.label == "Skill State"
+
+        test_flow = await SimpleComponentDialog.create_test_flow(
+            dialog, SkillFlowTestCase.leaf_skill, True
+        )
+
+        step1 = await test_flow.send("Hi")
+        step2 = await step1.assert_reply(assert_is_trace)
+        step2 = await step2.assert_reply("Hello, what is your name?")
+        step3 = await step2.assert_reply(assert_is_trace_and_label)
+        step4 = await step3.send("SomeName")
+        step5 = await step4.assert_reply("Hello SomeName, nice to meet you!")
+        step6 = await step5.assert_reply(assert_is_trace_and_label)
+        await step6.assert_reply(assert_is_trace)
+
+        self.assertEqual(
+            SimpleComponentDialog.dm_turn_result.turn_result.status,
+            DialogTurnStatus.Complete,
+        )
+
+    async def test_trace_bot_state(self):
+        SimpleComponentDialog.dm_turn_result = None
+        dialog = SimpleComponentDialog()
+
+        def assert_is_trace(activity, description):  # pylint: disable=unused-argument
+            assert activity.type == ActivityTypes.trace
+
+        def assert_is_trace_and_label(activity, description):
+            assert_is_trace(activity, description)
+            assert activity.label == "Bot State"
+
+        test_flow = await SimpleComponentDialog.create_test_flow(
+            dialog, SkillFlowTestCase.root_bot_only, True
+        )
+
+        step1 = await test_flow.send("Hi")
+        step2 = await step1.assert_reply("Hello, what is your name?")
+        step3 = await step2.assert_reply(assert_is_trace_and_label)
+        step4 = await step3.send("SomeName")
+        step5 = await step4.assert_reply("Hello SomeName, nice to meet you!")
+        await step5.assert_reply(assert_is_trace_and_label)
+
+        self.assertEqual(
+            SimpleComponentDialog.dm_turn_result.turn_result.status,
+            DialogTurnStatus.Complete,
+        )
diff --git a/libraries/botbuilder-dialogs/tests/test_dialogextensions.py b/libraries/botbuilder-dialogs/tests/test_dialogextensions.py
new file mode 100644
index 000000000..3c3e8ecec
--- /dev/null
+++ b/libraries/botbuilder-dialogs/tests/test_dialogextensions.py
@@ -0,0 +1,228 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+# pylint: disable=ungrouped-imports
+import enum
+import uuid
+
+import aiounittest
+
+from botframework.connector.auth import ClaimsIdentity, AuthenticationConstants
+from botbuilder.core import (
+    TurnContext,
+    MessageFactory,
+    MemoryStorage,
+    ConversationState,
+    UserState,
+    AdapterExtensions,
+    BotAdapter,
+)
+from botbuilder.core.adapters import (
+    TestFlow,
+    TestAdapter,
+)
+from botbuilder.core.skills import (
+    SkillHandler,
+    SkillConversationReference,
+)
+from botbuilder.core.transcript_logger import (
+    TranscriptLoggerMiddleware,
+    ConsoleTranscriptLogger,
+)
+from botbuilder.schema import ActivityTypes, Activity, EndOfConversationCodes
+from botbuilder.dialogs import (
+    ComponentDialog,
+    TextPrompt,
+    WaterfallDialog,
+    DialogInstance,
+    DialogReason,
+    WaterfallStepContext,
+    PromptOptions,
+    Dialog,
+    DialogExtensions,
+    DialogEvents,
+)
+
+
+class SimpleComponentDialog(ComponentDialog):
+    def __init__(self):
+        super().__init__("SimpleComponentDialog")
+
+        self.add_dialog(TextPrompt("TextPrompt"))
+        self.add_dialog(
+            WaterfallDialog("WaterfallDialog", [self.prompt_for_name, self.final_step])
+        )
+
+        self.initial_dialog_id = "WaterfallDialog"
+        self.end_reason = DialogReason.BeginCalled
+
+    async def end_dialog(
+        self, context: TurnContext, instance: DialogInstance, reason: DialogReason
+    ) -> None:
+        self.end_reason = reason
+        return await super().end_dialog(context, instance, reason)
+
+    async def prompt_for_name(self, step_context: WaterfallStepContext):
+        return await step_context.prompt(
+            "TextPrompt",
+            PromptOptions(
+                prompt=MessageFactory.text("Hello, what is your name?"),
+                retry_prompt=MessageFactory.text("Hello, what is your name again?"),
+            ),
+        )
+
+    async def final_step(self, step_context: WaterfallStepContext):
+        await step_context.context.send_activity(
+            f"Hello {step_context.result}, nice to meet you!"
+        )
+        return await step_context.end_dialog(step_context.result)
+
+
+class FlowTestCase(enum.Enum):
+    root_bot_only = 1
+    root_bot_consuming_skill = 2
+    middle_skill = 3
+    leaf_skill = 4
+
+
+class DialogExtensionsTests(aiounittest.AsyncTestCase):
+    def __init__(self, methodName):
+        super().__init__(methodName)
+        self.eoc_sent: Activity = None
+        self.skill_bot_id = str(uuid.uuid4())
+        self.parent_bot_id = str(uuid.uuid4())
+
+    async def handles_bot_and_skills_test_cases(
+        self, test_case: FlowTestCase, send_eoc: bool
+    ):
+        dialog = SimpleComponentDialog()
+
+        test_flow = self.create_test_flow(dialog, test_case)
+
+        await test_flow.send("Hi")
+        await test_flow.assert_reply("Hello, what is your name?")
+        await test_flow.send("SomeName")
+        await test_flow.assert_reply("Hello SomeName, nice to meet you!")
+
+        assert dialog.end_reason == DialogReason.EndCalled
+
+        if send_eoc:
+            self.assertIsNotNone(
+                self.eoc_sent, "Skills should send EndConversation to channel"
+            )
+            assert ActivityTypes.end_of_conversation == self.eoc_sent.type
+            assert EndOfConversationCodes.completed_successfully == self.eoc_sent.code
+            assert self.eoc_sent.value == "SomeName"
+        else:
+            self.assertIsNone(
+                self.eoc_sent, "Root bot should not send EndConversation to channel"
+            )
+
+    async def test_handles_root_bot_only(self):
+        return await self.handles_bot_and_skills_test_cases(
+            FlowTestCase.root_bot_only, False
+        )
+
+    async def test_handles_root_bot_consuming_skill(self):
+        return await self.handles_bot_and_skills_test_cases(
+            FlowTestCase.root_bot_consuming_skill, False
+        )
+
+    async def test_handles_middle_skill(self):
+        return await self.handles_bot_and_skills_test_cases(
+            FlowTestCase.middle_skill, True
+        )
+
+    async def test_handles_leaf_skill(self):
+        return await self.handles_bot_and_skills_test_cases(
+            FlowTestCase.leaf_skill, True
+        )
+
+    async def test_skill_handles_eoc_from_parent(self):
+        dialog = SimpleComponentDialog()
+        test_flow = self.create_test_flow(dialog, FlowTestCase.leaf_skill)
+
+        await test_flow.send("Hi")
+        await test_flow.assert_reply("Hello, what is your name?")
+        await test_flow.send(
+            Activity(
+                type=ActivityTypes.end_of_conversation, caller_id=self.parent_bot_id,
+            )
+        )
+
+        self.assertIsNone(
+            self.eoc_sent,
+            "Skill should not send back EoC when an EoC is sent from a parent",
+        )
+        assert dialog.end_reason == DialogReason.CancelCalled
+
+    async def test_skill_handles_reprompt_from_parent(self):
+        dialog = SimpleComponentDialog()
+        test_flow = self.create_test_flow(dialog, FlowTestCase.leaf_skill)
+
+        await test_flow.send("Hi")
+        await test_flow.assert_reply("Hello, what is your name?")
+        await test_flow.send(
+            Activity(
+                type=ActivityTypes.event,
+                caller_id=self.parent_bot_id,
+                name=DialogEvents.reprompt_dialog,
+            )
+        )
+        await test_flow.assert_reply("Hello, what is your name?")
+
+        assert dialog.end_reason == DialogReason.BeginCalled
+
+    def create_test_flow(self, dialog: Dialog, test_case: FlowTestCase) -> TestFlow:
+        conversation_id = str(uuid.uuid4())
+        storage = MemoryStorage()
+        convo_state = ConversationState(storage)
+        user_state = UserState(storage)
+
+        async def logic(context: TurnContext):
+            if test_case != FlowTestCase.root_bot_only:
+                claims_identity = ClaimsIdentity(
+                    {
+                        AuthenticationConstants.VERSION_CLAIM: "2.0",
+                        AuthenticationConstants.AUDIENCE_CLAIM: self.skill_bot_id,
+                        AuthenticationConstants.AUTHORIZED_PARTY: self.parent_bot_id,
+                    },
+                    True,
+                )
+                context.turn_state[BotAdapter.BOT_IDENTITY_KEY] = claims_identity
+
+                if test_case == FlowTestCase.root_bot_consuming_skill:
+                    context.turn_state[
+                        SkillHandler.SKILL_CONVERSATION_REFERENCE_KEY
+                    ] = SkillConversationReference(
+                        None, AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
+                    )
+
+                if test_case == FlowTestCase.middle_skill:
+                    context.turn_state[
+                        SkillHandler.SKILL_CONVERSATION_REFERENCE_KEY
+                    ] = SkillConversationReference(None, self.parent_bot_id)
+
+            async def capture_eoc(
+                inner_context: TurnContext, activities: [], next
+            ):  # pylint: disable=unused-argument
+                for activity in activities:
+                    if activity.type == ActivityTypes.end_of_conversation:
+                        self.eoc_sent = activity
+                        break
+                return await next()
+
+            context.on_send_activities(capture_eoc)
+
+            await DialogExtensions.run_dialog(
+                dialog, context, convo_state.create_property("DialogState")
+            )
+
+        adapter = TestAdapter(
+            logic, TestAdapter.create_conversation_reference(conversation_id)
+        )
+        AdapterExtensions.use_storage(adapter, storage)
+        AdapterExtensions.use_bot_state(adapter, user_state, convo_state)
+        adapter.use(TranscriptLoggerMiddleware(ConsoleTranscriptLogger()))
+
+        return TestFlow(None, adapter)
diff --git a/libraries/botbuilder-dialogs/tests/test_oauth_prompt.py b/libraries/botbuilder-dialogs/tests/test_oauth_prompt.py
index a5802103a..a6b22553b 100644
--- a/libraries/botbuilder-dialogs/tests/test_oauth_prompt.py
+++ b/libraries/botbuilder-dialogs/tests/test_oauth_prompt.py
@@ -9,6 +9,7 @@
     ChannelAccount,
     ConversationAccount,
     InputHints,
+    SignInConstants,
     TokenResponse,
 )
 
@@ -260,3 +261,156 @@ async def callback_handler(turn_context: TurnContext):
 
         await adapter.send("Hello")
         self.assertTrue(called)
+
+    async def test_should_end_oauth_prompt_on_invalid_message_when_end_on_invalid_message(
+        self,
+    ):
+        connection_name = "myConnection"
+        token = "abc123"
+        magic_code = "888999"
+
+        async def exec_test(turn_context: TurnContext):
+            dialog_context = await dialogs.create_context(turn_context)
+
+            results = await dialog_context.continue_dialog()
+
+            if results.status == DialogTurnStatus.Empty:
+                await dialog_context.prompt("prompt", PromptOptions())
+            elif results.status == DialogTurnStatus.Complete:
+                if results.result and results.result.token:
+                    await turn_context.send_activity("Failed")
+
+                else:
+                    await turn_context.send_activity("Ended")
+
+            await convo_state.save_changes(turn_context)
+
+        # Initialize TestAdapter.
+        adapter = TestAdapter(exec_test)
+
+        # Create ConversationState with MemoryStorage and register the state as middleware.
+        convo_state = ConversationState(MemoryStorage())
+
+        # Create a DialogState property, DialogSet and AttachmentPrompt.
+        dialog_state = convo_state.create_property("dialog_state")
+        dialogs = DialogSet(dialog_state)
+        dialogs.add(
+            OAuthPrompt(
+                "prompt",
+                OAuthPromptSettings(connection_name, "Login", None, 300000, None, True),
+            )
+        )
+
+        def inspector(
+            activity: Activity, description: str = None
+        ):  # pylint: disable=unused-argument
+            assert len(activity.attachments) == 1
+            assert (
+                activity.attachments[0].content_type
+                == CardFactory.content_types.oauth_card
+            )
+
+            # send a mock EventActivity back to the bot with the token
+            adapter.add_user_token(
+                connection_name,
+                activity.channel_id,
+                activity.recipient.id,
+                token,
+                magic_code,
+            )
+
+        step1 = await adapter.send("Hello")
+        step2 = await step1.assert_reply(inspector)
+        step3 = await step2.send("test invalid message")
+        await step3.assert_reply("Ended")
+
+    async def test_should_timeout_oauth_prompt_with_message_activity(self,):
+        activity = Activity(type=ActivityTypes.message, text="any")
+        await self.run_timeout_test(activity)
+
+    async def test_should_timeout_oauth_prompt_with_token_response_event_activity(
+        self,
+    ):
+        activity = Activity(
+            type=ActivityTypes.event, name=SignInConstants.token_response_event_name
+        )
+        await self.run_timeout_test(activity)
+
+    async def test_should_timeout_oauth_prompt_with_verify_state_operation_activity(
+        self,
+    ):
+        activity = Activity(
+            type=ActivityTypes.invoke, name=SignInConstants.verify_state_operation_name
+        )
+        await self.run_timeout_test(activity)
+
+    async def test_should_not_timeout_oauth_prompt_with_custom_event_activity(self,):
+        activity = Activity(type=ActivityTypes.event, name="custom event name")
+        await self.run_timeout_test(activity, False, "Ended", "Failed")
+
+    async def run_timeout_test(
+        self,
+        activity: Activity,
+        should_succeed: bool = True,
+        token_response: str = "Failed",
+        no_token_resonse="Ended",
+    ):
+        connection_name = "myConnection"
+        token = "abc123"
+        magic_code = "888999"
+
+        async def exec_test(turn_context: TurnContext):
+            dialog_context = await dialogs.create_context(turn_context)
+
+            results = await dialog_context.continue_dialog()
+
+            if results.status == DialogTurnStatus.Empty:
+                await dialog_context.prompt("prompt", PromptOptions())
+            elif results.status == DialogTurnStatus.Complete or (
+                results.status == DialogTurnStatus.Waiting and not should_succeed
+            ):
+                if results.result and results.result.token:
+                    await turn_context.send_activity(token_response)
+
+                else:
+                    await turn_context.send_activity(no_token_resonse)
+
+            await convo_state.save_changes(turn_context)
+
+        # Initialize TestAdapter.
+        adapter = TestAdapter(exec_test)
+
+        # Create ConversationState with MemoryStorage and register the state as middleware.
+        convo_state = ConversationState(MemoryStorage())
+
+        # Create a DialogState property, DialogSet and AttachmentPrompt.
+        dialog_state = convo_state.create_property("dialog_state")
+        dialogs = DialogSet(dialog_state)
+        dialogs.add(
+            OAuthPrompt(
+                "prompt", OAuthPromptSettings(connection_name, "Login", None, 1),
+            )
+        )
+
+        def inspector(
+            activity: Activity, description: str = None
+        ):  # pylint: disable=unused-argument
+            assert len(activity.attachments) == 1
+            assert (
+                activity.attachments[0].content_type
+                == CardFactory.content_types.oauth_card
+            )
+
+            # send a mock EventActivity back to the bot with the token
+            adapter.add_user_token(
+                connection_name,
+                activity.channel_id,
+                activity.recipient.id,
+                token,
+                magic_code,
+            )
+
+        step1 = await adapter.send("Hello")
+        step2 = await step1.assert_reply(inspector)
+        step3 = await step2.send(activity)
+        await step3.assert_reply(no_token_resonse)
diff --git a/libraries/botbuilder-dialogs/tests/test_object_path.py b/libraries/botbuilder-dialogs/tests/test_object_path.py
new file mode 100644
index 000000000..447f52893
--- /dev/null
+++ b/libraries/botbuilder-dialogs/tests/test_object_path.py
@@ -0,0 +1,225 @@
+import aiounittest
+
+from botbuilder.dialogs import ObjectPath
+
+
+class Location:
+    def __init__(self, lat: float = None, long: float = None):
+        self.lat = lat
+        self.long = long
+
+
+class Options:
+    def __init__(
+        self,
+        first_name: str = None,
+        last_name: str = None,
+        age: int = None,
+        boolean: bool = None,
+        dictionary: dict = None,
+        location: Location = None,
+    ):
+        self.first_name = first_name
+        self.last_name = last_name
+        self.age = age
+        self.boolean = boolean
+        self.dictionary = dictionary
+        self.location = location
+
+
+class ObjectPathTests(aiounittest.AsyncTestCase):
+    async def test_typed_only_default(self):
+        default_options = Options(
+            last_name="Smith",
+            first_name="Fred",
+            age=22,
+            location=Location(lat=1.2312312, long=3.234234,),
+        )
+
+        overlay = Options()
+
+        result = ObjectPath.assign(default_options, overlay)
+        assert result.last_name == default_options.last_name
+        assert result.first_name == default_options.first_name
+        assert result.age == default_options.age
+        assert result.boolean == default_options.boolean
+        assert result.location.lat == default_options.location.lat
+        assert result.location.long == default_options.location.long
+
+    async def test_typed_only_overlay(self):
+        default_options = Options()
+
+        overlay = Options(
+            last_name="Smith",
+            first_name="Fred",
+            age=22,
+            location=Location(lat=1.2312312, long=3.234234,),
+        )
+
+        result = ObjectPath.assign(default_options, overlay)
+        assert result.last_name == overlay.last_name
+        assert result.first_name == overlay.first_name
+        assert result.age == overlay.age
+        assert result.boolean == overlay.boolean
+        assert result.location.lat == overlay.location.lat
+        assert result.location.long == overlay.location.long
+
+    async def test_typed_full_overlay(self):
+        default_options = Options(
+            last_name="Smith",
+            first_name="Fred",
+            age=22,
+            location=Location(lat=1.2312312, long=3.234234,),
+            dictionary={"one": 1, "two": 2},
+        )
+
+        overlay = Options(
+            last_name="Grant",
+            first_name="Eddit",
+            age=32,
+            location=Location(lat=2.2312312, long=2.234234,),
+            dictionary={"one": 99, "three": 3},
+        )
+
+        result = ObjectPath.assign(default_options, overlay)
+        assert result.last_name == overlay.last_name
+        assert result.first_name == overlay.first_name
+        assert result.age == overlay.age
+        assert result.boolean == overlay.boolean
+        assert result.location.lat == overlay.location.lat
+        assert result.location.long == overlay.location.long
+        assert "one" in result.dictionary
+        assert result.dictionary["one"] == 99
+        assert "two" in result.dictionary
+        assert "three" in result.dictionary
+
+    async def test_typed_partial_overlay(self):
+        default_options = Options(
+            last_name="Smith",
+            first_name="Fred",
+            age=22,
+            location=Location(lat=1.2312312, long=3.234234,),
+        )
+
+        overlay = Options(last_name="Grant",)
+
+        result = ObjectPath.assign(default_options, overlay)
+        assert result.last_name == overlay.last_name
+        assert result.first_name == default_options.first_name
+        assert result.age == default_options.age
+        assert result.boolean == default_options.boolean
+        assert result.location.lat == default_options.location.lat
+        assert result.location.long == default_options.location.long
+
+    async def test_typed_no_target(self):
+        overlay = Options(
+            last_name="Smith",
+            first_name="Fred",
+            age=22,
+            location=Location(lat=1.2312312, long=3.234234,),
+        )
+
+        result = ObjectPath.assign(None, overlay)
+        assert result.last_name == overlay.last_name
+        assert result.first_name == overlay.first_name
+        assert result.age == overlay.age
+        assert result.boolean == overlay.boolean
+        assert result.location.lat == overlay.location.lat
+        assert result.location.long == overlay.location.long
+
+    async def test_typed_no_overlay(self):
+        default_options = Options(
+            last_name="Smith",
+            first_name="Fred",
+            age=22,
+            location=Location(lat=1.2312312, long=3.234234,),
+        )
+
+        result = ObjectPath.assign(default_options, None)
+        assert result.last_name == default_options.last_name
+        assert result.first_name == default_options.first_name
+        assert result.age == default_options.age
+        assert result.boolean == default_options.boolean
+        assert result.location.lat == default_options.location.lat
+        assert result.location.long == default_options.location.long
+
+    async def test_no_target_or_overlay(self):
+        result = ObjectPath.assign(None, None, Options)
+        assert result
+
+    async def test_dict_partial_overlay(self):
+        default_options = {
+            "last_name": "Smith",
+            "first_name": "Fred",
+            "age": 22,
+            "location": Location(lat=1.2312312, long=3.234234,),
+        }
+
+        overlay = {
+            "last_name": "Grant",
+        }
+
+        result = ObjectPath.assign(default_options, overlay)
+        assert result["last_name"] == overlay["last_name"]
+        assert result["first_name"] == default_options["first_name"]
+        assert result["age"] == default_options["age"]
+        assert result["location"].lat == default_options["location"].lat
+        assert result["location"].long == default_options["location"].long
+
+    async def test_dict_to_typed_overlay(self):
+        default_options = Options(
+            last_name="Smith",
+            first_name="Fred",
+            age=22,
+            location=Location(lat=1.2312312, long=3.234234,),
+        )
+
+        overlay = {
+            "last_name": "Grant",
+        }
+
+        result = ObjectPath.assign(default_options, overlay)
+        assert result.last_name == overlay["last_name"]
+        assert result.first_name == default_options.first_name
+        assert result.age == default_options.age
+        assert result.boolean == default_options.boolean
+        assert result.location.lat == default_options.location.lat
+        assert result.location.long == default_options.location.long
+
+    async def test_set_value(self):
+        test = {}
+        ObjectPath.set_path_value(test, "x.y.z", 15)
+        ObjectPath.set_path_value(test, "x.p", "hello")
+        ObjectPath.set_path_value(test, "foo", {"Bar": 15, "Blat": "yo"})
+        ObjectPath.set_path_value(test, "x.a[1]", "yabba")
+        ObjectPath.set_path_value(test, "x.a[0]", "dabba")
+        ObjectPath.set_path_value(test, "null", None)
+
+        assert ObjectPath.get_path_value(test, "x.y.z") == 15
+        assert ObjectPath.get_path_value(test, "x.p") == "hello"
+        assert ObjectPath.get_path_value(test, "foo.bar") == 15
+
+        assert not ObjectPath.try_get_path_value(test, "foo.Blatxxx")
+        assert ObjectPath.try_get_path_value(test, "x.a[1]") == "yabba"
+        assert ObjectPath.try_get_path_value(test, "x.a[0]") == "dabba"
+
+        assert not ObjectPath.try_get_path_value(test, "null")
+
+    async def test_remove_path_value(self):
+        test = {}
+        ObjectPath.set_path_value(test, "x.y.z", 15)
+        ObjectPath.set_path_value(test, "x.p", "hello")
+        ObjectPath.set_path_value(test, "foo", {"Bar": 15, "Blat": "yo"})
+        ObjectPath.set_path_value(test, "x.a[1]", "yabba")
+        ObjectPath.set_path_value(test, "x.a[0]", "dabba")
+
+        ObjectPath.remove_path_value(test, "x.y.z")
+        with self.assertRaises(KeyError):
+            ObjectPath.get_path_value(test, "x.y.z")
+
+        assert ObjectPath.get_path_value(test, "x.y.z", 99) == 99
+
+        ObjectPath.remove_path_value(test, "x.a[1]")
+        assert not ObjectPath.try_get_path_value(test, "x.a[1]")
+
+        assert ObjectPath.try_get_path_value(test, "x.a[0]") == "dabba"
diff --git a/libraries/botbuilder-dialogs/tests/test_skill_dialog.py b/libraries/botbuilder-dialogs/tests/test_skill_dialog.py
new file mode 100644
index 000000000..91b6dfcba
--- /dev/null
+++ b/libraries/botbuilder-dialogs/tests/test_skill_dialog.py
@@ -0,0 +1,639 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+import uuid
+from http import HTTPStatus
+from typing import Callable, Union, List
+from unittest.mock import Mock
+
+import aiounittest
+from botframework.connector.token_api.models import TokenExchangeResource
+from botbuilder.core import (
+    ConversationState,
+    MemoryStorage,
+    InvokeResponse,
+    TurnContext,
+    MessageFactory,
+)
+from botbuilder.core.card_factory import ContentTypes
+from botbuilder.core.skills import (
+    BotFrameworkSkill,
+    ConversationIdFactoryBase,
+    SkillConversationIdFactoryOptions,
+    SkillConversationReference,
+    BotFrameworkClient,
+)
+from botbuilder.schema import (
+    Activity,
+    ActivityTypes,
+    ConversationReference,
+    OAuthCard,
+    Attachment,
+    ConversationAccount,
+    ChannelAccount,
+    ExpectedReplies,
+    DeliveryModes,
+)
+from botbuilder.testing import DialogTestClient
+
+from botbuilder.dialogs import (
+    SkillDialog,
+    SkillDialogOptions,
+    BeginSkillDialogOptions,
+    DialogTurnStatus,
+)
+
+
+class SimpleConversationIdFactory(ConversationIdFactoryBase):
+    def __init__(self):
+        self.conversation_refs = {}
+        self.create_count = 0
+
+    async def create_skill_conversation_id(
+        self,
+        options_or_conversation_reference: Union[
+            SkillConversationIdFactoryOptions, ConversationReference
+        ],
+    ) -> str:
+        self.create_count += 1
+        key = (
+            options_or_conversation_reference.activity.conversation.id
+            + options_or_conversation_reference.activity.service_url
+        )
+        if key not in self.conversation_refs:
+            self.conversation_refs[key] = SkillConversationReference(
+                conversation_reference=TurnContext.get_conversation_reference(
+                    options_or_conversation_reference.activity
+                ),
+                oauth_scope=options_or_conversation_reference.from_bot_oauth_scope,
+            )
+        return key
+
+    async def get_conversation_reference(
+        self, skill_conversation_id: str
+    ) -> Union[SkillConversationReference, ConversationReference]:
+        return self.conversation_refs[skill_conversation_id]
+
+    async def delete_conversation_reference(self, skill_conversation_id: str):
+        self.conversation_refs.pop(skill_conversation_id, None)
+        return
+
+
+class SkillDialogTests(aiounittest.AsyncTestCase):
+    async def test_constructor_validation_test(self):
+        # missing dialog_id
+        with self.assertRaises(TypeError):
+            SkillDialog(SkillDialogOptions(), None)
+
+        # missing dialog options
+        with self.assertRaises(TypeError):
+            SkillDialog(None, "dialog_id")
+
+    async def test_begin_dialog_options_validation(self):
+        dialog_options = SkillDialogOptions()
+        sut = SkillDialog(dialog_options, dialog_id="dialog_id")
+
+        # empty options should raise
+        client = DialogTestClient("test", sut)
+        with self.assertRaises(TypeError):
+            await client.send_activity("irrelevant")
+
+        # non DialogArgs should raise
+        client = DialogTestClient("test", sut, {})
+        with self.assertRaises(TypeError):
+            await client.send_activity("irrelevant")
+
+        # Activity in DialogArgs should be set
+        client = DialogTestClient("test", sut, BeginSkillDialogOptions(None))
+        with self.assertRaises(TypeError):
+            await client.send_activity("irrelevant")
+
+    async def test_begin_dialog_calls_skill_no_deliverymode(self):
+        return await self.begin_dialog_calls_skill(None)
+
+    async def test_begin_dialog_calls_skill_expect_replies(self):
+        return await self.begin_dialog_calls_skill(DeliveryModes.expect_replies)
+
+    async def begin_dialog_calls_skill(self, deliver_mode: str):
+        activity_sent = None
+        from_bot_id_sent = None
+        to_bot_id_sent = None
+        to_url_sent = None
+
+        async def capture(
+            from_bot_id: str,
+            to_bot_id: str,
+            to_url: str,
+            service_url: str,  # pylint: disable=unused-argument
+            conversation_id: str,  # pylint: disable=unused-argument
+            activity: Activity,
+        ):
+            nonlocal from_bot_id_sent, to_bot_id_sent, to_url_sent, activity_sent
+            from_bot_id_sent = from_bot_id
+            to_bot_id_sent = to_bot_id
+            to_url_sent = to_url
+            activity_sent = activity
+
+        mock_skill_client = self._create_mock_skill_client(capture)
+
+        conversation_state = ConversationState(MemoryStorage())
+        dialog_options = SkillDialogTests.create_skill_dialog_options(
+            conversation_state, mock_skill_client
+        )
+
+        sut = SkillDialog(dialog_options, "dialog_id")
+        activity_to_send = MessageFactory.text(str(uuid.uuid4()))
+        activity_to_send.delivery_mode = deliver_mode
+
+        client = DialogTestClient(
+            "test",
+            sut,
+            BeginSkillDialogOptions(activity=activity_to_send),
+            conversation_state=conversation_state,
+        )
+
+        assert len(dialog_options.conversation_id_factory.conversation_refs) == 0
+
+        # Send something to the dialog to start it
+        await client.send_activity(MessageFactory.text("irrelevant"))
+
+        # Assert results and data sent to the SkillClient for fist turn
+        assert len(dialog_options.conversation_id_factory.conversation_refs) == 1
+        assert dialog_options.bot_id == from_bot_id_sent
+        assert dialog_options.skill.app_id == to_bot_id_sent
+        assert dialog_options.skill.skill_endpoint == to_url_sent
+        assert activity_to_send.text == activity_sent.text
+        assert DialogTurnStatus.Waiting == client.dialog_turn_result.status
+
+        # Send a second message to continue the dialog
+        await client.send_activity(MessageFactory.text("Second message"))
+
+        # Assert results for second turn
+        assert len(dialog_options.conversation_id_factory.conversation_refs) == 1
+        assert activity_sent.text == "Second message"
+        assert DialogTurnStatus.Waiting == client.dialog_turn_result.status
+
+        # Send EndOfConversation to the dialog
+        await client.send_activity(Activity(type=ActivityTypes.end_of_conversation))
+
+        # Assert we are done.
+        assert DialogTurnStatus.Complete == client.dialog_turn_result.status
+
+    async def test_should_handle_invoke_activities(self):
+        activity_sent = None
+        from_bot_id_sent = None
+        to_bot_id_sent = None
+        to_url_sent = None
+
+        async def capture(
+            from_bot_id: str,
+            to_bot_id: str,
+            to_url: str,
+            service_url: str,  # pylint: disable=unused-argument
+            conversation_id: str,  # pylint: disable=unused-argument
+            activity: Activity,
+        ):
+            nonlocal from_bot_id_sent, to_bot_id_sent, to_url_sent, activity_sent
+            from_bot_id_sent = from_bot_id
+            to_bot_id_sent = to_bot_id
+            to_url_sent = to_url
+            activity_sent = activity
+
+        mock_skill_client = self._create_mock_skill_client(capture)
+
+        conversation_state = ConversationState(MemoryStorage())
+        dialog_options = SkillDialogTests.create_skill_dialog_options(
+            conversation_state, mock_skill_client
+        )
+
+        sut = SkillDialog(dialog_options, "dialog_id")
+        activity_to_send = Activity(type=ActivityTypes.invoke, name=str(uuid.uuid4()),)
+
+        client = DialogTestClient(
+            "test",
+            sut,
+            BeginSkillDialogOptions(activity=activity_to_send),
+            conversation_state=conversation_state,
+        )
+
+        # Send something to the dialog to start it
+        await client.send_activity(MessageFactory.text("irrelevant"))
+
+        # Assert results and data sent to the SkillClient for fist turn
+        assert dialog_options.bot_id == from_bot_id_sent
+        assert dialog_options.skill.app_id == to_bot_id_sent
+        assert dialog_options.skill.skill_endpoint == to_url_sent
+        assert activity_to_send.text == activity_sent.text
+        assert DialogTurnStatus.Waiting == client.dialog_turn_result.status
+
+        # Send a second message to continue the dialog
+        await client.send_activity(MessageFactory.text("Second message"))
+
+        # Assert results for second turn
+        assert activity_sent.text == "Second message"
+        assert DialogTurnStatus.Waiting == client.dialog_turn_result.status
+
+        # Send EndOfConversation to the dialog
+        await client.send_activity(Activity(type=ActivityTypes.end_of_conversation))
+
+        # Assert we are done.
+        assert DialogTurnStatus.Complete == client.dialog_turn_result.status
+
+    async def test_cancel_dialog_sends_eoc(self):
+        activity_sent = None
+
+        async def capture(
+            from_bot_id: str,  # pylint: disable=unused-argument
+            to_bot_id: str,  # pylint: disable=unused-argument
+            to_url: str,  # pylint: disable=unused-argument
+            service_url: str,  # pylint: disable=unused-argument
+            conversation_id: str,  # pylint: disable=unused-argument
+            activity: Activity,
+        ):
+            nonlocal activity_sent
+            activity_sent = activity
+
+        mock_skill_client = self._create_mock_skill_client(capture)
+
+        conversation_state = ConversationState(MemoryStorage())
+        dialog_options = SkillDialogTests.create_skill_dialog_options(
+            conversation_state, mock_skill_client
+        )
+
+        sut = SkillDialog(dialog_options, "dialog_id")
+        activity_to_send = MessageFactory.text(str(uuid.uuid4()))
+
+        client = DialogTestClient(
+            "test",
+            sut,
+            BeginSkillDialogOptions(activity=activity_to_send),
+            conversation_state=conversation_state,
+        )
+
+        # Send something to the dialog to start it
+        await client.send_activity(MessageFactory.text("irrelevant"))
+
+        # Cancel the dialog so it sends an EoC to the skill
+        await client.dialog_context.cancel_all_dialogs()
+
+        assert activity_sent
+        assert activity_sent.type == ActivityTypes.end_of_conversation
+
+    async def test_should_throw_on_post_failure(self):
+        # This mock client will fail
+        mock_skill_client = self._create_mock_skill_client(None, 500)
+
+        conversation_state = ConversationState(MemoryStorage())
+        dialog_options = SkillDialogTests.create_skill_dialog_options(
+            conversation_state, mock_skill_client
+        )
+
+        sut = SkillDialog(dialog_options, "dialog_id")
+        activity_to_send = MessageFactory.text(str(uuid.uuid4()))
+
+        client = DialogTestClient(
+            "test",
+            sut,
+            BeginSkillDialogOptions(activity=activity_to_send),
+            conversation_state=conversation_state,
+        )
+
+        # A send should raise an exception
+        with self.assertRaises(Exception):
+            await client.send_activity("irrelevant")
+
+    async def test_should_intercept_oauth_cards_for_sso(self):
+        connection_name = "connectionName"
+        first_response = ExpectedReplies(
+            activities=[
+                SkillDialogTests.create_oauth_card_attachment_activity("https://test")
+            ]
+        )
+
+        sequence = 0
+
+        async def post_return():
+            nonlocal sequence
+            if sequence == 0:
+                result = InvokeResponse(body=first_response, status=HTTPStatus.OK)
+            else:
+                result = InvokeResponse(status=HTTPStatus.OK)
+            sequence += 1
+            return result
+
+        mock_skill_client = self._create_mock_skill_client(None, post_return)
+        conversation_state = ConversationState(MemoryStorage())
+
+        dialog_options = SkillDialogTests.create_skill_dialog_options(
+            conversation_state, mock_skill_client, connection_name
+        )
+        sut = SkillDialog(dialog_options, dialog_id="dialog")
+        activity_to_send = SkillDialogTests.create_send_activity()
+
+        client = DialogTestClient(
+            "test",
+            sut,
+            BeginSkillDialogOptions(activity=activity_to_send,),
+            conversation_state=conversation_state,
+        )
+
+        client.test_adapter.add_exchangeable_token(
+            connection_name, "test", "User1", "https://test", "https://test1"
+        )
+
+        final_activity = await client.send_activity(MessageFactory.text("irrelevant"))
+        self.assertIsNone(final_activity)
+
+    async def test_should_not_intercept_oauth_cards_for_empty_connection_name(self):
+        connection_name = "connectionName"
+        first_response = ExpectedReplies(
+            activities=[
+                SkillDialogTests.create_oauth_card_attachment_activity("https://test")
+            ]
+        )
+
+        sequence = 0
+
+        async def post_return():
+            nonlocal sequence
+            if sequence == 0:
+                result = InvokeResponse(body=first_response, status=HTTPStatus.OK)
+            else:
+                result = InvokeResponse(status=HTTPStatus.OK)
+            sequence += 1
+            return result
+
+        mock_skill_client = self._create_mock_skill_client(None, post_return)
+        conversation_state = ConversationState(MemoryStorage())
+
+        dialog_options = SkillDialogTests.create_skill_dialog_options(
+            conversation_state, mock_skill_client
+        )
+        sut = SkillDialog(dialog_options, dialog_id="dialog")
+        activity_to_send = SkillDialogTests.create_send_activity()
+
+        client = DialogTestClient(
+            "test",
+            sut,
+            BeginSkillDialogOptions(activity=activity_to_send,),
+            conversation_state=conversation_state,
+        )
+
+        client.test_adapter.add_exchangeable_token(
+            connection_name, "test", "User1", "https://test", "https://test1"
+        )
+
+        final_activity = await client.send_activity(MessageFactory.text("irrelevant"))
+        self.assertIsNotNone(final_activity)
+        self.assertEqual(len(final_activity.attachments), 1)
+
+    async def test_should_not_intercept_oauth_cards_for_empty_token(self):
+        first_response = ExpectedReplies(
+            activities=[
+                SkillDialogTests.create_oauth_card_attachment_activity("https://test")
+            ]
+        )
+
+        sequence = 0
+
+        async def post_return():
+            nonlocal sequence
+            if sequence == 0:
+                result = InvokeResponse(body=first_response, status=HTTPStatus.OK)
+            else:
+                result = InvokeResponse(status=HTTPStatus.OK)
+            sequence += 1
+            return result
+
+        mock_skill_client = self._create_mock_skill_client(None, post_return)
+        conversation_state = ConversationState(MemoryStorage())
+
+        dialog_options = SkillDialogTests.create_skill_dialog_options(
+            conversation_state, mock_skill_client
+        )
+        sut = SkillDialog(dialog_options, dialog_id="dialog")
+        activity_to_send = SkillDialogTests.create_send_activity()
+
+        client = DialogTestClient(
+            "test",
+            sut,
+            BeginSkillDialogOptions(activity=activity_to_send,),
+            conversation_state=conversation_state,
+        )
+
+        # Don't add exchangeable token to test adapter
+
+        final_activity = await client.send_activity(MessageFactory.text("irrelevant"))
+        self.assertIsNotNone(final_activity)
+        self.assertEqual(len(final_activity.attachments), 1)
+
+    async def test_should_not_intercept_oauth_cards_for_token_exception(self):
+        connection_name = "connectionName"
+        first_response = ExpectedReplies(
+            activities=[
+                SkillDialogTests.create_oauth_card_attachment_activity("https://test")
+            ]
+        )
+
+        sequence = 0
+
+        async def post_return():
+            nonlocal sequence
+            if sequence == 0:
+                result = InvokeResponse(body=first_response, status=HTTPStatus.OK)
+            else:
+                result = InvokeResponse(status=HTTPStatus.OK)
+            sequence += 1
+            return result
+
+        mock_skill_client = self._create_mock_skill_client(None, post_return)
+        conversation_state = ConversationState(MemoryStorage())
+
+        dialog_options = SkillDialogTests.create_skill_dialog_options(
+            conversation_state, mock_skill_client, connection_name
+        )
+        sut = SkillDialog(dialog_options, dialog_id="dialog")
+        activity_to_send = SkillDialogTests.create_send_activity()
+        initial_dialog_options = BeginSkillDialogOptions(activity=activity_to_send,)
+
+        client = DialogTestClient(
+            "test", sut, initial_dialog_options, conversation_state=conversation_state,
+        )
+        client.test_adapter.throw_on_exchange_request(
+            connection_name, "test", "User1", "https://test"
+        )
+
+        final_activity = await client.send_activity(MessageFactory.text("irrelevant"))
+        self.assertIsNotNone(final_activity)
+        self.assertEqual(len(final_activity.attachments), 1)
+
+    async def test_should_not_intercept_oauth_cards_for_bad_request(self):
+        connection_name = "connectionName"
+        first_response = ExpectedReplies(
+            activities=[
+                SkillDialogTests.create_oauth_card_attachment_activity("https://test")
+            ]
+        )
+
+        sequence = 0
+
+        async def post_return():
+            nonlocal sequence
+            if sequence == 0:
+                result = InvokeResponse(body=first_response, status=HTTPStatus.OK)
+            else:
+                result = InvokeResponse(status=HTTPStatus.CONFLICT)
+            sequence += 1
+            return result
+
+        mock_skill_client = self._create_mock_skill_client(None, post_return)
+        conversation_state = ConversationState(MemoryStorage())
+
+        dialog_options = SkillDialogTests.create_skill_dialog_options(
+            conversation_state, mock_skill_client, connection_name
+        )
+        sut = SkillDialog(dialog_options, dialog_id="dialog")
+        activity_to_send = SkillDialogTests.create_send_activity()
+
+        client = DialogTestClient(
+            "test",
+            sut,
+            BeginSkillDialogOptions(activity=activity_to_send,),
+            conversation_state=conversation_state,
+        )
+
+        client.test_adapter.add_exchangeable_token(
+            connection_name, "test", "User1", "https://test", "https://test1"
+        )
+
+        final_activity = await client.send_activity(MessageFactory.text("irrelevant"))
+        self.assertIsNotNone(final_activity)
+        self.assertEqual(len(final_activity.attachments), 1)
+
+    async def test_end_of_conversation_from_expect_replies_calls_delete_conversation_reference(
+        self,
+    ):
+        activity_sent: Activity = None
+
+        # Callback to capture the parameters sent to the skill
+        async def capture_action(
+            from_bot_id: str,  # pylint: disable=unused-argument
+            to_bot_id: str,  # pylint: disable=unused-argument
+            to_uri: str,  # pylint: disable=unused-argument
+            service_url: str,  # pylint: disable=unused-argument
+            conversation_id: str,  # pylint: disable=unused-argument
+            activity: Activity,
+        ):
+            # Capture values sent to the skill so we can assert the right parameters were used.
+            nonlocal activity_sent
+            activity_sent = activity
+
+        eoc = Activity.create_end_of_conversation_activity()
+        expected_replies = list([eoc])
+
+        # Create a mock skill client to intercept calls and capture what is sent.
+        mock_skill_client = self._create_mock_skill_client(
+            capture_action, expected_replies=expected_replies
+        )
+
+        # Use Memory for conversation state
+        conversation_state = ConversationState(MemoryStorage())
+        dialog_options = self.create_skill_dialog_options(
+            conversation_state, mock_skill_client
+        )
+
+        # Create the SkillDialogInstance and the activity to send.
+        sut = SkillDialog(dialog_options, dialog_id="dialog")
+        activity_to_send = Activity.create_message_activity()
+        activity_to_send.delivery_mode = DeliveryModes.expect_replies
+        activity_to_send.text = str(uuid.uuid4())
+        client = DialogTestClient(
+            "test",
+            sut,
+            BeginSkillDialogOptions(activity_to_send),
+            conversation_state=conversation_state,
+        )
+
+        # Send something to the dialog to start it
+        await client.send_activity("hello")
+
+        simple_id_factory: SimpleConversationIdFactory = dialog_options.conversation_id_factory
+        self.assertEqual(0, len(simple_id_factory.conversation_refs))
+        self.assertEqual(1, simple_id_factory.create_count)
+
+    @staticmethod
+    def create_skill_dialog_options(
+        conversation_state: ConversationState,
+        skill_client: BotFrameworkClient,
+        connection_name: str = None,
+    ):
+        return SkillDialogOptions(
+            bot_id=str(uuid.uuid4()),
+            skill_host_endpoint="http://test.contoso.com/skill/messages",
+            conversation_id_factory=SimpleConversationIdFactory(),
+            conversation_state=conversation_state,
+            skill_client=skill_client,
+            skill=BotFrameworkSkill(
+                app_id=str(uuid.uuid4()),
+                skill_endpoint="http://testskill.contoso.com/api/messages",
+            ),
+            connection_name=connection_name,
+        )
+
+    @staticmethod
+    def create_send_activity() -> Activity:
+        return Activity(
+            type=ActivityTypes.message,
+            delivery_mode=DeliveryModes.expect_replies,
+            text=str(uuid.uuid4()),
+        )
+
+    @staticmethod
+    def create_oauth_card_attachment_activity(uri: str) -> Activity:
+        oauth_card = OAuthCard(token_exchange_resource=TokenExchangeResource(uri=uri))
+        attachment = Attachment(
+            content_type=ContentTypes.oauth_card, content=oauth_card,
+        )
+
+        attachment_activity = MessageFactory.attachment(attachment)
+        attachment_activity.conversation = ConversationAccount(id=str(uuid.uuid4()))
+        attachment_activity.from_property = ChannelAccount(id="blah", name="name")
+
+        return attachment_activity
+
+    def _create_mock_skill_client(
+        self,
+        callback: Callable,
+        return_status: Union[Callable, int] = 200,
+        expected_replies: List[Activity] = None,
+    ) -> BotFrameworkClient:
+        mock_client = Mock()
+        activity_list = ExpectedReplies(
+            activities=expected_replies or [MessageFactory.text("dummy activity")]
+        )
+
+        async def mock_post_activity(
+            from_bot_id: str,
+            to_bot_id: str,
+            to_url: str,
+            service_url: str,
+            conversation_id: str,
+            activity: Activity,
+        ):
+            nonlocal callback, return_status
+            if callback:
+                await callback(
+                    from_bot_id,
+                    to_bot_id,
+                    to_url,
+                    service_url,
+                    conversation_id,
+                    activity,
+                )
+
+            if isinstance(return_status, Callable):
+                return await return_status()
+            return InvokeResponse(status=return_status, body=activity_list)
+
+        mock_client.post_activity.side_effect = mock_post_activity
+
+        return mock_client
diff --git a/libraries/botbuilder-dialogs/tests/test_waterfall.py b/libraries/botbuilder-dialogs/tests/test_waterfall.py
index 2ace9b666..c26f6ee01 100644
--- a/libraries/botbuilder-dialogs/tests/test_waterfall.py
+++ b/libraries/botbuilder-dialogs/tests/test_waterfall.py
@@ -21,19 +21,19 @@ def __init__(self, dialog_id: str):
         super(MyWaterfallDialog, self).__init__(dialog_id)
 
         async def waterfall2_step1(
-            step_context: WaterfallStepContext
+            step_context: WaterfallStepContext,
         ) -> DialogTurnResult:
             await step_context.context.send_activity("step1")
             return Dialog.end_of_turn
 
         async def waterfall2_step2(
-            step_context: WaterfallStepContext
+            step_context: WaterfallStepContext,
         ) -> DialogTurnResult:
             await step_context.context.send_activity("step2")
             return Dialog.end_of_turn
 
         async def waterfall2_step3(
-            step_context: WaterfallStepContext
+            step_context: WaterfallStepContext,
         ) -> DialogTurnResult:
             await step_context.context.send_activity("step3")
             return Dialog.end_of_turn
diff --git a/libraries/botbuilder-integration-aiohttp/README.rst b/libraries/botbuilder-integration-aiohttp/README.rst
new file mode 100644
index 000000000..eb32dd702
--- /dev/null
+++ b/libraries/botbuilder-integration-aiohttp/README.rst
@@ -0,0 +1,83 @@
+
+=========================================
+BotBuilder-Integration-Aiohttp for Python
+=========================================
+
+.. image:: https://dev.azure.com/FuseLabs/SDK_v4/_apis/build/status/Python/Python-CI-PR-yaml?branchName=master
+   :target:  https://dev.azure.com/FuseLabs/SDK_v4/_apis/build/status/Python/Python-CI-PR-yaml?branchName=master
+   :align: right
+   :alt: Azure DevOps status for master branch
+.. image:: https://badge.fury.io/py/botbuilder-integration-aiohttp.svg
+   :target: https://badge.fury.io/py/botbuilder-integration-aiohttp
+   :alt: Latest PyPI package version
+
+Within the Bot Framework, This library enables you to integrate your bot within an aiohttp web application.
+
+How to Install
+==============
+
+.. code-block:: python
+  
+  pip install botbuilder-integration-aiohttp
+
+
+Documentation/Wiki
+==================
+
+You can find more information on the botbuilder-python project by visiting our `Wiki`_.
+
+Requirements
+============
+
+* `Python >= 3.7.0`_
+
+
+Source Code
+===========
+The latest developer version is available in a github repository:
+https://github.com/Microsoft/botbuilder-python/
+
+
+Contributing
+============
+
+This project welcomes contributions and suggestions.  Most contributions require you to agree to a
+Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
+the rights to use your contribution. For details, visit https://cla.microsoft.com.
+
+When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
+a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
+provided by the bot. You will only need to do this once across all repos using our CLA.
+
+This project has adopted the `Microsoft Open Source Code of Conduct`_.
+For more information see the `Code of Conduct FAQ`_ or
+contact `opencode@microsoft.com`_ with any additional questions or comments.
+
+Reporting Security Issues
+=========================
+
+Security issues and bugs should be reported privately, via email, to the Microsoft Security
+Response Center (MSRC) at `secure@microsoft.com`_. You should
+receive a response within 24 hours. If for some reason you do not, please follow up via
+email to ensure we received your original message. Further information, including the
+`MSRC PGP`_ key, can be found in
+the `Security TechCenter`_.
+
+License
+=======
+
+Copyright (c) Microsoft Corporation. All rights reserved.
+
+Licensed under the MIT_ License.
+
+.. _Wiki: https://github.com/Microsoft/botbuilder-python/wiki
+.. _Python >= 3.7.0: https://www.python.org/downloads/
+.. _MIT: https://github.com/Microsoft/vscode/blob/master/LICENSE.txt
+.. _Microsoft Open Source Code of Conduct: https://opensource.microsoft.com/codeofconduct/
+.. _Code of Conduct FAQ: https://opensource.microsoft.com/codeofconduct/faq/
+.. _opencode@microsoft.com: mailto:opencode@microsoft.com
+.. _secure@microsoft.com: mailto:secure@microsoft.com
+.. _MSRC PGP: https://technet.microsoft.com/en-us/security/dn606155
+.. _Security TechCenter: https://github.com/Microsoft/vscode/blob/master/LICENSE.txt
+
+.. `_
\ No newline at end of file
diff --git a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/__init__.py b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/__init__.py
new file mode 100644
index 000000000..1bb31e665
--- /dev/null
+++ b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/__init__.py
@@ -0,0 +1,16 @@
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+# --------------------------------------------------------------------------
+
+from .aiohttp_channel_service import aiohttp_channel_service_routes
+from .aiohttp_channel_service_exception_middleware import aiohttp_error_middleware
+from .bot_framework_http_client import BotFrameworkHttpClient
+
+__all__ = [
+    "aiohttp_channel_service_routes",
+    "aiohttp_error_middleware",
+    "BotFrameworkHttpClient",
+]
diff --git a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/about.py b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/about.py
new file mode 100644
index 000000000..9ba957138
--- /dev/null
+++ b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/about.py
@@ -0,0 +1,14 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+
+__title__ = "botbuilder-integration-aiohttp"
+__version__ = (
+    os.environ["packageVersion"] if "packageVersion" in os.environ else "4.12.0"
+)
+__uri__ = "https://www.github.com/Microsoft/botbuilder-python"
+__author__ = "Microsoft"
+__description__ = "Microsoft Bot Framework Bot Builder"
+__summary__ = "Microsoft Bot Framework Bot Builder SDK for Python."
+__license__ = "MIT"
diff --git a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/aiohttp_channel_service.py b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/aiohttp_channel_service.py
new file mode 100644
index 000000000..af2545d89
--- /dev/null
+++ b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/aiohttp_channel_service.py
@@ -0,0 +1,176 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+import json
+from typing import List, Union, Type
+
+from aiohttp.web import RouteTableDef, Request, Response
+from msrest.serialization import Model
+
+from botbuilder.schema import (
+    Activity,
+    AttachmentData,
+    ConversationParameters,
+    Transcript,
+)
+
+from botbuilder.core import ChannelServiceHandler
+
+
+async def deserialize_from_body(
+    request: Request, target_model: Type[Model]
+) -> Activity:
+    if "application/json" in request.headers["Content-Type"]:
+        body = await request.json()
+    else:
+        return Response(status=415)
+
+    return target_model().deserialize(body)
+
+
+def get_serialized_response(model_or_list: Union[Model, List[Model]]) -> Response:
+    if isinstance(model_or_list, Model):
+        json_obj = model_or_list.serialize()
+    else:
+        json_obj = [model.serialize() for model in model_or_list]
+
+    return Response(body=json.dumps(json_obj), content_type="application/json")
+
+
+def aiohttp_channel_service_routes(
+    handler: ChannelServiceHandler, base_url: str = ""
+) -> RouteTableDef:
+    # pylint: disable=unused-variable
+    routes = RouteTableDef()
+
+    @routes.post(base_url + "/v3/conversations/{conversation_id}/activities")
+    async def send_to_conversation(request: Request):
+        activity = await deserialize_from_body(request, Activity)
+        result = await handler.handle_send_to_conversation(
+            request.headers.get("Authorization"),
+            request.match_info["conversation_id"],
+            activity,
+        )
+
+        return get_serialized_response(result)
+
+    @routes.post(
+        base_url + "/v3/conversations/{conversation_id}/activities/{activity_id}"
+    )
+    async def reply_to_activity(request: Request):
+        activity = await deserialize_from_body(request, Activity)
+        result = await handler.handle_reply_to_activity(
+            request.headers.get("Authorization"),
+            request.match_info["conversation_id"],
+            request.match_info["activity_id"],
+            activity,
+        )
+
+        return get_serialized_response(result)
+
+    @routes.put(
+        base_url + "/v3/conversations/{conversation_id}/activities/{activity_id}"
+    )
+    async def update_activity(request: Request):
+        activity = await deserialize_from_body(request, Activity)
+        result = await handler.handle_update_activity(
+            request.headers.get("Authorization"),
+            request.match_info["conversation_id"],
+            request.match_info["activity_id"],
+            activity,
+        )
+
+        return get_serialized_response(result)
+
+    @routes.delete(
+        base_url + "/v3/conversations/{conversation_id}/activities/{activity_id}"
+    )
+    async def delete_activity(request: Request):
+        await handler.handle_delete_activity(
+            request.headers.get("Authorization"),
+            request.match_info["conversation_id"],
+            request.match_info["activity_id"],
+        )
+
+        return Response()
+
+    @routes.get(
+        base_url
+        + "/v3/conversations/{conversation_id}/activities/{activity_id}/members"
+    )
+    async def get_activity_members(request: Request):
+        result = await handler.handle_get_activity_members(
+            request.headers.get("Authorization"),
+            request.match_info["conversation_id"],
+            request.match_info["activity_id"],
+        )
+
+        return get_serialized_response(result)
+
+    @routes.post(base_url + "/")
+    async def create_conversation(request: Request):
+        conversation_parameters = deserialize_from_body(request, ConversationParameters)
+        result = await handler.handle_create_conversation(
+            request.headers.get("Authorization"), conversation_parameters
+        )
+
+        return get_serialized_response(result)
+
+    @routes.get(base_url + "/")
+    async def get_conversation(request: Request):
+        # TODO: continuation token?
+        result = await handler.handle_get_conversations(
+            request.headers.get("Authorization")
+        )
+
+        return get_serialized_response(result)
+
+    @routes.get(base_url + "/v3/conversations/{conversation_id}/members")
+    async def get_conversation_members(request: Request):
+        result = await handler.handle_get_conversation_members(
+            request.headers.get("Authorization"), request.match_info["conversation_id"],
+        )
+
+        return get_serialized_response(result)
+
+    @routes.get(base_url + "/v3/conversations/{conversation_id}/pagedmembers")
+    async def get_conversation_paged_members(request: Request):
+        # TODO: continuation token? page size?
+        result = await handler.handle_get_conversation_paged_members(
+            request.headers.get("Authorization"), request.match_info["conversation_id"],
+        )
+
+        return get_serialized_response(result)
+
+    @routes.delete(base_url + "/v3/conversations/{conversation_id}/members/{member_id}")
+    async def delete_conversation_member(request: Request):
+        result = await handler.handle_delete_conversation_member(
+            request.headers.get("Authorization"),
+            request.match_info["conversation_id"],
+            request.match_info["member_id"],
+        )
+
+        return get_serialized_response(result)
+
+    @routes.post(base_url + "/v3/conversations/{conversation_id}/activities/history")
+    async def send_conversation_history(request: Request):
+        transcript = deserialize_from_body(request, Transcript)
+        result = await handler.handle_send_conversation_history(
+            request.headers.get("Authorization"),
+            request.match_info["conversation_id"],
+            transcript,
+        )
+
+        return get_serialized_response(result)
+
+    @routes.post(base_url + "/v3/conversations/{conversation_id}/attachments")
+    async def upload_attachment(request: Request):
+        attachment_data = deserialize_from_body(request, AttachmentData)
+        result = await handler.handle_upload_attachment(
+            request.headers.get("Authorization"),
+            request.match_info["conversation_id"],
+            attachment_data,
+        )
+
+        return get_serialized_response(result)
+
+    return routes
diff --git a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/aiohttp_channel_service_exception_middleware.py b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/aiohttp_channel_service_exception_middleware.py
new file mode 100644
index 000000000..7c5091121
--- /dev/null
+++ b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/aiohttp_channel_service_exception_middleware.py
@@ -0,0 +1,29 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from aiohttp.web import (
+    middleware,
+    HTTPNotImplemented,
+    HTTPUnauthorized,
+    HTTPNotFound,
+    HTTPInternalServerError,
+)
+
+from botbuilder.core import BotActionNotImplementedError
+
+
+@middleware
+async def aiohttp_error_middleware(request, handler):
+    try:
+        response = await handler(request)
+        return response
+    except BotActionNotImplementedError:
+        raise HTTPNotImplemented()
+    except NotImplementedError:
+        raise HTTPNotImplemented()
+    except PermissionError:
+        raise HTTPUnauthorized()
+    except KeyError:
+        raise HTTPNotFound()
+    except Exception:
+        raise HTTPInternalServerError()
diff --git a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_client.py b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_client.py
new file mode 100644
index 000000000..164818e87
--- /dev/null
+++ b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_client.py
@@ -0,0 +1,179 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+# pylint: disable=no-member
+
+import json
+from typing import Dict
+from logging import Logger
+
+import aiohttp
+from botbuilder.core import InvokeResponse
+from botbuilder.core.skills import BotFrameworkClient
+from botbuilder.schema import (
+    Activity,
+    ExpectedReplies,
+    ConversationReference,
+    ConversationAccount,
+    ChannelAccount,
+    RoleTypes,
+)
+from botframework.connector.auth import (
+    ChannelProvider,
+    CredentialProvider,
+    MicrosoftAppCredentials,
+    AppCredentials,
+    MicrosoftGovernmentAppCredentials,
+)
+
+
+class BotFrameworkHttpClient(BotFrameworkClient):
+
+    """
+    A skill host adapter that implements the API to forward activity to a skill and
+    implements routing ChannelAPI calls from the skill up through the bot/adapter.
+    """
+
+    INVOKE_ACTIVITY_NAME = "SkillEvents.ChannelApiInvoke"
+    _BOT_IDENTITY_KEY = "BotIdentity"
+    _APP_CREDENTIALS_CACHE: Dict[str, MicrosoftAppCredentials] = {}
+
+    def __init__(
+        self,
+        credential_provider: CredentialProvider,
+        channel_provider: ChannelProvider = None,
+        logger: Logger = None,
+    ):
+        if not credential_provider:
+            raise TypeError("credential_provider can't be None")
+
+        self._credential_provider = credential_provider
+        self._channel_provider = channel_provider
+        self._logger = logger
+        self._session = aiohttp.ClientSession()
+
+    async def post_activity(
+        self,
+        from_bot_id: str,
+        to_bot_id: str,
+        to_url: str,
+        service_url: str,
+        conversation_id: str,
+        activity: Activity,
+    ) -> InvokeResponse:
+        app_credentials = await self._get_app_credentials(from_bot_id, to_bot_id)
+
+        if not app_credentials:
+            raise KeyError("Unable to get appCredentials to connect to the skill")
+
+        # Get token for the skill call
+        token = (
+            app_credentials.get_access_token()
+            if app_credentials.microsoft_app_id
+            else None
+        )
+
+        # Capture current activity settings before changing them.
+        original_conversation_id = activity.conversation.id
+        original_service_url = activity.service_url
+        original_relates_to = activity.relates_to
+        original_recipient = activity.recipient
+
+        try:
+            activity.relates_to = ConversationReference(
+                service_url=activity.service_url,
+                activity_id=activity.id,
+                channel_id=activity.channel_id,
+                conversation=ConversationAccount(
+                    id=activity.conversation.id,
+                    name=activity.conversation.name,
+                    conversation_type=activity.conversation.conversation_type,
+                    aad_object_id=activity.conversation.aad_object_id,
+                    is_group=activity.conversation.is_group,
+                    role=activity.conversation.role,
+                    tenant_id=activity.conversation.tenant_id,
+                    properties=activity.conversation.properties,
+                ),
+                bot=None,
+            )
+            activity.conversation.id = conversation_id
+            activity.service_url = service_url
+            if not activity.recipient:
+                activity.recipient = ChannelAccount(role=RoleTypes.skill)
+            else:
+                activity.recipient.role = RoleTypes.skill
+
+            status, content = await self._post_content(to_url, token, activity)
+
+            return InvokeResponse(status=status, body=content)
+
+        finally:
+            # Restore activity properties.
+            activity.conversation.id = original_conversation_id
+            activity.service_url = original_service_url
+            activity.relates_to = original_relates_to
+            activity.recipient = original_recipient
+
+    async def _post_content(
+        self, to_url: str, token: str, activity: Activity
+    ) -> (int, object):
+        headers_dict = {
+            "Content-type": "application/json; charset=utf-8",
+        }
+        if token:
+            headers_dict.update(
+                {"Authorization": f"Bearer {token}",}
+            )
+
+        json_content = json.dumps(activity.serialize())
+        resp = await self._session.post(
+            to_url, data=json_content.encode("utf-8"), headers=headers_dict,
+        )
+        resp.raise_for_status()
+        data = (await resp.read()).decode()
+        return resp.status, json.loads(data) if data else None
+
+    async def post_buffered_activity(
+        self,
+        from_bot_id: str,
+        to_bot_id: str,
+        to_url: str,
+        service_url: str,
+        conversation_id: str,
+        activity: Activity,
+    ) -> [Activity]:
+        """
+        Helper method to return a list of activities when an Activity is being
+        sent with DeliveryMode == expectReplies.
+        """
+        response = await self.post_activity(
+            from_bot_id, to_bot_id, to_url, service_url, conversation_id, activity
+        )
+        if not response or (response.status / 100) != 2:
+            return []
+        return ExpectedReplies().deserialize(response.body).activities
+
+    async def _get_app_credentials(
+        self, app_id: str, oauth_scope: str
+    ) -> AppCredentials:
+        if not app_id:
+            return MicrosoftAppCredentials.empty()
+
+        # in the cache?
+        cache_key = f"{app_id}{oauth_scope}"
+        app_credentials = BotFrameworkHttpClient._APP_CREDENTIALS_CACHE.get(cache_key)
+        if app_credentials:
+            return app_credentials
+
+        # create a new AppCredentials
+        app_password = await self._credential_provider.get_app_password(app_id)
+
+        app_credentials = (
+            MicrosoftGovernmentAppCredentials(app_id, app_password, scope=oauth_scope)
+            if self._channel_provider and self._channel_provider.is_government()
+            else MicrosoftAppCredentials(app_id, app_password, oauth_scope=oauth_scope)
+        )
+
+        # put it in the cache
+        BotFrameworkHttpClient._APP_CREDENTIALS_CACHE[cache_key] = app_credentials
+
+        return app_credentials
diff --git a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/skills/__init__.py b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/skills/__init__.py
new file mode 100644
index 000000000..71aaa71cf
--- /dev/null
+++ b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/skills/__init__.py
@@ -0,0 +1,4 @@
+from .skill_http_client import SkillHttpClient
+
+
+__all__ = ["SkillHttpClient"]
diff --git a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/skills/skill_http_client.py b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/skills/skill_http_client.py
new file mode 100644
index 000000000..68da498ab
--- /dev/null
+++ b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/skills/skill_http_client.py
@@ -0,0 +1,75 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from logging import Logger
+
+from botbuilder.core import InvokeResponse
+from botbuilder.integration.aiohttp import BotFrameworkHttpClient
+from botbuilder.core.skills import (
+    ConversationIdFactoryBase,
+    SkillConversationIdFactoryOptions,
+    BotFrameworkSkill,
+)
+from botbuilder.schema import Activity
+from botframework.connector.auth import (
+    AuthenticationConstants,
+    ChannelProvider,
+    GovernmentConstants,
+    SimpleCredentialProvider,
+)
+
+
+class SkillHttpClient(BotFrameworkHttpClient):
+    def __init__(
+        self,
+        credential_provider: SimpleCredentialProvider,
+        skill_conversation_id_factory: ConversationIdFactoryBase,
+        channel_provider: ChannelProvider = None,
+        logger: Logger = None,
+    ):
+        if not skill_conversation_id_factory:
+            raise TypeError(
+                "SkillHttpClient(): skill_conversation_id_factory can't be None"
+            )
+
+        super().__init__(credential_provider)
+
+        self._skill_conversation_id_factory = skill_conversation_id_factory
+        self._channel_provider = channel_provider
+
+    async def post_activity_to_skill(
+        self,
+        from_bot_id: str,
+        to_skill: BotFrameworkSkill,
+        service_url: str,
+        activity: Activity,
+        originating_audience: str = None,
+    ) -> InvokeResponse:
+
+        if originating_audience is None:
+            originating_audience = (
+                GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
+                if self._channel_provider is not None
+                and self._channel_provider.is_government()
+                else AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
+            )
+
+        options = SkillConversationIdFactoryOptions(
+            from_bot_oauth_scope=originating_audience,
+            from_bot_id=from_bot_id,
+            activity=activity,
+            bot_framework_skill=to_skill,
+        )
+
+        skill_conversation_id = await self._skill_conversation_id_factory.create_skill_conversation_id(
+            options
+        )
+
+        return await super().post_activity(
+            from_bot_id,
+            to_skill.app_id,
+            to_skill.skill_endpoint,
+            service_url,
+            skill_conversation_id,
+            activity,
+        )
diff --git a/libraries/botbuilder-integration-aiohttp/requirements.txt b/libraries/botbuilder-integration-aiohttp/requirements.txt
new file mode 100644
index 000000000..2d93ed698
--- /dev/null
+++ b/libraries/botbuilder-integration-aiohttp/requirements.txt
@@ -0,0 +1,4 @@
+msrest==0.6.10
+botframework-connector==4.12.0
+botbuilder-schema==4.12.0
+aiohttp==3.6.2
diff --git a/libraries/botbuilder-integration-aiohttp/setup.cfg b/libraries/botbuilder-integration-aiohttp/setup.cfg
new file mode 100644
index 000000000..68c61a226
--- /dev/null
+++ b/libraries/botbuilder-integration-aiohttp/setup.cfg
@@ -0,0 +1,2 @@
+[bdist_wheel]
+universal=0
\ No newline at end of file
diff --git a/libraries/botbuilder-integration-aiohttp/setup.py b/libraries/botbuilder-integration-aiohttp/setup.py
new file mode 100644
index 000000000..e1e06e54b
--- /dev/null
+++ b/libraries/botbuilder-integration-aiohttp/setup.py
@@ -0,0 +1,55 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+from setuptools import setup
+
+VERSION = os.environ["packageVersion"] if "packageVersion" in os.environ else "4.12.0"
+REQUIRES = [
+    "botbuilder-schema==4.12.0",
+    "botframework-connector==4.12.0",
+    "botbuilder-core==4.12.0",
+    "yarl<=1.4.2",
+    "aiohttp==3.6.2",
+]
+
+root = os.path.abspath(os.path.dirname(__file__))
+
+with open(os.path.join(root, "botbuilder", "integration", "aiohttp", "about.py")) as f:
+    package_info = {}
+    info = f.read()
+    exec(info, package_info)
+
+with open(os.path.join(root, "README.rst"), encoding="utf-8") as f:
+    long_description = f.read()
+
+setup(
+    name=package_info["__title__"],
+    version=package_info["__version__"],
+    url=package_info["__uri__"],
+    author=package_info["__author__"],
+    description=package_info["__description__"],
+    keywords=[
+        "BotBuilderIntegrationAiohttp",
+        "bots",
+        "ai",
+        "botframework",
+        "botbuilder",
+    ],
+    long_description=long_description,
+    long_description_content_type="text/x-rst",
+    license=package_info["__license__"],
+    packages=[
+        "botbuilder.integration.aiohttp",
+        "botbuilder.integration.aiohttp.skills",
+    ],
+    install_requires=REQUIRES,
+    classifiers=[
+        "Programming Language :: Python :: 3.7",
+        "Intended Audience :: Developers",
+        "License :: OSI Approved :: MIT License",
+        "Operating System :: OS Independent",
+        "Development Status :: 5 - Production/Stable",
+        "Topic :: Scientific/Engineering :: Artificial Intelligence",
+    ],
+)
diff --git a/libraries/botbuilder-integration-aiohttp/tests/skills/test_skill_http_client.py b/libraries/botbuilder-integration-aiohttp/tests/skills/test_skill_http_client.py
new file mode 100644
index 000000000..df889cc82
--- /dev/null
+++ b/libraries/botbuilder-integration-aiohttp/tests/skills/test_skill_http_client.py
@@ -0,0 +1,205 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from uuid import uuid4
+from typing import Awaitable, Callable, Dict, Union
+
+
+from unittest.mock import Mock
+import aiounittest
+
+from botbuilder.core import MessageFactory, InvokeResponse
+from botbuilder.core.skills import (
+    BotFrameworkSkill,
+    ConversationIdFactoryBase,
+    SkillConversationIdFactoryOptions,
+    SkillConversationReference,
+)
+from botbuilder.integration.aiohttp.skills import SkillHttpClient
+from botbuilder.schema import Activity, ConversationAccount, ConversationReference
+from botframework.connector.auth import (
+    AuthenticationConstants,
+    ChannelProvider,
+    GovernmentConstants,
+)
+
+
+class SimpleConversationIdFactory(ConversationIdFactoryBase):
+    def __init__(self, conversation_id: str):
+        self._conversation_id = conversation_id
+        self._conversation_refs: Dict[str, SkillConversationReference] = {}
+        # Public property to capture and assert the options passed to CreateSkillConversationIdAsync.
+        self.creation_options: SkillConversationIdFactoryOptions = None
+
+    async def create_skill_conversation_id(
+        self,
+        options_or_conversation_reference: Union[
+            SkillConversationIdFactoryOptions, ConversationReference
+        ],
+    ) -> str:
+        self.creation_options = options_or_conversation_reference
+
+        key = self._conversation_id
+        self._conversation_refs[key] = self._conversation_refs.get(
+            key,
+            SkillConversationReference(
+                conversation_reference=options_or_conversation_reference.activity.get_conversation_reference(),
+                oauth_scope=options_or_conversation_reference.from_bot_oauth_scope,
+            ),
+        )
+        return key
+
+    async def get_conversation_reference(
+        self, skill_conversation_id: str
+    ) -> SkillConversationReference:
+        return self._conversation_refs[skill_conversation_id]
+
+    async def delete_conversation_reference(self, skill_conversation_id: str):
+        raise NotImplementedError()
+
+
+class TestSkillHttpClientTests(aiounittest.AsyncTestCase):
+    async def test_post_activity_with_originating_audience(self):
+        conversation_id = str(uuid4())
+        conversation_id_factory = SimpleConversationIdFactory(conversation_id)
+        test_activity = MessageFactory.text("some message")
+        test_activity.conversation = ConversationAccount()
+        skill = BotFrameworkSkill(
+            id="SomeSkill",
+            app_id="",
+            skill_endpoint="https://someskill.com/api/messages",
+        )
+
+        async def _mock_post_content(
+            to_url: str,
+            token: str,  # pylint: disable=unused-argument
+            activity: Activity,
+        ) -> (int, object):
+            nonlocal self
+            self.assertEqual(skill.skill_endpoint, to_url)
+            # Assert that the activity being sent has what we expect.
+            self.assertEqual(conversation_id, activity.conversation.id)
+            self.assertEqual("https://parentbot.com/api/messages", activity.service_url)
+
+            # Create mock response.
+            return 200, None
+
+        sut = await self._create_http_client_with_mock_handler(
+            _mock_post_content, conversation_id_factory
+        )
+
+        result = await sut.post_activity_to_skill(
+            "",
+            skill,
+            "https://parentbot.com/api/messages",
+            test_activity,
+            "someOriginatingAudience",
+        )
+
+        # Assert factory options
+        self.assertEqual("", conversation_id_factory.creation_options.from_bot_id)
+        self.assertEqual(
+            "someOriginatingAudience",
+            conversation_id_factory.creation_options.from_bot_oauth_scope,
+        )
+        self.assertEqual(
+            test_activity, conversation_id_factory.creation_options.activity
+        )
+        self.assertEqual(
+            skill, conversation_id_factory.creation_options.bot_framework_skill
+        )
+
+        # Assert result
+        self.assertIsInstance(result, InvokeResponse)
+        self.assertEqual(200, result.status)
+
+    async def test_post_activity_using_invoke_response(self):
+        for is_gov in [True, False]:
+            with self.subTest(is_government=is_gov):
+                # pylint: disable=undefined-variable
+                # pylint: disable=cell-var-from-loop
+                conversation_id = str(uuid4())
+                conversation_id_factory = SimpleConversationIdFactory(conversation_id)
+                test_activity = MessageFactory.text("some message")
+                test_activity.conversation = ConversationAccount()
+                expected_oauth_scope = (
+                    AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
+                )
+                mock_channel_provider: ChannelProvider = Mock(spec=ChannelProvider)
+
+                def is_government_mock():
+                    nonlocal expected_oauth_scope
+                    if is_government:
+                        expected_oauth_scope = (
+                            GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
+                        )
+
+                    return is_government
+
+                mock_channel_provider.is_government = Mock(
+                    side_effect=is_government_mock
+                )
+
+                skill = BotFrameworkSkill(
+                    id="SomeSkill",
+                    app_id="",
+                    skill_endpoint="https://someskill.com/api/messages",
+                )
+
+                async def _mock_post_content(
+                    to_url: str,
+                    token: str,  # pylint: disable=unused-argument
+                    activity: Activity,
+                ) -> (int, object):
+                    nonlocal self
+
+                    self.assertEqual(skill.skill_endpoint, to_url)
+                    # Assert that the activity being sent has what we expect.
+                    self.assertEqual(conversation_id, activity.conversation.id)
+                    self.assertEqual(
+                        "https://parentbot.com/api/messages", activity.service_url
+                    )
+
+                    # Create mock response.
+                    return 200, None
+
+                sut = await self._create_http_client_with_mock_handler(
+                    _mock_post_content, conversation_id_factory
+                )
+                result = await sut.post_activity_to_skill(
+                    "", skill, "https://parentbot.com/api/messages", test_activity
+                )
+
+                # Assert factory options
+                self.assertEqual(
+                    "", conversation_id_factory.creation_options.from_bot_id
+                )
+                self.assertEqual(
+                    expected_oauth_scope,
+                    conversation_id_factory.creation_options.from_bot_oauth_scope,
+                )
+                self.assertEqual(
+                    test_activity, conversation_id_factory.creation_options.activity
+                )
+                self.assertEqual(
+                    skill, conversation_id_factory.creation_options.bot_framework_skill
+                )
+
+                # Assert result
+                self.assertIsInstance(result, InvokeResponse)
+                self.assertEqual(200, result.status)
+
+    # Helper to create an HttpClient with a mock message handler that executes function argument to validate the request
+    # and mock a response.
+    async def _create_http_client_with_mock_handler(
+        self,
+        value_function: Callable[[object], Awaitable[object]],
+        id_factory: ConversationIdFactoryBase,
+        channel_provider: ChannelProvider = None,
+    ) -> SkillHttpClient:
+        # pylint: disable=protected-access
+        client = SkillHttpClient(Mock(), id_factory, channel_provider)
+        client._post_content = value_function
+        await client._session.close()
+
+        return client
diff --git a/libraries/botbuilder-integration-aiohttp/tests/test_bot_framework_http_client.py b/libraries/botbuilder-integration-aiohttp/tests/test_bot_framework_http_client.py
new file mode 100644
index 000000000..89ea01539
--- /dev/null
+++ b/libraries/botbuilder-integration-aiohttp/tests/test_bot_framework_http_client.py
@@ -0,0 +1,72 @@
+from unittest.mock import Mock
+
+import aiounittest
+from botbuilder.schema import ConversationAccount, ChannelAccount, RoleTypes
+from botbuilder.integration.aiohttp import BotFrameworkHttpClient
+from botframework.connector.auth import CredentialProvider, Activity
+
+
+class TestBotFrameworkHttpClient(aiounittest.AsyncTestCase):
+    async def test_should_create_connector_client(self):
+        with self.assertRaises(TypeError):
+            BotFrameworkHttpClient(None)
+
+    async def test_adds_recipient_and_sets_it_back_to_null(self):
+        mock_credential_provider = Mock(spec=CredentialProvider)
+
+        # pylint: disable=unused-argument
+        async def _mock_post_content(
+            to_url: str, token: str, activity: Activity
+        ) -> (int, object):
+            nonlocal self
+            self.assertIsNotNone(activity.recipient)
+            return 200, None
+
+        client = BotFrameworkHttpClient(credential_provider=mock_credential_provider)
+        client._post_content = _mock_post_content  # pylint: disable=protected-access
+
+        activity = Activity(conversation=ConversationAccount())
+
+        await client.post_activity(
+            None,
+            None,
+            "https://skillbot.com/api/messages",
+            "https://parentbot.com/api/messages",
+            "NewConversationId",
+            activity,
+        )
+
+        assert activity.recipient is None
+
+    async def test_does_not_overwrite_non_null_recipient_values(self):
+        skill_recipient_id = "skillBot"
+        mock_credential_provider = Mock(spec=CredentialProvider)
+
+        # pylint: disable=unused-argument
+        async def _mock_post_content(
+            to_url: str, token: str, activity: Activity
+        ) -> (int, object):
+            nonlocal self
+            self.assertIsNotNone(activity.recipient)
+            self.assertEqual(skill_recipient_id, activity.recipient.id)
+            return 200, None
+
+        client = BotFrameworkHttpClient(credential_provider=mock_credential_provider)
+        client._post_content = _mock_post_content  # pylint: disable=protected-access
+
+        activity = Activity(
+            conversation=ConversationAccount(),
+            recipient=ChannelAccount(id=skill_recipient_id),
+        )
+
+        await client.post_activity(
+            None,
+            None,
+            "https://skillbot.com/api/messages",
+            "https://parentbot.com/api/messages",
+            "NewConversationId",
+            activity,
+        )
+
+        assert activity.recipient.id == skill_recipient_id
+        assert activity.recipient.role is RoleTypes.skill
diff --git a/libraries/botbuilder-integration-applicationinsights-aiohttp/README.rst b/libraries/botbuilder-integration-applicationinsights-aiohttp/README.rst
new file mode 100644
index 000000000..1b2e58f0f
--- /dev/null
+++ b/libraries/botbuilder-integration-applicationinsights-aiohttp/README.rst
@@ -0,0 +1,87 @@
+
+========================================================
+BotBuilder-ApplicationInsights SDK extension for aiohttp
+========================================================
+
+.. image:: https://dev.azure.com/FuseLabs/SDK_v4/_apis/build/status/Python/Python-CI-PR-yaml?branchName=master
+   :target:  https://dev.azure.com/FuseLabs/SDK_v4/_apis/build/status/Python/Python-CI-PR-yaml?branchName=master
+   :align: right
+   :alt: Azure DevOps status for master branch
+.. image:: https://badge.fury.io/py/botbuilder-integration-applicationinsights-aiohttp.svg
+   :target: https://badge.fury.io/py/botbuilder-integration-applicationinsights-aiohttp
+   :alt: Latest PyPI package version
+
+Within the Bot Framework, BotBuilder-ApplicationInsights enables the Azure Application Insights service.
+
+Application Insights is an extensible Application Performance Management (APM) service for developers on multiple platforms. 
+Use it to monitor your live bot application. It includes powerful analytics tools to help you diagnose issues and to understand 
+what users actually do with your bot.
+
+How to Install
+==============
+
+.. code-block:: python
+  
+  pip install botbuilder-integration-applicationinsights-aiohttp
+
+
+Documentation/Wiki
+==================
+
+You can find more information on the botbuilder-python project by visiting our `Wiki`_.
+
+Requirements
+============
+
+* `Python >= 3.7.0`_
+
+
+Source Code
+===========
+The latest developer version is available in a github repository:
+https://github.com/Microsoft/botbuilder-python/
+
+
+Contributing
+============
+
+This project welcomes contributions and suggestions.  Most contributions require you to agree to a
+Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
+the rights to use your contribution. For details, visit https://cla.microsoft.com.
+
+When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
+a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
+provided by the bot. You will only need to do this once across all repos using our CLA.
+
+This project has adopted the `Microsoft Open Source Code of Conduct`_.
+For more information see the `Code of Conduct FAQ`_ or
+contact `opencode@microsoft.com`_ with any additional questions or comments.
+
+Reporting Security Issues
+=========================
+
+Security issues and bugs should be reported privately, via email, to the Microsoft Security
+Response Center (MSRC) at `secure@microsoft.com`_. You should
+receive a response within 24 hours. If for some reason you do not, please follow up via
+email to ensure we received your original message. Further information, including the
+`MSRC PGP`_ key, can be found in
+the `Security TechCenter`_.
+
+License
+=======
+
+Copyright (c) Microsoft Corporation. All rights reserved.
+
+Licensed under the MIT_ License.
+
+.. _Wiki: https://github.com/Microsoft/botbuilder-python/wiki
+.. _Python >= 3.7.0: https://www.python.org/downloads/
+.. _MIT: https://github.com/Microsoft/vscode/blob/master/LICENSE.txt
+.. _Microsoft Open Source Code of Conduct: https://opensource.microsoft.com/codeofconduct/
+.. _Code of Conduct FAQ: https://opensource.microsoft.com/codeofconduct/faq/
+.. _opencode@microsoft.com: mailto:opencode@microsoft.com
+.. _secure@microsoft.com: mailto:secure@microsoft.com
+.. _MSRC PGP: https://technet.microsoft.com/en-us/security/dn606155
+.. _Security TechCenter: https://github.com/Microsoft/vscode/blob/master/LICENSE.txt
+
+.. `_
\ No newline at end of file
diff --git a/libraries/botbuilder-integration-applicationinsights-aiohttp/botbuilder/integration/applicationinsights/aiohttp/__init__.py b/libraries/botbuilder-integration-applicationinsights-aiohttp/botbuilder/integration/applicationinsights/aiohttp/__init__.py
new file mode 100644
index 000000000..7dd6e6aa4
--- /dev/null
+++ b/libraries/botbuilder-integration-applicationinsights-aiohttp/botbuilder/integration/applicationinsights/aiohttp/__init__.py
@@ -0,0 +1,7 @@
+from .aiohttp_telemetry_middleware import bot_telemetry_middleware
+from .aiohttp_telemetry_processor import AiohttpTelemetryProcessor
+
+__all__ = [
+    "bot_telemetry_middleware",
+    "AiohttpTelemetryProcessor",
+]
diff --git a/libraries/botbuilder-integration-applicationinsights-aiohttp/botbuilder/integration/applicationinsights/aiohttp/about.py b/libraries/botbuilder-integration-applicationinsights-aiohttp/botbuilder/integration/applicationinsights/aiohttp/about.py
new file mode 100644
index 000000000..0365e66df
--- /dev/null
+++ b/libraries/botbuilder-integration-applicationinsights-aiohttp/botbuilder/integration/applicationinsights/aiohttp/about.py
@@ -0,0 +1,15 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+"""Bot Framework Application Insights integration package for aiohttp library."""
+
+import os
+
+__title__ = "botbuilder-integration-applicationinsights-aiohttp"
+__version__ = (
+    os.environ["packageVersion"] if "packageVersion" in os.environ else "4.12.0"
+)
+__uri__ = "https://www.github.com/Microsoft/botbuilder-python"
+__author__ = "Microsoft"
+__description__ = "Microsoft Bot Framework Bot Builder"
+__summary__ = "Microsoft Bot Framework Bot Builder SDK for Python."
+__license__ = "MIT"
diff --git a/libraries/botbuilder-integration-applicationinsights-aiohttp/botbuilder/integration/applicationinsights/aiohttp/aiohttp_telemetry_middleware.py b/libraries/botbuilder-integration-applicationinsights-aiohttp/botbuilder/integration/applicationinsights/aiohttp/aiohttp_telemetry_middleware.py
new file mode 100644
index 000000000..30615f5c2
--- /dev/null
+++ b/libraries/botbuilder-integration-applicationinsights-aiohttp/botbuilder/integration/applicationinsights/aiohttp/aiohttp_telemetry_middleware.py
@@ -0,0 +1,27 @@
+from threading import current_thread
+from aiohttp.web import middleware
+
+# Map of thread id => POST body text
+_REQUEST_BODIES = {}
+
+
+def retrieve_aiohttp_body():
+    """
+    Retrieve the POST body text from temporary cache.
+
+    The POST body corresponds with the thread id and should resides in
+    cache just for lifetime of request.
+    """
+    result = _REQUEST_BODIES.pop(current_thread().ident, None)
+    return result
+
+
+@middleware
+async def bot_telemetry_middleware(request, handler):
+    """Process the incoming Flask request."""
+    if "application/json" in request.headers["Content-Type"]:
+        body = await request.json()
+        _REQUEST_BODIES[current_thread().ident] = body
+
+    response = await handler(request)
+    return response
diff --git a/libraries/botbuilder-integration-applicationinsights-aiohttp/botbuilder/integration/applicationinsights/aiohttp/aiohttp_telemetry_processor.py b/libraries/botbuilder-integration-applicationinsights-aiohttp/botbuilder/integration/applicationinsights/aiohttp/aiohttp_telemetry_processor.py
new file mode 100644
index 000000000..2962a5fe8
--- /dev/null
+++ b/libraries/botbuilder-integration-applicationinsights-aiohttp/botbuilder/integration/applicationinsights/aiohttp/aiohttp_telemetry_processor.py
@@ -0,0 +1,24 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+"""Telemetry processor for aiohttp."""
+import sys
+
+from botbuilder.applicationinsights.processor.telemetry_processor import (
+    TelemetryProcessor,
+)
+from .aiohttp_telemetry_middleware import retrieve_aiohttp_body
+
+
+class AiohttpTelemetryProcessor(TelemetryProcessor):
+    def can_process(self) -> bool:
+        return self.detect_aiohttp()
+
+    def get_request_body(self) -> str:
+        if self.detect_aiohttp():
+            return retrieve_aiohttp_body()
+        return None
+
+    @staticmethod
+    def detect_aiohttp() -> bool:
+        """Detects if running in aiohttp."""
+        return "aiohttp" in sys.modules
diff --git a/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py b/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py
new file mode 100644
index 000000000..33f018439
--- /dev/null
+++ b/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py
@@ -0,0 +1,62 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+from setuptools import setup
+
+REQUIRES = [
+    "applicationinsights>=0.11.9",
+    "aiohttp==3.6.2",
+    "botbuilder-schema==4.12.0",
+    "botframework-connector==4.12.0",
+    "botbuilder-core==4.12.0",
+    "botbuilder-applicationinsights==4.12.0",
+]
+TESTS_REQUIRES = [
+    "aiounittest==1.3.0",
+]
+
+root = os.path.abspath(os.path.dirname(__file__))
+
+with open(
+    os.path.join(
+        root, "botbuilder", "integration", "applicationinsights", "aiohttp", "about.py"
+    )
+) as f:
+    package_info = {}
+    info = f.read()
+    exec(info, package_info)
+
+with open(os.path.join(root, "README.rst"), encoding="utf-8") as f:
+    long_description = f.read()
+
+setup(
+    name=package_info["__title__"],
+    version=package_info["__version__"],
+    url=package_info["__uri__"],
+    author=package_info["__author__"],
+    description=package_info["__description__"],
+    keywords=[
+        "BotBuilderApplicationInsights",
+        "bots",
+        "ai",
+        "botframework",
+        "botbuilder",
+        "aiohttp",
+    ],
+    long_description=long_description,
+    long_description_content_type="text/x-rst",
+    license=package_info["__license__"],
+    packages=["botbuilder.integration.applicationinsights.aiohttp"],
+    install_requires=REQUIRES + TESTS_REQUIRES,
+    tests_require=TESTS_REQUIRES,
+    include_package_data=True,
+    classifiers=[
+        "Programming Language :: Python :: 3.7",
+        "Intended Audience :: Developers",
+        "License :: OSI Approved :: MIT License",
+        "Operating System :: OS Independent",
+        "Development Status :: 5 - Production/Stable",
+        "Topic :: Scientific/Engineering :: Artificial Intelligence",
+    ],
+)
diff --git a/libraries/botbuilder-integration-applicationinsights-aiohttp/tests/test_aiohttp_processor.py b/libraries/botbuilder-integration-applicationinsights-aiohttp/tests/test_aiohttp_processor.py
new file mode 100644
index 000000000..37ca54267
--- /dev/null
+++ b/libraries/botbuilder-integration-applicationinsights-aiohttp/tests/test_aiohttp_processor.py
@@ -0,0 +1,26 @@
+from unittest.mock import Mock
+from aiounittest import AsyncTestCase
+
+import aiohttp  # pylint: disable=unused-import
+
+from botbuilder.integration.applicationinsights.aiohttp import (
+    aiohttp_telemetry_middleware,
+    AiohttpTelemetryProcessor,
+)
+
+
+class TestAiohttpTelemetryProcessor(AsyncTestCase):
+    # pylint: disable=protected-access
+    def test_can_process(self):
+        assert AiohttpTelemetryProcessor.detect_aiohttp()
+        assert AiohttpTelemetryProcessor().can_process()
+
+    def test_retrieve_aiohttp_body(self):
+        aiohttp_telemetry_middleware._REQUEST_BODIES = Mock()
+        aiohttp_telemetry_middleware._REQUEST_BODIES.pop = Mock(
+            return_value="test body"
+        )
+        assert aiohttp_telemetry_middleware.retrieve_aiohttp_body() == "test body"
+
+        assert AiohttpTelemetryProcessor().get_request_body() == "test body"
+        aiohttp_telemetry_middleware._REQUEST_BODIES = {}
diff --git a/libraries/botbuilder-integration-applicationinsights-aiohttp/tests/test_aiohttp_telemetry_middleware.py b/libraries/botbuilder-integration-applicationinsights-aiohttp/tests/test_aiohttp_telemetry_middleware.py
new file mode 100644
index 000000000..673040b4b
--- /dev/null
+++ b/libraries/botbuilder-integration-applicationinsights-aiohttp/tests/test_aiohttp_telemetry_middleware.py
@@ -0,0 +1,35 @@
+from asyncio import Future
+from unittest.mock import Mock, MagicMock
+from aiounittest import AsyncTestCase
+
+from botbuilder.integration.applicationinsights.aiohttp import (
+    bot_telemetry_middleware,
+    aiohttp_telemetry_middleware,
+)
+
+
+class TestAiohttpTelemetryMiddleware(AsyncTestCase):
+    # pylint: disable=protected-access
+    async def test_bot_telemetry_middleware(self):
+        req = Mock()
+        req.headers = {"Content-Type": "application/json"}
+        req.json = MagicMock(return_value=Future())
+        req.json.return_value.set_result("mock body")
+
+        async def handler(value):
+            return value
+
+        sut = await bot_telemetry_middleware(req, handler)
+
+        assert "mock body" in aiohttp_telemetry_middleware._REQUEST_BODIES.values()
+        aiohttp_telemetry_middleware._REQUEST_BODIES.clear()
+        assert req == sut
+
+    def test_retrieve_aiohttp_body(self):
+        aiohttp_telemetry_middleware._REQUEST_BODIES = Mock()
+        aiohttp_telemetry_middleware._REQUEST_BODIES.pop = Mock(
+            return_value="test body"
+        )
+        assert aiohttp_telemetry_middleware.retrieve_aiohttp_body() == "test body"
+
+        aiohttp_telemetry_middleware._REQUEST_BODIES = {}
diff --git a/libraries/botbuilder-schema/README.rst b/libraries/botbuilder-schema/README.rst
index abf3ae738..ff3eac173 100644
--- a/libraries/botbuilder-schema/README.rst
+++ b/libraries/botbuilder-schema/README.rst
@@ -3,8 +3,8 @@
 BotBuilder-Schema
 =================
 
-.. image:: https://fuselabs.visualstudio.com/SDK_v4/_apis/build/status/Python/SDK_v4-Python-CI?branchName=master
-   :target:  https://fuselabs.visualstudio.com/SDK_v4/_apis/build/status/Python/SDK_v4-Python-CI
+.. image:: https://dev.azure.com/FuseLabs/SDK_v4/_apis/build/status/Python/Python-CI-PR-yaml?branchName=master
+   :target:  https://dev.azure.com/FuseLabs/SDK_v4/_apis/build/status/Python/Python-CI-PR-yaml?branchName=master
    :align: right
    :alt: Azure DevOps status for master branch
 .. image:: https://badge.fury.io/py/botbuilder-schema.svg
diff --git a/libraries/botbuilder-schema/botbuilder/schema/__init__.py b/libraries/botbuilder-schema/botbuilder/schema/__init__.py
index b484cc672..734d6d91c 100644
--- a/libraries/botbuilder-schema/botbuilder/schema/__init__.py
+++ b/libraries/botbuilder-schema/botbuilder/schema/__init__.py
@@ -1,138 +1,57 @@
-# coding=utf-8
-# --------------------------------------------------------------------------
 # Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License. See License.txt in the project root for
-# license information.
-#
-# Code generated by Microsoft (R) AutoRest Code Generator.
-# Changes may cause incorrect behavior and will be lost if the code is
-# regenerated.
-# --------------------------------------------------------------------------
+# Licensed under the MIT License.
 
-try:
-    from ._models_py3 import Activity
-    from ._models_py3 import AnimationCard
-    from ._models_py3 import Attachment
-    from ._models_py3 import AttachmentData
-    from ._models_py3 import AttachmentInfo
-    from ._models_py3 import AttachmentView
-    from ._models_py3 import AudioCard
-    from ._models_py3 import BasicCard
-    from ._models_py3 import CardAction
-    from ._models_py3 import CardImage
-    from ._models_py3 import ChannelAccount
-    from ._models_py3 import ConversationAccount
-    from ._models_py3 import ConversationMembers
-    from ._models_py3 import ConversationParameters
-    from ._models_py3 import ConversationReference
-    from ._models_py3 import ConversationResourceResponse
-    from ._models_py3 import ConversationsResult
-    from ._models_py3 import Entity
-    from ._models_py3 import Error
-    from ._models_py3 import ErrorResponse, ErrorResponseException
-    from ._models_py3 import Fact
-    from ._models_py3 import GeoCoordinates
-    from ._models_py3 import HeroCard
-    from ._models_py3 import InnerHttpError
-    from ._models_py3 import MediaCard
-    from ._models_py3 import MediaEventValue
-    from ._models_py3 import MediaUrl
-    from ._models_py3 import Mention
-    from ._models_py3 import MessageReaction
-    from ._models_py3 import MicrosoftPayMethodData
-    from ._models_py3 import OAuthCard
-    from ._models_py3 import PagedMembersResult
-    from ._models_py3 import PaymentAddress
-    from ._models_py3 import PaymentCurrencyAmount
-    from ._models_py3 import PaymentDetails
-    from ._models_py3 import PaymentDetailsModifier
-    from ._models_py3 import PaymentItem
-    from ._models_py3 import PaymentMethodData
-    from ._models_py3 import PaymentOptions
-    from ._models_py3 import PaymentRequest
-    from ._models_py3 import PaymentRequestComplete
-    from ._models_py3 import PaymentRequestCompleteResult
-    from ._models_py3 import PaymentRequestUpdate
-    from ._models_py3 import PaymentRequestUpdateResult
-    from ._models_py3 import PaymentResponse
-    from ._models_py3 import PaymentShippingOption
-    from ._models_py3 import Place
-    from ._models_py3 import ReceiptCard
-    from ._models_py3 import ReceiptItem
-    from ._models_py3 import ResourceResponse
-    from ._models_py3 import SemanticAction
-    from ._models_py3 import SigninCard
-    from ._models_py3 import SuggestedActions
-    from ._models_py3 import TextHighlight
-    from ._models_py3 import Thing
-    from ._models_py3 import ThumbnailCard
-    from ._models_py3 import ThumbnailUrl
-    from ._models_py3 import TokenRequest
-    from ._models_py3 import TokenResponse
-    from ._models_py3 import Transcript
-    from ._models_py3 import VideoCard
-except (SyntaxError, ImportError):
-    from ._models import Activity
-    from ._models import AnimationCard
-    from ._models import Attachment
-    from ._models import AttachmentData
-    from ._models import AttachmentInfo
-    from ._models import AttachmentView
-    from ._models import AudioCard
-    from ._models import BasicCard
-    from ._models import CardAction
-    from ._models import CardImage
-    from ._models import ChannelAccount
-    from ._models import ConversationAccount
-    from ._models import ConversationMembers
-    from ._models import ConversationParameters
-    from ._models import ConversationReference
-    from ._models import ConversationResourceResponse
-    from ._models import ConversationsResult
-    from ._models import Entity
-    from ._models import Error
-    from ._models import ErrorResponse, ErrorResponseException
-    from ._models import Fact
-    from ._models import GeoCoordinates
-    from ._models import HeroCard
-    from ._models import InnerHttpError
-    from ._models import MediaCard
-    from ._models import MediaEventValue
-    from ._models import MediaUrl
-    from ._models import Mention
-    from ._models import MessageReaction
-    from ._models import MicrosoftPayMethodData
-    from ._models import OAuthCard
-    from ._models import PagedMembersResult
-    from ._models import PaymentAddress
-    from ._models import PaymentCurrencyAmount
-    from ._models import PaymentDetails
-    from ._models import PaymentDetailsModifier
-    from ._models import PaymentItem
-    from ._models import PaymentMethodData
-    from ._models import PaymentOptions
-    from ._models import PaymentRequest
-    from ._models import PaymentRequestComplete
-    from ._models import PaymentRequestCompleteResult
-    from ._models import PaymentRequestUpdate
-    from ._models import PaymentRequestUpdateResult
-    from ._models import PaymentResponse
-    from ._models import PaymentShippingOption
-    from ._models import Place
-    from ._models import ReceiptCard
-    from ._models import ReceiptItem
-    from ._models import ResourceResponse
-    from ._models import SemanticAction
-    from ._models import SigninCard
-    from ._models import SuggestedActions
-    from ._models import TextHighlight
-    from ._models import Thing
-    from ._models import ThumbnailCard
-    from ._models import ThumbnailUrl
-    from ._models import TokenRequest
-    from ._models import TokenResponse
-    from ._models import Transcript
-    from ._models import VideoCard
+from ._models_py3 import Activity
+from ._models_py3 import ActivityEventNames
+from ._models_py3 import AnimationCard
+from ._models_py3 import Attachment
+from ._models_py3 import AttachmentData
+from ._models_py3 import AttachmentInfo
+from ._models_py3 import AttachmentView
+from ._models_py3 import AudioCard
+from ._models_py3 import BasicCard
+from ._models_py3 import CardAction
+from ._models_py3 import CardImage
+from ._models_py3 import ChannelAccount
+from ._models_py3 import ConversationAccount
+from ._models_py3 import ConversationMembers
+from ._models_py3 import ConversationParameters
+from ._models_py3 import ConversationReference
+from ._models_py3 import ConversationResourceResponse
+from ._models_py3 import ConversationsResult
+from ._models_py3 import ExpectedReplies
+from ._models_py3 import Entity
+from ._models_py3 import Error
+from ._models_py3 import ErrorResponse, ErrorResponseException
+from ._models_py3 import Fact
+from ._models_py3 import GeoCoordinates
+from ._models_py3 import HeroCard
+from ._models_py3 import InnerHttpError
+from ._models_py3 import MediaCard
+from ._models_py3 import MediaEventValue
+from ._models_py3 import MediaUrl
+from ._models_py3 import Mention
+from ._models_py3 import MessageReaction
+from ._models_py3 import OAuthCard
+from ._models_py3 import PagedMembersResult
+from ._models_py3 import Place
+from ._models_py3 import ReceiptCard
+from ._models_py3 import ReceiptItem
+from ._models_py3 import ResourceResponse
+from ._models_py3 import SemanticAction
+from ._models_py3 import SigninCard
+from ._models_py3 import SuggestedActions
+from ._models_py3 import TextHighlight
+from ._models_py3 import Thing
+from ._models_py3 import ThumbnailCard
+from ._models_py3 import ThumbnailUrl
+from ._models_py3 import TokenExchangeInvokeRequest
+from ._models_py3 import TokenExchangeInvokeResponse
+from ._models_py3 import TokenExchangeState
+from ._models_py3 import TokenRequest
+from ._models_py3 import TokenResponse
+from ._models_py3 import Transcript
+from ._models_py3 import VideoCard
 from ._connector_client_enums import (
     ActionTypes,
     ActivityImportance,
@@ -148,8 +67,15 @@
     TextFormatTypes,
 )
 
+from ._sign_in_enums import SignInConstants
+from .callerid_constants import CallerIdConstants
+from .health_results import HealthResults
+from .healthcheck_response import HealthCheckResponse
+from .speech_constants import SpeechConstants
+
 __all__ = [
     "Activity",
+    "ActivityEventNames",
     "AnimationCard",
     "Attachment",
     "AttachmentData",
@@ -166,6 +92,7 @@
     "ConversationReference",
     "ConversationResourceResponse",
     "ConversationsResult",
+    "ExpectedReplies",
     "Entity",
     "Error",
     "ErrorResponse",
@@ -179,34 +106,23 @@
     "MediaUrl",
     "Mention",
     "MessageReaction",
-    "MicrosoftPayMethodData",
     "OAuthCard",
     "PagedMembersResult",
-    "PaymentAddress",
-    "PaymentCurrencyAmount",
-    "PaymentDetails",
-    "PaymentDetailsModifier",
-    "PaymentItem",
-    "PaymentMethodData",
-    "PaymentOptions",
-    "PaymentRequest",
-    "PaymentRequestComplete",
-    "PaymentRequestCompleteResult",
-    "PaymentRequestUpdate",
-    "PaymentRequestUpdateResult",
-    "PaymentResponse",
-    "PaymentShippingOption",
     "Place",
     "ReceiptCard",
     "ReceiptItem",
     "ResourceResponse",
     "SemanticAction",
     "SigninCard",
+    "SignInConstants",
     "SuggestedActions",
     "TextHighlight",
     "Thing",
     "ThumbnailCard",
     "ThumbnailUrl",
+    "TokenExchangeInvokeRequest",
+    "TokenExchangeInvokeResponse",
+    "TokenExchangeState",
     "TokenRequest",
     "TokenResponse",
     "Transcript",
@@ -223,4 +139,8 @@
     "DeliveryModes",
     "ContactRelationUpdateActionTypes",
     "InstallationUpdateActionTypes",
+    "CallerIdConstants",
+    "HealthResults",
+    "HealthCheckResponse",
+    "SpeechConstants",
 ]
diff --git a/libraries/botbuilder-schema/botbuilder/schema/_connector_client_enums.py b/libraries/botbuilder-schema/botbuilder/schema/_connector_client_enums.py
index 605600aa9..289944b5a 100644
--- a/libraries/botbuilder-schema/botbuilder/schema/_connector_client_enums.py
+++ b/libraries/botbuilder-schema/botbuilder/schema/_connector_client_enums.py
@@ -1,13 +1,5 @@
-# coding=utf-8
-# --------------------------------------------------------------------------
 # Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License. See License.txt in the project root for
-# license information.
-#
-# Code generated by Microsoft (R) AutoRest Code Generator.
-# Changes may cause incorrect behavior and will be lost if the code is
-# regenerated.
-# --------------------------------------------------------------------------
+# Licensed under the MIT License.
 
 from enum import Enum
 
@@ -16,6 +8,7 @@ class RoleTypes(str, Enum):
 
     user = "user"
     bot = "bot"
+    skill = "skill"
 
 
 class ActivityTypes(str, Enum):
@@ -27,6 +20,7 @@ class ActivityTypes(str, Enum):
     end_of_conversation = "endOfConversation"
     event = "event"
     invoke = "invoke"
+    invoke_response = "invokeResponse"
     delete_user_data = "deleteUserData"
     message_update = "messageUpdate"
     message_delete = "messageDelete"
@@ -74,7 +68,6 @@ class ActionTypes(str, Enum):
     download_file = "downloadFile"
     signin = "signin"
     call = "call"
-    payment = "payment"
     message_back = "messageBack"
 
 
@@ -99,6 +92,8 @@ class DeliveryModes(str, Enum):
 
     normal = "normal"
     notification = "notification"
+    expect_replies = "expectReplies"
+    ephemeral = "ephemeral"
 
 
 class ContactRelationUpdateActionTypes(str, Enum):
diff --git a/libraries/botbuilder-schema/botbuilder/schema/_models.py b/libraries/botbuilder-schema/botbuilder/schema/_models.py
deleted file mode 100644
index 736ddcf81..000000000
--- a/libraries/botbuilder-schema/botbuilder/schema/_models.py
+++ /dev/null
@@ -1,2079 +0,0 @@
-# coding=utf-8
-# --------------------------------------------------------------------------
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License. See License.txt in the project root for
-# license information.
-#
-# Code generated by Microsoft (R) AutoRest Code Generator.
-# Changes may cause incorrect behavior and will be lost if the code is
-# regenerated.
-# --------------------------------------------------------------------------
-
-from msrest.serialization import Model
-from msrest.exceptions import HttpOperationError
-
-
-class Activity(Model):
-    """An Activity is the basic communication type for the Bot Framework 3.0
-    protocol.
-
-    :param type: Contains the activity type. Possible values include:
-     'message', 'contactRelationUpdate', 'conversationUpdate', 'typing',
-     'endOfConversation', 'event', 'invoke', 'deleteUserData', 'messageUpdate',
-     'messageDelete', 'installationUpdate', 'messageReaction', 'suggestion',
-     'trace', 'handoff'
-    :type type: str or ~botframework.connector.models.ActivityTypes
-    :param id: Contains an ID that uniquely identifies the activity on the
-     channel.
-    :type id: str
-    :param timestamp: Contains the date and time that the message was sent, in
-     UTC, expressed in ISO-8601 format.
-    :type timestamp: datetime
-    :param local_timestamp: Contains the local date and time of the message
-     expressed in ISO-8601 format.
-     For example, 2016-09-23T13:07:49.4714686-07:00.
-    :type local_timestamp: datetime
-    :param local_timezone: Contains the name of the local timezone of the message,
-     expressed in IANA Time Zone database format.
-     For example, America/Los_Angeles.
-    :type local_timezone: str
-    :param service_url: Contains the URL that specifies the channel's service
-     endpoint. Set by the channel.
-    :type service_url: str
-    :param channel_id: Contains an ID that uniquely identifies the channel.
-     Set by the channel.
-    :type channel_id: str
-    :param from_property: Identifies the sender of the message.
-    :type from_property: ~botframework.connector.models.ChannelAccount
-    :param conversation: Identifies the conversation to which the activity
-     belongs.
-    :type conversation: ~botframework.connector.models.ConversationAccount
-    :param recipient: Identifies the recipient of the message.
-    :type recipient: ~botframework.connector.models.ChannelAccount
-    :param text_format: Format of text fields Default:markdown. Possible
-     values include: 'markdown', 'plain', 'xml'
-    :type text_format: str or ~botframework.connector.models.TextFormatTypes
-    :param attachment_layout: The layout hint for multiple attachments.
-     Default: list. Possible values include: 'list', 'carousel'
-    :type attachment_layout: str or
-     ~botframework.connector.models.AttachmentLayoutTypes
-    :param members_added: The collection of members added to the conversation.
-    :type members_added: list[~botframework.connector.models.ChannelAccount]
-    :param members_removed: The collection of members removed from the
-     conversation.
-    :type members_removed: list[~botframework.connector.models.ChannelAccount]
-    :param reactions_added: The collection of reactions added to the
-     conversation.
-    :type reactions_added:
-     list[~botframework.connector.models.MessageReaction]
-    :param reactions_removed: The collection of reactions removed from the
-     conversation.
-    :type reactions_removed:
-     list[~botframework.connector.models.MessageReaction]
-    :param topic_name: The updated topic name of the conversation.
-    :type topic_name: str
-    :param history_disclosed: Indicates whether the prior history of the
-     channel is disclosed.
-    :type history_disclosed: bool
-    :param locale: A locale name for the contents of the text field.
-     The locale name is a combination of an ISO 639 two- or three-letter
-     culture code associated with a language
-     and an ISO 3166 two-letter subculture code associated with a country or
-     region.
-     The locale name can also correspond to a valid BCP-47 language tag.
-    :type locale: str
-    :param text: The text content of the message.
-    :type text: str
-    :param speak: The text to speak.
-    :type speak: str
-    :param input_hint: Indicates whether your bot is accepting,
-     expecting, or ignoring user input after the message is delivered to the
-     client. Possible values include: 'acceptingInput', 'ignoringInput',
-     'expectingInput'
-    :type input_hint: str or ~botframework.connector.models.InputHints
-    :param summary: The text to display if the channel cannot render cards.
-    :type summary: str
-    :param suggested_actions: The suggested actions for the activity.
-    :type suggested_actions: ~botframework.connector.models.SuggestedActions
-    :param attachments: Attachments
-    :type attachments: list[~botframework.connector.models.Attachment]
-    :param entities: Represents the entities that were mentioned in the
-     message.
-    :type entities: list[~botframework.connector.models.Entity]
-    :param channel_data: Contains channel-specific content.
-    :type channel_data: object
-    :param action: Indicates whether the recipient of a contactRelationUpdate
-     was added or removed from the sender's contact list.
-    :type action: str
-    :param reply_to_id: Contains the ID of the message to which this message
-     is a reply.
-    :type reply_to_id: str
-    :param label: A descriptive label for the activity.
-    :type label: str
-    :param value_type: The type of the activity's value object.
-    :type value_type: str
-    :param value: A value that is associated with the activity.
-    :type value: object
-    :param name: The name of the operation associated with an invoke or event
-     activity.
-    :type name: str
-    :param relates_to: A reference to another conversation or activity.
-    :type relates_to: ~botframework.connector.models.ConversationReference
-    :param code: The a code for endOfConversation activities that indicates
-     why the conversation ended. Possible values include: 'unknown',
-     'completedSuccessfully', 'userCancelled', 'botTimedOut',
-     'botIssuedInvalidMessage', 'channelFailed'
-    :type code: str or ~botframework.connector.models.EndOfConversationCodes
-    :param expiration: The time at which the activity should be considered to
-     be "expired" and should not be presented to the recipient.
-    :type expiration: datetime
-    :param importance: The importance of the activity. Possible values
-     include: 'low', 'normal', 'high'
-    :type importance: str or ~botframework.connector.models.ActivityImportance
-    :param delivery_mode: A delivery hint to signal to the recipient alternate
-     delivery paths for the activity.
-     The default delivery mode is "default". Possible values include: 'normal',
-     'notification'
-    :type delivery_mode: str or ~botframework.connector.models.DeliveryModes
-    :param listen_for: List of phrases and references that speech and language
-     priming systems should listen for
-    :type listen_for: list[str]
-    :param text_highlights: The collection of text fragments to highlight when
-     the activity contains a ReplyToId value.
-    :type text_highlights: list[~botframework.connector.models.TextHighlight]
-    :param semantic_action: An optional programmatic action accompanying this
-     request
-    :type semantic_action: ~botframework.connector.models.SemanticAction
-    :param caller_id: A string containing an IRI identifying the caller of a
-     bot. This field is not intended to be transmitted over the wire, but is
-     instead populated by bots and clients based on cryptographically 
-     verifiable data that asserts the identity of the callers (e.g. tokens).
-    :type caller_id: str
-    """
-
-    _attribute_map = {
-        "type": {"key": "type", "type": "str"},
-        "id": {"key": "id", "type": "str"},
-        "timestamp": {"key": "timestamp", "type": "iso-8601"},
-        "local_timestamp": {"key": "localTimestamp", "type": "iso-8601"},
-        "local_timezone": {"key": "localTimezone", "type": "str"},
-        "service_url": {"key": "serviceUrl", "type": "str"},
-        "channel_id": {"key": "channelId", "type": "str"},
-        "from_property": {"key": "from", "type": "ChannelAccount"},
-        "conversation": {"key": "conversation", "type": "ConversationAccount"},
-        "recipient": {"key": "recipient", "type": "ChannelAccount"},
-        "text_format": {"key": "textFormat", "type": "str"},
-        "attachment_layout": {"key": "attachmentLayout", "type": "str"},
-        "members_added": {"key": "membersAdded", "type": "[ChannelAccount]"},
-        "members_removed": {"key": "membersRemoved", "type": "[ChannelAccount]"},
-        "reactions_added": {"key": "reactionsAdded", "type": "[MessageReaction]"},
-        "reactions_removed": {"key": "reactionsRemoved", "type": "[MessageReaction]"},
-        "topic_name": {"key": "topicName", "type": "str"},
-        "history_disclosed": {"key": "historyDisclosed", "type": "bool"},
-        "locale": {"key": "locale", "type": "str"},
-        "text": {"key": "text", "type": "str"},
-        "speak": {"key": "speak", "type": "str"},
-        "input_hint": {"key": "inputHint", "type": "str"},
-        "summary": {"key": "summary", "type": "str"},
-        "suggested_actions": {"key": "suggestedActions", "type": "SuggestedActions"},
-        "attachments": {"key": "attachments", "type": "[Attachment]"},
-        "entities": {"key": "entities", "type": "[Entity]"},
-        "channel_data": {"key": "channelData", "type": "object"},
-        "action": {"key": "action", "type": "str"},
-        "reply_to_id": {"key": "replyToId", "type": "str"},
-        "label": {"key": "label", "type": "str"},
-        "value_type": {"key": "valueType", "type": "str"},
-        "value": {"key": "value", "type": "object"},
-        "name": {"key": "name", "type": "str"},
-        "relates_to": {"key": "relatesTo", "type": "ConversationReference"},
-        "code": {"key": "code", "type": "str"},
-        "expiration": {"key": "expiration", "type": "iso-8601"},
-        "importance": {"key": "importance", "type": "str"},
-        "delivery_mode": {"key": "deliveryMode", "type": "str"},
-        "listen_for": {"key": "listenFor", "type": "[str]"},
-        "text_highlights": {"key": "textHighlights", "type": "[TextHighlight]"},
-        "semantic_action": {"key": "semanticAction", "type": "SemanticAction"},
-        "caller_id": {"key": "callerId", "type": "str"},
-    }
-
-    def __init__(self, **kwargs):
-        super(Activity, self).__init__(**kwargs)
-        self.type = kwargs.get("type", None)
-        self.id = kwargs.get("id", None)
-        self.timestamp = kwargs.get("timestamp", None)
-        self.local_timestamp = kwargs.get("local_timestamp", None)
-        self.local_timezone = kwargs.get("local_timezone", None)
-        self.service_url = kwargs.get("service_url", None)
-        self.channel_id = kwargs.get("channel_id", None)
-        self.from_property = kwargs.get("from_property", None)
-        self.conversation = kwargs.get("conversation", None)
-        self.recipient = kwargs.get("recipient", None)
-        self.text_format = kwargs.get("text_format", None)
-        self.attachment_layout = kwargs.get("attachment_layout", None)
-        self.members_added = kwargs.get("members_added", None)
-        self.members_removed = kwargs.get("members_removed", None)
-        self.reactions_added = kwargs.get("reactions_added", None)
-        self.reactions_removed = kwargs.get("reactions_removed", None)
-        self.topic_name = kwargs.get("topic_name", None)
-        self.history_disclosed = kwargs.get("history_disclosed", None)
-        self.locale = kwargs.get("locale", None)
-        self.text = kwargs.get("text", None)
-        self.speak = kwargs.get("speak", None)
-        self.input_hint = kwargs.get("input_hint", None)
-        self.summary = kwargs.get("summary", None)
-        self.suggested_actions = kwargs.get("suggested_actions", None)
-        self.attachments = kwargs.get("attachments", None)
-        self.entities = kwargs.get("entities", None)
-        self.channel_data = kwargs.get("channel_data", None)
-        self.action = kwargs.get("action", None)
-        self.reply_to_id = kwargs.get("reply_to_id", None)
-        self.label = kwargs.get("label", None)
-        self.value_type = kwargs.get("value_type", None)
-        self.value = kwargs.get("value", None)
-        self.name = kwargs.get("name", None)
-        self.relates_to = kwargs.get("relates_to", None)
-        self.code = kwargs.get("code", None)
-        self.expiration = kwargs.get("expiration", None)
-        self.importance = kwargs.get("importance", None)
-        self.delivery_mode = kwargs.get("delivery_mode", None)
-        self.listen_for = kwargs.get("listen_for", None)
-        self.text_highlights = kwargs.get("text_highlights", None)
-        self.semantic_action = kwargs.get("semantic_action", None)
-        self.caller_id = kwargs.get("caller_id", None)
-
-
-class AnimationCard(Model):
-    """An animation card (Ex: gif or short video clip).
-
-    :param title: Title of this card
-    :type title: str
-    :param subtitle: Subtitle of this card
-    :type subtitle: str
-    :param text: Text of this card
-    :type text: str
-    :param image: Thumbnail placeholder
-    :type image: ~botframework.connector.models.ThumbnailUrl
-    :param media: Media URLs for this card. When this field contains more than
-     one URL, each URL is an alternative format of the same content.
-    :type media: list[~botframework.connector.models.MediaUrl]
-    :param buttons: Actions on this card
-    :type buttons: list[~botframework.connector.models.CardAction]
-    :param shareable: This content may be shared with others (default:true)
-    :type shareable: bool
-    :param autoloop: Should the client loop playback at end of content
-     (default:true)
-    :type autoloop: bool
-    :param autostart: Should the client automatically start playback of media
-     in this card (default:true)
-    :type autostart: bool
-    :param aspect: Aspect ratio of thumbnail/media placeholder. Allowed values
-     are "16:9" and "4:3"
-    :type aspect: str
-    :param duration: Describes the length of the media content without
-     requiring a receiver to open the content. Formatted as an ISO 8601
-     Duration field.
-    :type duration: str
-    :param value: Supplementary parameter for this card
-    :type value: object
-    """
-
-    _attribute_map = {
-        "title": {"key": "title", "type": "str"},
-        "subtitle": {"key": "subtitle", "type": "str"},
-        "text": {"key": "text", "type": "str"},
-        "image": {"key": "image", "type": "ThumbnailUrl"},
-        "media": {"key": "media", "type": "[MediaUrl]"},
-        "buttons": {"key": "buttons", "type": "[CardAction]"},
-        "shareable": {"key": "shareable", "type": "bool"},
-        "autoloop": {"key": "autoloop", "type": "bool"},
-        "autostart": {"key": "autostart", "type": "bool"},
-        "aspect": {"key": "aspect", "type": "str"},
-        "duration": {"key": "duration", "type": "str"},
-        "value": {"key": "value", "type": "object"},
-    }
-
-    def __init__(self, **kwargs):
-        super(AnimationCard, self).__init__(**kwargs)
-        self.title = kwargs.get("title", None)
-        self.subtitle = kwargs.get("subtitle", None)
-        self.text = kwargs.get("text", None)
-        self.image = kwargs.get("image", None)
-        self.media = kwargs.get("media", None)
-        self.buttons = kwargs.get("buttons", None)
-        self.shareable = kwargs.get("shareable", None)
-        self.autoloop = kwargs.get("autoloop", None)
-        self.autostart = kwargs.get("autostart", None)
-        self.aspect = kwargs.get("aspect", None)
-        self.duration = kwargs.get("duration", None)
-        self.value = kwargs.get("value", None)
-
-
-class Attachment(Model):
-    """An attachment within an activity.
-
-    :param content_type: mimetype/Contenttype for the file
-    :type content_type: str
-    :param content_url: Content Url
-    :type content_url: str
-    :param content: Embedded content
-    :type content: object
-    :param name: (OPTIONAL) The name of the attachment
-    :type name: str
-    :param thumbnail_url: (OPTIONAL) Thumbnail associated with attachment
-    :type thumbnail_url: str
-    """
-
-    _attribute_map = {
-        "content_type": {"key": "contentType", "type": "str"},
-        "content_url": {"key": "contentUrl", "type": "str"},
-        "content": {"key": "content", "type": "object"},
-        "name": {"key": "name", "type": "str"},
-        "thumbnail_url": {"key": "thumbnailUrl", "type": "str"},
-    }
-
-    def __init__(self, **kwargs):
-        super(Attachment, self).__init__(**kwargs)
-        self.content_type = kwargs.get("content_type", None)
-        self.content_url = kwargs.get("content_url", None)
-        self.content = kwargs.get("content", None)
-        self.name = kwargs.get("name", None)
-        self.thumbnail_url = kwargs.get("thumbnail_url", None)
-
-
-class AttachmentData(Model):
-    """Attachment data.
-
-    :param type: Content-Type of the attachment
-    :type type: str
-    :param name: Name of the attachment
-    :type name: str
-    :param original_base64: Attachment content
-    :type original_base64: bytearray
-    :param thumbnail_base64: Attachment thumbnail
-    :type thumbnail_base64: bytearray
-    """
-
-    _attribute_map = {
-        "type": {"key": "type", "type": "str"},
-        "name": {"key": "name", "type": "str"},
-        "original_base64": {"key": "originalBase64", "type": "bytearray"},
-        "thumbnail_base64": {"key": "thumbnailBase64", "type": "bytearray"},
-    }
-
-    def __init__(self, **kwargs):
-        super(AttachmentData, self).__init__(**kwargs)
-        self.type = kwargs.get("type", None)
-        self.name = kwargs.get("name", None)
-        self.original_base64 = kwargs.get("original_base64", None)
-        self.thumbnail_base64 = kwargs.get("thumbnail_base64", None)
-
-
-class AttachmentInfo(Model):
-    """Metadata for an attachment.
-
-    :param name: Name of the attachment
-    :type name: str
-    :param type: ContentType of the attachment
-    :type type: str
-    :param views: attachment views
-    :type views: list[~botframework.connector.models.AttachmentView]
-    """
-
-    _attribute_map = {
-        "name": {"key": "name", "type": "str"},
-        "type": {"key": "type", "type": "str"},
-        "views": {"key": "views", "type": "[AttachmentView]"},
-    }
-
-    def __init__(self, **kwargs):
-        super(AttachmentInfo, self).__init__(**kwargs)
-        self.name = kwargs.get("name", None)
-        self.type = kwargs.get("type", None)
-        self.views = kwargs.get("views", None)
-
-
-class AttachmentView(Model):
-    """Attachment View name and size.
-
-    :param view_id: Id of the attachment
-    :type view_id: str
-    :param size: Size of the attachment
-    :type size: int
-    """
-
-    _attribute_map = {
-        "view_id": {"key": "viewId", "type": "str"},
-        "size": {"key": "size", "type": "int"},
-    }
-
-    def __init__(self, **kwargs):
-        super(AttachmentView, self).__init__(**kwargs)
-        self.view_id = kwargs.get("view_id", None)
-        self.size = kwargs.get("size", None)
-
-
-class AudioCard(Model):
-    """Audio card.
-
-    :param title: Title of this card
-    :type title: str
-    :param subtitle: Subtitle of this card
-    :type subtitle: str
-    :param text: Text of this card
-    :type text: str
-    :param image: Thumbnail placeholder
-    :type image: ~botframework.connector.models.ThumbnailUrl
-    :param media: Media URLs for this card. When this field contains more than
-     one URL, each URL is an alternative format of the same content.
-    :type media: list[~botframework.connector.models.MediaUrl]
-    :param buttons: Actions on this card
-    :type buttons: list[~botframework.connector.models.CardAction]
-    :param shareable: This content may be shared with others (default:true)
-    :type shareable: bool
-    :param autoloop: Should the client loop playback at end of content
-     (default:true)
-    :type autoloop: bool
-    :param autostart: Should the client automatically start playback of media
-     in this card (default:true)
-    :type autostart: bool
-    :param aspect: Aspect ratio of thumbnail/media placeholder. Allowed values
-     are "16:9" and "4:3"
-    :type aspect: str
-    :param duration: Describes the length of the media content without
-     requiring a receiver to open the content. Formatted as an ISO 8601
-     Duration field.
-    :type duration: str
-    :param value: Supplementary parameter for this card
-    :type value: object
-    """
-
-    _attribute_map = {
-        "title": {"key": "title", "type": "str"},
-        "subtitle": {"key": "subtitle", "type": "str"},
-        "text": {"key": "text", "type": "str"},
-        "image": {"key": "image", "type": "ThumbnailUrl"},
-        "media": {"key": "media", "type": "[MediaUrl]"},
-        "buttons": {"key": "buttons", "type": "[CardAction]"},
-        "shareable": {"key": "shareable", "type": "bool"},
-        "autoloop": {"key": "autoloop", "type": "bool"},
-        "autostart": {"key": "autostart", "type": "bool"},
-        "aspect": {"key": "aspect", "type": "str"},
-        "duration": {"key": "duration", "type": "str"},
-        "value": {"key": "value", "type": "object"},
-    }
-
-    def __init__(self, **kwargs):
-        super(AudioCard, self).__init__(**kwargs)
-        self.title = kwargs.get("title", None)
-        self.subtitle = kwargs.get("subtitle", None)
-        self.text = kwargs.get("text", None)
-        self.image = kwargs.get("image", None)
-        self.media = kwargs.get("media", None)
-        self.buttons = kwargs.get("buttons", None)
-        self.shareable = kwargs.get("shareable", None)
-        self.autoloop = kwargs.get("autoloop", None)
-        self.autostart = kwargs.get("autostart", None)
-        self.aspect = kwargs.get("aspect", None)
-        self.duration = kwargs.get("duration", None)
-        self.value = kwargs.get("value", None)
-
-
-class BasicCard(Model):
-    """A basic card.
-
-    :param title: Title of the card
-    :type title: str
-    :param subtitle: Subtitle of the card
-    :type subtitle: str
-    :param text: Text for the card
-    :type text: str
-    :param images: Array of images for the card
-    :type images: list[~botframework.connector.models.CardImage]
-    :param buttons: Set of actions applicable to the current card
-    :type buttons: list[~botframework.connector.models.CardAction]
-    :param tap: This action will be activated when user taps on the card
-     itself
-    :type tap: ~botframework.connector.models.CardAction
-    """
-
-    _attribute_map = {
-        "title": {"key": "title", "type": "str"},
-        "subtitle": {"key": "subtitle", "type": "str"},
-        "text": {"key": "text", "type": "str"},
-        "images": {"key": "images", "type": "[CardImage]"},
-        "buttons": {"key": "buttons", "type": "[CardAction]"},
-        "tap": {"key": "tap", "type": "CardAction"},
-    }
-
-    def __init__(self, **kwargs):
-        super(BasicCard, self).__init__(**kwargs)
-        self.title = kwargs.get("title", None)
-        self.subtitle = kwargs.get("subtitle", None)
-        self.text = kwargs.get("text", None)
-        self.images = kwargs.get("images", None)
-        self.buttons = kwargs.get("buttons", None)
-        self.tap = kwargs.get("tap", None)
-
-
-class CardAction(Model):
-    """A clickable action.
-
-    :param type: The type of action implemented by this button. Possible
-     values include: 'openUrl', 'imBack', 'postBack', 'playAudio', 'playVideo',
-     'showImage', 'downloadFile', 'signin', 'call', 'payment', 'messageBack'
-    :type type: str or ~botframework.connector.models.ActionTypes
-    :param title: Text description which appears on the button
-    :type title: str
-    :param image: Image URL which will appear on the button, next to text
-     label
-    :type image: str
-    :param text: Text for this action
-    :type text: str
-    :param display_text: (Optional) text to display in the chat feed if the
-     button is clicked
-    :type display_text: str
-    :param value: Supplementary parameter for action. Content of this property
-     depends on the ActionType
-    :type value: object
-    :param channel_data: Channel-specific data associated with this action
-    :type channel_data: object
-    """
-
-    _attribute_map = {
-        "type": {"key": "type", "type": "str"},
-        "title": {"key": "title", "type": "str"},
-        "image": {"key": "image", "type": "str"},
-        "text": {"key": "text", "type": "str"},
-        "display_text": {"key": "displayText", "type": "str"},
-        "value": {"key": "value", "type": "object"},
-        "channel_data": {"key": "channelData", "type": "object"},
-    }
-
-    def __init__(self, **kwargs):
-        super(CardAction, self).__init__(**kwargs)
-        self.type = kwargs.get("type", None)
-        self.title = kwargs.get("title", None)
-        self.image = kwargs.get("image", None)
-        self.text = kwargs.get("text", None)
-        self.display_text = kwargs.get("display_text", None)
-        self.value = kwargs.get("value", None)
-        self.channel_data = kwargs.get("channel_data", None)
-
-
-class CardImage(Model):
-    """An image on a card.
-
-    :param url: URL thumbnail image for major content property
-    :type url: str
-    :param alt: Image description intended for screen readers
-    :type alt: str
-    :param tap: Action assigned to specific Attachment
-    :type tap: ~botframework.connector.models.CardAction
-    """
-
-    _attribute_map = {
-        "url": {"key": "url", "type": "str"},
-        "alt": {"key": "alt", "type": "str"},
-        "tap": {"key": "tap", "type": "CardAction"},
-    }
-
-    def __init__(self, **kwargs):
-        super(CardImage, self).__init__(**kwargs)
-        self.url = kwargs.get("url", None)
-        self.alt = kwargs.get("alt", None)
-        self.tap = kwargs.get("tap", None)
-
-
-class ChannelAccount(Model):
-    """Channel account information needed to route a message.
-
-    :param id: Channel id for the user or bot on this channel (Example:
-     joe@smith.com, or @joesmith or 123456)
-    :type id: str
-    :param name: Display friendly name
-    :type name: str
-    :param aad_object_id: This account's object ID within Azure Active
-     Directory (AAD)
-    :type aad_object_id: str
-    :param role: Role of the entity behind the account (Example: User, Bot,
-     etc.). Possible values include: 'user', 'bot'
-    :type role: str or ~botframework.connector.models.RoleTypes
-    """
-
-    _attribute_map = {
-        "id": {"key": "id", "type": "str"},
-        "name": {"key": "name", "type": "str"},
-        "aad_object_id": {"key": "aadObjectId", "type": "str"},
-        "role": {"key": "role", "type": "str"},
-    }
-
-    def __init__(self, **kwargs):
-        super(ChannelAccount, self).__init__(**kwargs)
-        self.id = kwargs.get("id", None)
-        self.name = kwargs.get("name", None)
-        self.aad_object_id = kwargs.get("aad_object_id", None)
-        self.role = kwargs.get("role", None)
-
-
-class ConversationAccount(Model):
-    """Conversation account represents the identity of the conversation within a channel.
-
-    :param is_group: Indicates whether the conversation contains more than two
-     participants at the time the activity was generated
-    :type is_group: bool
-    :param conversation_type: Indicates the type of the conversation in
-     channels that distinguish between conversation types
-    :type conversation_type: str
-    :param id: Channel id for the user or bot on this channel (Example:
-     joe@smith.com, or @joesmith or 123456)
-    :type id: str
-    :param name: Display friendly name
-    :type name: str
-    :param aad_object_id: This account's object ID within Azure Active
-     Directory (AAD)
-    :type aad_object_id: str
-    :param role: Role of the entity behind the account (Example: User, Bot,
-     etc.). Possible values include: 'user', 'bot'
-    :type role: str or ~botframework.connector.models.RoleTypes
-    :param tenant_id: This conversation's tenant ID
-    :type tenant_id: str
-    """
-
-    _attribute_map = {
-        "is_group": {"key": "isGroup", "type": "bool"},
-        "conversation_type": {"key": "conversationType", "type": "str"},
-        "id": {"key": "id", "type": "str"},
-        "name": {"key": "name", "type": "str"},
-        "aad_object_id": {"key": "aadObjectId", "type": "str"},
-        "role": {"key": "role", "type": "str"},
-        "tenant_id": {"key": "tenantID", "type": "str"},
-    }
-
-    def __init__(self, **kwargs):
-        super(ConversationAccount, self).__init__(**kwargs)
-        self.is_group = kwargs.get("is_group", None)
-        self.conversation_type = kwargs.get("conversation_type", None)
-        self.id = kwargs.get("id", None)
-        self.name = kwargs.get("name", None)
-        self.aad_object_id = kwargs.get("aad_object_id", None)
-        self.role = kwargs.get("role", None)
-        self.tenant_id = kwargs.get("tenant_id", None)
-
-
-class ConversationMembers(Model):
-    """Conversation and its members.
-
-    :param id: Conversation ID
-    :type id: str
-    :param members: List of members in this conversation
-    :type members: list[~botframework.connector.models.ChannelAccount]
-    """
-
-    _attribute_map = {
-        "id": {"key": "id", "type": "str"},
-        "members": {"key": "members", "type": "[ChannelAccount]"},
-    }
-
-    def __init__(self, **kwargs):
-        super(ConversationMembers, self).__init__(**kwargs)
-        self.id = kwargs.get("id", None)
-        self.members = kwargs.get("members", None)
-
-
-class ConversationParameters(Model):
-    """Parameters for creating a new conversation.
-
-    :param is_group: IsGroup
-    :type is_group: bool
-    :param bot: The bot address for this conversation
-    :type bot: ~botframework.connector.models.ChannelAccount
-    :param members: Members to add to the conversation
-    :type members: list[~botframework.connector.models.ChannelAccount]
-    :param topic_name: (Optional) Topic of the conversation (if supported by
-     the channel)
-    :type topic_name: str
-    :param activity: (Optional) When creating a new conversation, use this
-     activity as the initial message to the conversation
-    :type activity: ~botframework.connector.models.Activity
-    :param channel_data: Channel specific payload for creating the
-     conversation
-    :type channel_data: object
-    :param tenant_id: (Optional) The tenant ID in which the conversation should be created
-    :type tenant_id: str
-    """
-
-    _attribute_map = {
-        "is_group": {"key": "isGroup", "type": "bool"},
-        "bot": {"key": "bot", "type": "ChannelAccount"},
-        "members": {"key": "members", "type": "[ChannelAccount]"},
-        "topic_name": {"key": "topicName", "type": "str"},
-        "activity": {"key": "activity", "type": "Activity"},
-        "channel_data": {"key": "channelData", "type": "object"},
-        "tenant_id": {"key": "tenantID", "type": "str"},
-    }
-
-    def __init__(self, **kwargs):
-        super(ConversationParameters, self).__init__(**kwargs)
-        self.is_group = kwargs.get("is_group", None)
-        self.bot = kwargs.get("bot", None)
-        self.members = kwargs.get("members", None)
-        self.topic_name = kwargs.get("topic_name", None)
-        self.activity = kwargs.get("activity", None)
-        self.channel_data = kwargs.get("channel_data", None)
-        self.tenant_id = kwargs.get("tenant_id", None)
-
-
-class ConversationReference(Model):
-    """An object relating to a particular point in a conversation.
-
-    :param activity_id: (Optional) ID of the activity to refer to
-    :type activity_id: str
-    :param user: (Optional) User participating in this conversation
-    :type user: ~botframework.connector.models.ChannelAccount
-    :param bot: Bot participating in this conversation
-    :type bot: ~botframework.connector.models.ChannelAccount
-    :param conversation: Conversation reference
-    :type conversation: ~botframework.connector.models.ConversationAccount
-    :param channel_id: Channel ID
-    :type channel_id: str
-    :param service_url: Service endpoint where operations concerning the
-     referenced conversation may be performed
-    :type service_url: str
-    """
-
-    _attribute_map = {
-        "activity_id": {"key": "activityId", "type": "str"},
-        "user": {"key": "user", "type": "ChannelAccount"},
-        "bot": {"key": "bot", "type": "ChannelAccount"},
-        "conversation": {"key": "conversation", "type": "ConversationAccount"},
-        "channel_id": {"key": "channelId", "type": "str"},
-        "service_url": {"key": "serviceUrl", "type": "str"},
-    }
-
-    def __init__(self, **kwargs):
-        super(ConversationReference, self).__init__(**kwargs)
-        self.activity_id = kwargs.get("activity_id", None)
-        self.user = kwargs.get("user", None)
-        self.bot = kwargs.get("bot", None)
-        self.conversation = kwargs.get("conversation", None)
-        self.channel_id = kwargs.get("channel_id", None)
-        self.service_url = kwargs.get("service_url", None)
-
-
-class ConversationResourceResponse(Model):
-    """A response containing a resource.
-
-    :param activity_id: ID of the Activity (if sent)
-    :type activity_id: str
-    :param service_url: Service endpoint where operations concerning the
-     conversation may be performed
-    :type service_url: str
-    :param id: Id of the resource
-    :type id: str
-    """
-
-    _attribute_map = {
-        "activity_id": {"key": "activityId", "type": "str"},
-        "service_url": {"key": "serviceUrl", "type": "str"},
-        "id": {"key": "id", "type": "str"},
-    }
-
-    def __init__(self, **kwargs):
-        super(ConversationResourceResponse, self).__init__(**kwargs)
-        self.activity_id = kwargs.get("activity_id", None)
-        self.service_url = kwargs.get("service_url", None)
-        self.id = kwargs.get("id", None)
-
-
-class ConversationsResult(Model):
-    """Conversations result.
-
-    :param continuation_token: Paging token
-    :type continuation_token: str
-    :param conversations: List of conversations
-    :type conversations:
-     list[~botframework.connector.models.ConversationMembers]
-    """
-
-    _attribute_map = {
-        "continuation_token": {"key": "continuationToken", "type": "str"},
-        "conversations": {"key": "conversations", "type": "[ConversationMembers]"},
-    }
-
-    def __init__(self, **kwargs):
-        super(ConversationsResult, self).__init__(**kwargs)
-        self.continuation_token = kwargs.get("continuation_token", None)
-        self.conversations = kwargs.get("conversations", None)
-
-
-class Entity(Model):
-    """Metadata object pertaining to an activity.
-
-    :param type: Type of this entity (RFC 3987 IRI)
-    :type type: str
-    """
-
-    _attribute_map = {"type": {"key": "type", "type": "str"}}
-
-    def __init__(self, **kwargs):
-        super(Entity, self).__init__(**kwargs)
-        self.type = kwargs.get("type", None)
-
-
-class Error(Model):
-    """Object representing error information.
-
-    :param code: Error code
-    :type code: str
-    :param message: Error message
-    :type message: str
-    :param inner_http_error: Error from inner http call
-    :type inner_http_error: ~botframework.connector.models.InnerHttpError
-    """
-
-    _attribute_map = {
-        "code": {"key": "code", "type": "str"},
-        "message": {"key": "message", "type": "str"},
-        "inner_http_error": {"key": "innerHttpError", "type": "InnerHttpError"},
-    }
-
-    def __init__(self, **kwargs):
-        super(Error, self).__init__(**kwargs)
-        self.code = kwargs.get("code", None)
-        self.message = kwargs.get("message", None)
-        self.inner_http_error = kwargs.get("inner_http_error", None)
-
-
-class ErrorResponse(Model):
-    """An HTTP API response.
-
-    :param error: Error message
-    :type error: ~botframework.connector.models.Error
-    """
-
-    _attribute_map = {"error": {"key": "error", "type": "Error"}}
-
-    def __init__(self, **kwargs):
-        super(ErrorResponse, self).__init__(**kwargs)
-        self.error = kwargs.get("error", None)
-
-
-class ErrorResponseException(HttpOperationError):
-    """Server responsed with exception of type: 'ErrorResponse'.
-
-    :param deserialize: A deserializer
-    :param response: Server response to be deserialized.
-    """
-
-    def __init__(self, deserialize, response, *args):
-
-        super(ErrorResponseException, self).__init__(
-            deserialize, response, "ErrorResponse", *args
-        )
-
-
-class Fact(Model):
-    """Set of key-value pairs. Advantage of this section is that key and value
-    properties will be
-    rendered with default style information with some delimiter between them.
-    So there is no need for developer to specify style information.
-
-    :param key: The key for this Fact
-    :type key: str
-    :param value: The value for this Fact
-    :type value: str
-    """
-
-    _attribute_map = {
-        "key": {"key": "key", "type": "str"},
-        "value": {"key": "value", "type": "str"},
-    }
-
-    def __init__(self, **kwargs):
-        super(Fact, self).__init__(**kwargs)
-        self.key = kwargs.get("key", None)
-        self.value = kwargs.get("value", None)
-
-
-class GeoCoordinates(Model):
-    """GeoCoordinates (entity type: "https://schema.org/GeoCoordinates").
-
-    :param elevation: Elevation of the location [WGS
-     84](https://en.wikipedia.org/wiki/World_Geodetic_System)
-    :type elevation: float
-    :param latitude: Latitude of the location [WGS
-     84](https://en.wikipedia.org/wiki/World_Geodetic_System)
-    :type latitude: float
-    :param longitude: Longitude of the location [WGS
-     84](https://en.wikipedia.org/wiki/World_Geodetic_System)
-    :type longitude: float
-    :param type: The type of the thing
-    :type type: str
-    :param name: The name of the thing
-    :type name: str
-    """
-
-    _attribute_map = {
-        "elevation": {"key": "elevation", "type": "float"},
-        "latitude": {"key": "latitude", "type": "float"},
-        "longitude": {"key": "longitude", "type": "float"},
-        "type": {"key": "type", "type": "str"},
-        "name": {"key": "name", "type": "str"},
-    }
-
-    def __init__(self, **kwargs):
-        super(GeoCoordinates, self).__init__(**kwargs)
-        self.elevation = kwargs.get("elevation", None)
-        self.latitude = kwargs.get("latitude", None)
-        self.longitude = kwargs.get("longitude", None)
-        self.type = kwargs.get("type", None)
-        self.name = kwargs.get("name", None)
-
-
-class HeroCard(Model):
-    """A Hero card (card with a single, large image).
-
-    :param title: Title of the card
-    :type title: str
-    :param subtitle: Subtitle of the card
-    :type subtitle: str
-    :param text: Text for the card
-    :type text: str
-    :param images: Array of images for the card
-    :type images: list[~botframework.connector.models.CardImage]
-    :param buttons: Set of actions applicable to the current card
-    :type buttons: list[~botframework.connector.models.CardAction]
-    :param tap: This action will be activated when user taps on the card
-     itself
-    :type tap: ~botframework.connector.models.CardAction
-    """
-
-    _attribute_map = {
-        "title": {"key": "title", "type": "str"},
-        "subtitle": {"key": "subtitle", "type": "str"},
-        "text": {"key": "text", "type": "str"},
-        "images": {"key": "images", "type": "[CardImage]"},
-        "buttons": {"key": "buttons", "type": "[CardAction]"},
-        "tap": {"key": "tap", "type": "CardAction"},
-    }
-
-    def __init__(self, **kwargs):
-        super(HeroCard, self).__init__(**kwargs)
-        self.title = kwargs.get("title", None)
-        self.subtitle = kwargs.get("subtitle", None)
-        self.text = kwargs.get("text", None)
-        self.images = kwargs.get("images", None)
-        self.buttons = kwargs.get("buttons", None)
-        self.tap = kwargs.get("tap", None)
-
-
-class InnerHttpError(Model):
-    """Object representing inner http error.
-
-    :param status_code: HttpStatusCode from failed request
-    :type status_code: int
-    :param body: Body from failed request
-    :type body: object
-    """
-
-    _attribute_map = {
-        "status_code": {"key": "statusCode", "type": "int"},
-        "body": {"key": "body", "type": "object"},
-    }
-
-    def __init__(self, **kwargs):
-        super(InnerHttpError, self).__init__(**kwargs)
-        self.status_code = kwargs.get("status_code", None)
-        self.body = kwargs.get("body", None)
-
-
-class MediaCard(Model):
-    """Media card.
-
-    :param title: Title of this card
-    :type title: str
-    :param subtitle: Subtitle of this card
-    :type subtitle: str
-    :param text: Text of this card
-    :type text: str
-    :param image: Thumbnail placeholder
-    :type image: ~botframework.connector.models.ThumbnailUrl
-    :param media: Media URLs for this card. When this field contains more than
-     one URL, each URL is an alternative format of the same content.
-    :type media: list[~botframework.connector.models.MediaUrl]
-    :param buttons: Actions on this card
-    :type buttons: list[~botframework.connector.models.CardAction]
-    :param shareable: This content may be shared with others (default:true)
-    :type shareable: bool
-    :param autoloop: Should the client loop playback at end of content
-     (default:true)
-    :type autoloop: bool
-    :param autostart: Should the client automatically start playback of media
-     in this card (default:true)
-    :type autostart: bool
-    :param aspect: Aspect ratio of thumbnail/media placeholder. Allowed values
-     are "16:9" and "4:3"
-    :type aspect: str
-    :param duration: Describes the length of the media content without
-     requiring a receiver to open the content. Formatted as an ISO 8601
-     Duration field.
-    :type duration: str
-    :param value: Supplementary parameter for this card
-    :type value: object
-    """
-
-    _attribute_map = {
-        "title": {"key": "title", "type": "str"},
-        "subtitle": {"key": "subtitle", "type": "str"},
-        "text": {"key": "text", "type": "str"},
-        "image": {"key": "image", "type": "ThumbnailUrl"},
-        "media": {"key": "media", "type": "[MediaUrl]"},
-        "buttons": {"key": "buttons", "type": "[CardAction]"},
-        "shareable": {"key": "shareable", "type": "bool"},
-        "autoloop": {"key": "autoloop", "type": "bool"},
-        "autostart": {"key": "autostart", "type": "bool"},
-        "aspect": {"key": "aspect", "type": "str"},
-        "duration": {"key": "duration", "type": "str"},
-        "value": {"key": "value", "type": "object"},
-    }
-
-    def __init__(self, **kwargs):
-        super(MediaCard, self).__init__(**kwargs)
-        self.title = kwargs.get("title", None)
-        self.subtitle = kwargs.get("subtitle", None)
-        self.text = kwargs.get("text", None)
-        self.image = kwargs.get("image", None)
-        self.media = kwargs.get("media", None)
-        self.buttons = kwargs.get("buttons", None)
-        self.shareable = kwargs.get("shareable", None)
-        self.autoloop = kwargs.get("autoloop", None)
-        self.autostart = kwargs.get("autostart", None)
-        self.aspect = kwargs.get("aspect", None)
-        self.duration = kwargs.get("duration", None)
-        self.value = kwargs.get("value", None)
-
-
-class MediaEventValue(Model):
-    """Supplementary parameter for media events.
-
-    :param card_value: Callback parameter specified in the Value field of the
-     MediaCard that originated this event
-    :type card_value: object
-    """
-
-    _attribute_map = {"card_value": {"key": "cardValue", "type": "object"}}
-
-    def __init__(self, **kwargs):
-        super(MediaEventValue, self).__init__(**kwargs)
-        self.card_value = kwargs.get("card_value", None)
-
-
-class MediaUrl(Model):
-    """Media URL.
-
-    :param url: Url for the media
-    :type url: str
-    :param profile: Optional profile hint to the client to differentiate
-     multiple MediaUrl objects from each other
-    :type profile: str
-    """
-
-    _attribute_map = {
-        "url": {"key": "url", "type": "str"},
-        "profile": {"key": "profile", "type": "str"},
-    }
-
-    def __init__(self, **kwargs):
-        super(MediaUrl, self).__init__(**kwargs)
-        self.url = kwargs.get("url", None)
-        self.profile = kwargs.get("profile", None)
-
-
-class Mention(Model):
-    """Mention information (entity type: "mention").
-
-    :param mentioned: The mentioned user
-    :type mentioned: ~botframework.connector.models.ChannelAccount
-    :param text: Sub Text which represents the mention (can be null or empty)
-    :type text: str
-    :param type: Type of this entity (RFC 3987 IRI)
-    :type type: str
-    """
-
-    _attribute_map = {
-        "mentioned": {"key": "mentioned", "type": "ChannelAccount"},
-        "text": {"key": "text", "type": "str"},
-        "type": {"key": "type", "type": "str"},
-    }
-
-    def __init__(self, **kwargs):
-        super(Mention, self).__init__(**kwargs)
-        self.mentioned = kwargs.get("mentioned", None)
-        self.text = kwargs.get("text", None)
-        self.type = kwargs.get("type", None)
-
-
-class MessageReaction(Model):
-    """Message reaction object.
-
-    :param type: Message reaction type. Possible values include: 'like',
-     'plusOne'
-    :type type: str or ~botframework.connector.models.MessageReactionTypes
-    """
-
-    _attribute_map = {"type": {"key": "type", "type": "str"}}
-
-    def __init__(self, **kwargs):
-        super(MessageReaction, self).__init__(**kwargs)
-        self.type = kwargs.get("type", None)
-
-
-class MicrosoftPayMethodData(Model):
-    """W3C Payment Method Data for Microsoft Pay.
-
-    :param merchant_id: Microsoft Pay Merchant ID
-    :type merchant_id: str
-    :param supported_networks: Supported payment networks (e.g., "visa" and
-     "mastercard")
-    :type supported_networks: list[str]
-    :param supported_types: Supported payment types (e.g., "credit")
-    :type supported_types: list[str]
-    """
-
-    _attribute_map = {
-        "merchant_id": {"key": "merchantId", "type": "str"},
-        "supported_networks": {"key": "supportedNetworks", "type": "[str]"},
-        "supported_types": {"key": "supportedTypes", "type": "[str]"},
-    }
-
-    def __init__(self, **kwargs):
-        super(MicrosoftPayMethodData, self).__init__(**kwargs)
-        self.merchant_id = kwargs.get("merchant_id", None)
-        self.supported_networks = kwargs.get("supported_networks", None)
-        self.supported_types = kwargs.get("supported_types", None)
-
-
-class OAuthCard(Model):
-    """A card representing a request to perform a sign in via OAuth.
-
-    :param text: Text for signin request
-    :type text: str
-    :param connection_name: The name of the registered connection
-    :type connection_name: str
-    :param buttons: Action to use to perform signin
-    :type buttons: list[~botframework.connector.models.CardAction]
-    """
-
-    _attribute_map = {
-        "text": {"key": "text", "type": "str"},
-        "connection_name": {"key": "connectionName", "type": "str"},
-        "buttons": {"key": "buttons", "type": "[CardAction]"},
-    }
-
-    def __init__(self, **kwargs):
-        super(OAuthCard, self).__init__(**kwargs)
-        self.text = kwargs.get("text", None)
-        self.connection_name = kwargs.get("connection_name", None)
-        self.buttons = kwargs.get("buttons", None)
-
-
-class PagedMembersResult(Model):
-    """Page of members.
-
-    :param continuation_token: Paging token
-    :type continuation_token: str
-    :param members: The Channel Accounts.
-    :type members: list[~botframework.connector.models.ChannelAccount]
-    """
-
-    _attribute_map = {
-        "continuation_token": {"key": "continuationToken", "type": "str"},
-        "members": {"key": "members", "type": "[ChannelAccount]"},
-    }
-
-    def __init__(self, **kwargs):
-        super(PagedMembersResult, self).__init__(**kwargs)
-        self.continuation_token = kwargs.get("continuation_token", None)
-        self.members = kwargs.get("members", None)
-
-
-class PaymentAddress(Model):
-    """Address within a Payment Request.
-
-    :param country: This is the CLDR (Common Locale Data Repository) region
-     code. For example, US, GB, CN, or JP
-    :type country: str
-    :param address_line: This is the most specific part of the address. It can
-     include, for example, a street name, a house number, apartment number, a
-     rural delivery route, descriptive instructions, or a post office box
-     number.
-    :type address_line: list[str]
-    :param region: This is the top level administrative subdivision of the
-     country. For example, this can be a state, a province, an oblast, or a
-     prefecture.
-    :type region: str
-    :param city: This is the city/town portion of the address.
-    :type city: str
-    :param dependent_locality: This is the dependent locality or sublocality
-     within a city. For example, used for neighborhoods, boroughs, districts,
-     or UK dependent localities.
-    :type dependent_locality: str
-    :param postal_code: This is the postal code or ZIP code, also known as PIN
-     code in India.
-    :type postal_code: str
-    :param sorting_code: This is the sorting code as used in, for example,
-     France.
-    :type sorting_code: str
-    :param language_code: This is the BCP-47 language code for the address.
-     It's used to determine the field separators and the order of fields when
-     formatting the address for display.
-    :type language_code: str
-    :param organization: This is the organization, firm, company, or
-     institution at this address.
-    :type organization: str
-    :param recipient: This is the name of the recipient or contact person.
-    :type recipient: str
-    :param phone: This is the phone number of the recipient or contact person.
-    :type phone: str
-    """
-
-    _attribute_map = {
-        "country": {"key": "country", "type": "str"},
-        "address_line": {"key": "addressLine", "type": "[str]"},
-        "region": {"key": "region", "type": "str"},
-        "city": {"key": "city", "type": "str"},
-        "dependent_locality": {"key": "dependentLocality", "type": "str"},
-        "postal_code": {"key": "postalCode", "type": "str"},
-        "sorting_code": {"key": "sortingCode", "type": "str"},
-        "language_code": {"key": "languageCode", "type": "str"},
-        "organization": {"key": "organization", "type": "str"},
-        "recipient": {"key": "recipient", "type": "str"},
-        "phone": {"key": "phone", "type": "str"},
-    }
-
-    def __init__(self, **kwargs):
-        super(PaymentAddress, self).__init__(**kwargs)
-        self.country = kwargs.get("country", None)
-        self.address_line = kwargs.get("address_line", None)
-        self.region = kwargs.get("region", None)
-        self.city = kwargs.get("city", None)
-        self.dependent_locality = kwargs.get("dependent_locality", None)
-        self.postal_code = kwargs.get("postal_code", None)
-        self.sorting_code = kwargs.get("sorting_code", None)
-        self.language_code = kwargs.get("language_code", None)
-        self.organization = kwargs.get("organization", None)
-        self.recipient = kwargs.get("recipient", None)
-        self.phone = kwargs.get("phone", None)
-
-
-class PaymentCurrencyAmount(Model):
-    """Supplies monetary amounts.
-
-    :param currency: A currency identifier
-    :type currency: str
-    :param value: Decimal monetary value
-    :type value: str
-    :param currency_system: Currency system
-    :type currency_system: str
-    """
-
-    _attribute_map = {
-        "currency": {"key": "currency", "type": "str"},
-        "value": {"key": "value", "type": "str"},
-        "currency_system": {"key": "currencySystem", "type": "str"},
-    }
-
-    def __init__(self, **kwargs):
-        super(PaymentCurrencyAmount, self).__init__(**kwargs)
-        self.currency = kwargs.get("currency", None)
-        self.value = kwargs.get("value", None)
-        self.currency_system = kwargs.get("currency_system", None)
-
-
-class PaymentDetails(Model):
-    """Provides information about the requested transaction.
-
-    :param total: Contains the total amount of the payment request
-    :type total: ~botframework.connector.models.PaymentItem
-    :param display_items: Contains line items for the payment request that the
-     user agent may display
-    :type display_items: list[~botframework.connector.models.PaymentItem]
-    :param shipping_options: A sequence containing the different shipping
-     options for the user to choose from
-    :type shipping_options:
-     list[~botframework.connector.models.PaymentShippingOption]
-    :param modifiers: Contains modifiers for particular payment method
-     identifiers
-    :type modifiers:
-     list[~botframework.connector.models.PaymentDetailsModifier]
-    :param error: Error description
-    :type error: str
-    """
-
-    _attribute_map = {
-        "total": {"key": "total", "type": "PaymentItem"},
-        "display_items": {"key": "displayItems", "type": "[PaymentItem]"},
-        "shipping_options": {
-            "key": "shippingOptions",
-            "type": "[PaymentShippingOption]",
-        },
-        "modifiers": {"key": "modifiers", "type": "[PaymentDetailsModifier]"},
-        "error": {"key": "error", "type": "str"},
-    }
-
-    def __init__(self, **kwargs):
-        super(PaymentDetails, self).__init__(**kwargs)
-        self.total = kwargs.get("total", None)
-        self.display_items = kwargs.get("display_items", None)
-        self.shipping_options = kwargs.get("shipping_options", None)
-        self.modifiers = kwargs.get("modifiers", None)
-        self.error = kwargs.get("error", None)
-
-
-class PaymentDetailsModifier(Model):
-    """Provides details that modify the PaymentDetails based on payment method
-    identifier.
-
-    :param supported_methods: Contains a sequence of payment method
-     identifiers
-    :type supported_methods: list[str]
-    :param total: This value overrides the total field in the PaymentDetails
-     dictionary for the payment method identifiers in the supportedMethods
-     field
-    :type total: ~botframework.connector.models.PaymentItem
-    :param additional_display_items: Provides additional display items that
-     are appended to the displayItems field in the PaymentDetails dictionary
-     for the payment method identifiers in the supportedMethods field
-    :type additional_display_items:
-     list[~botframework.connector.models.PaymentItem]
-    :param data: A JSON-serializable object that provides optional information
-     that might be needed by the supported payment methods
-    :type data: object
-    """
-
-    _attribute_map = {
-        "supported_methods": {"key": "supportedMethods", "type": "[str]"},
-        "total": {"key": "total", "type": "PaymentItem"},
-        "additional_display_items": {
-            "key": "additionalDisplayItems",
-            "type": "[PaymentItem]",
-        },
-        "data": {"key": "data", "type": "object"},
-    }
-
-    def __init__(self, **kwargs):
-        super(PaymentDetailsModifier, self).__init__(**kwargs)
-        self.supported_methods = kwargs.get("supported_methods", None)
-        self.total = kwargs.get("total", None)
-        self.additional_display_items = kwargs.get("additional_display_items", None)
-        self.data = kwargs.get("data", None)
-
-
-class PaymentItem(Model):
-    """Indicates what the payment request is for and the value asked for.
-
-    :param label: Human-readable description of the item
-    :type label: str
-    :param amount: Monetary amount for the item
-    :type amount: ~botframework.connector.models.PaymentCurrencyAmount
-    :param pending: When set to true this flag means that the amount field is
-     not final.
-    :type pending: bool
-    """
-
-    _attribute_map = {
-        "label": {"key": "label", "type": "str"},
-        "amount": {"key": "amount", "type": "PaymentCurrencyAmount"},
-        "pending": {"key": "pending", "type": "bool"},
-    }
-
-    def __init__(self, **kwargs):
-        super(PaymentItem, self).__init__(**kwargs)
-        self.label = kwargs.get("label", None)
-        self.amount = kwargs.get("amount", None)
-        self.pending = kwargs.get("pending", None)
-
-
-class PaymentMethodData(Model):
-    """Indicates a set of supported payment methods and any associated payment
-    method specific data for those methods.
-
-    :param supported_methods: Required sequence of strings containing payment
-     method identifiers for payment methods that the merchant web site accepts
-    :type supported_methods: list[str]
-    :param data: A JSON-serializable object that provides optional information
-     that might be needed by the supported payment methods
-    :type data: object
-    """
-
-    _attribute_map = {
-        "supported_methods": {"key": "supportedMethods", "type": "[str]"},
-        "data": {"key": "data", "type": "object"},
-    }
-
-    def __init__(self, **kwargs):
-        super(PaymentMethodData, self).__init__(**kwargs)
-        self.supported_methods = kwargs.get("supported_methods", None)
-        self.data = kwargs.get("data", None)
-
-
-class PaymentOptions(Model):
-    """Provides information about the options desired for the payment request.
-
-    :param request_payer_name: Indicates whether the user agent should collect
-     and return the payer's name as part of the payment request
-    :type request_payer_name: bool
-    :param request_payer_email: Indicates whether the user agent should
-     collect and return the payer's email address as part of the payment
-     request
-    :type request_payer_email: bool
-    :param request_payer_phone: Indicates whether the user agent should
-     collect and return the payer's phone number as part of the payment request
-    :type request_payer_phone: bool
-    :param request_shipping: Indicates whether the user agent should collect
-     and return a shipping address as part of the payment request
-    :type request_shipping: bool
-    :param shipping_type: If requestShipping is set to true, then the
-     shippingType field may be used to influence the way the user agent
-     presents the user interface for gathering the shipping address
-    :type shipping_type: str
-    """
-
-    _attribute_map = {
-        "request_payer_name": {"key": "requestPayerName", "type": "bool"},
-        "request_payer_email": {"key": "requestPayerEmail", "type": "bool"},
-        "request_payer_phone": {"key": "requestPayerPhone", "type": "bool"},
-        "request_shipping": {"key": "requestShipping", "type": "bool"},
-        "shipping_type": {"key": "shippingType", "type": "str"},
-    }
-
-    def __init__(self, **kwargs):
-        super(PaymentOptions, self).__init__(**kwargs)
-        self.request_payer_name = kwargs.get("request_payer_name", None)
-        self.request_payer_email = kwargs.get("request_payer_email", None)
-        self.request_payer_phone = kwargs.get("request_payer_phone", None)
-        self.request_shipping = kwargs.get("request_shipping", None)
-        self.shipping_type = kwargs.get("shipping_type", None)
-
-
-class PaymentRequest(Model):
-    """A request to make a payment.
-
-    :param id: ID of this payment request
-    :type id: str
-    :param method_data: Allowed payment methods for this request
-    :type method_data: list[~botframework.connector.models.PaymentMethodData]
-    :param details: Details for this request
-    :type details: ~botframework.connector.models.PaymentDetails
-    :param options: Provides information about the options desired for the
-     payment request
-    :type options: ~botframework.connector.models.PaymentOptions
-    :param expires: Expiration for this request, in ISO 8601 duration format
-     (e.g., 'P1D')
-    :type expires: str
-    """
-
-    _attribute_map = {
-        "id": {"key": "id", "type": "str"},
-        "method_data": {"key": "methodData", "type": "[PaymentMethodData]"},
-        "details": {"key": "details", "type": "PaymentDetails"},
-        "options": {"key": "options", "type": "PaymentOptions"},
-        "expires": {"key": "expires", "type": "str"},
-    }
-
-    def __init__(self, **kwargs):
-        super(PaymentRequest, self).__init__(**kwargs)
-        self.id = kwargs.get("id", None)
-        self.method_data = kwargs.get("method_data", None)
-        self.details = kwargs.get("details", None)
-        self.options = kwargs.get("options", None)
-        self.expires = kwargs.get("expires", None)
-
-
-class PaymentRequestComplete(Model):
-    """Payload delivered when completing a payment request.
-
-    :param id: Payment request ID
-    :type id: str
-    :param payment_request: Initial payment request
-    :type payment_request: ~botframework.connector.models.PaymentRequest
-    :param payment_response: Corresponding payment response
-    :type payment_response: ~botframework.connector.models.PaymentResponse
-    """
-
-    _attribute_map = {
-        "id": {"key": "id", "type": "str"},
-        "payment_request": {"key": "paymentRequest", "type": "PaymentRequest"},
-        "payment_response": {"key": "paymentResponse", "type": "PaymentResponse"},
-    }
-
-    def __init__(self, **kwargs):
-        super(PaymentRequestComplete, self).__init__(**kwargs)
-        self.id = kwargs.get("id", None)
-        self.payment_request = kwargs.get("payment_request", None)
-        self.payment_response = kwargs.get("payment_response", None)
-
-
-class PaymentRequestCompleteResult(Model):
-    """Result from a completed payment request.
-
-    :param result: Result of the payment request completion
-    :type result: str
-    """
-
-    _attribute_map = {"result": {"key": "result", "type": "str"}}
-
-    def __init__(self, **kwargs):
-        super(PaymentRequestCompleteResult, self).__init__(**kwargs)
-        self.result = kwargs.get("result", None)
-
-
-class PaymentRequestUpdate(Model):
-    """An update to a payment request.
-
-    :param id: ID for the payment request to update
-    :type id: str
-    :param details: Update payment details
-    :type details: ~botframework.connector.models.PaymentDetails
-    :param shipping_address: Updated shipping address
-    :type shipping_address: ~botframework.connector.models.PaymentAddress
-    :param shipping_option: Updated shipping options
-    :type shipping_option: str
-    """
-
-    _attribute_map = {
-        "id": {"key": "id", "type": "str"},
-        "details": {"key": "details", "type": "PaymentDetails"},
-        "shipping_address": {"key": "shippingAddress", "type": "PaymentAddress"},
-        "shipping_option": {"key": "shippingOption", "type": "str"},
-    }
-
-    def __init__(self, **kwargs):
-        super(PaymentRequestUpdate, self).__init__(**kwargs)
-        self.id = kwargs.get("id", None)
-        self.details = kwargs.get("details", None)
-        self.shipping_address = kwargs.get("shipping_address", None)
-        self.shipping_option = kwargs.get("shipping_option", None)
-
-
-class PaymentRequestUpdateResult(Model):
-    """A result object from a Payment Request Update invoke operation.
-
-    :param details: Update payment details
-    :type details: ~botframework.connector.models.PaymentDetails
-    """
-
-    _attribute_map = {"details": {"key": "details", "type": "PaymentDetails"}}
-
-    def __init__(self, **kwargs):
-        super(PaymentRequestUpdateResult, self).__init__(**kwargs)
-        self.details = kwargs.get("details", None)
-
-
-class PaymentResponse(Model):
-    """A PaymentResponse is returned when a user has selected a payment method and
-    approved a payment request.
-
-    :param method_name: The payment method identifier for the payment method
-     that the user selected to fulfil the transaction
-    :type method_name: str
-    :param details: A JSON-serializable object that provides a payment method
-     specific message used by the merchant to process the transaction and
-     determine successful fund transfer
-    :type details: object
-    :param shipping_address: If the requestShipping flag was set to true in
-     the PaymentOptions passed to the PaymentRequest constructor, then
-     shippingAddress will be the full and final shipping address chosen by the
-     user
-    :type shipping_address: ~botframework.connector.models.PaymentAddress
-    :param shipping_option: If the requestShipping flag was set to true in the
-     PaymentOptions passed to the PaymentRequest constructor, then
-     shippingOption will be the id attribute of the selected shipping option
-    :type shipping_option: str
-    :param payer_email: If the requestPayerEmail flag was set to true in the
-     PaymentOptions passed to the PaymentRequest constructor, then payerEmail
-     will be the email address chosen by the user
-    :type payer_email: str
-    :param payer_phone: If the requestPayerPhone flag was set to true in the
-     PaymentOptions passed to the PaymentRequest constructor, then payerPhone
-     will be the phone number chosen by the user
-    :type payer_phone: str
-    """
-
-    _attribute_map = {
-        "method_name": {"key": "methodName", "type": "str"},
-        "details": {"key": "details", "type": "object"},
-        "shipping_address": {"key": "shippingAddress", "type": "PaymentAddress"},
-        "shipping_option": {"key": "shippingOption", "type": "str"},
-        "payer_email": {"key": "payerEmail", "type": "str"},
-        "payer_phone": {"key": "payerPhone", "type": "str"},
-    }
-
-    def __init__(self, **kwargs):
-        super(PaymentResponse, self).__init__(**kwargs)
-        self.method_name = kwargs.get("method_name", None)
-        self.details = kwargs.get("details", None)
-        self.shipping_address = kwargs.get("shipping_address", None)
-        self.shipping_option = kwargs.get("shipping_option", None)
-        self.payer_email = kwargs.get("payer_email", None)
-        self.payer_phone = kwargs.get("payer_phone", None)
-
-
-class PaymentShippingOption(Model):
-    """Describes a shipping option.
-
-    :param id: String identifier used to reference this PaymentShippingOption
-    :type id: str
-    :param label: Human-readable description of the item
-    :type label: str
-    :param amount: Contains the monetary amount for the item
-    :type amount: ~botframework.connector.models.PaymentCurrencyAmount
-    :param selected: Indicates whether this is the default selected
-     PaymentShippingOption
-    :type selected: bool
-    """
-
-    _attribute_map = {
-        "id": {"key": "id", "type": "str"},
-        "label": {"key": "label", "type": "str"},
-        "amount": {"key": "amount", "type": "PaymentCurrencyAmount"},
-        "selected": {"key": "selected", "type": "bool"},
-    }
-
-    def __init__(self, **kwargs):
-        super(PaymentShippingOption, self).__init__(**kwargs)
-        self.id = kwargs.get("id", None)
-        self.label = kwargs.get("label", None)
-        self.amount = kwargs.get("amount", None)
-        self.selected = kwargs.get("selected", None)
-
-
-class Place(Model):
-    """Place (entity type: "https://schema.org/Place").
-
-    :param address: Address of the place (may be `string` or complex object of
-     type `PostalAddress`)
-    :type address: object
-    :param geo: Geo coordinates of the place (may be complex object of type
-     `GeoCoordinates` or `GeoShape`)
-    :type geo: object
-    :param has_map: Map to the place (may be `string` (URL) or complex object
-     of type `Map`)
-    :type has_map: object
-    :param type: The type of the thing
-    :type type: str
-    :param name: The name of the thing
-    :type name: str
-    """
-
-    _attribute_map = {
-        "address": {"key": "address", "type": "object"},
-        "geo": {"key": "geo", "type": "object"},
-        "has_map": {"key": "hasMap", "type": "object"},
-        "type": {"key": "type", "type": "str"},
-        "name": {"key": "name", "type": "str"},
-    }
-
-    def __init__(self, **kwargs):
-        super(Place, self).__init__(**kwargs)
-        self.address = kwargs.get("address", None)
-        self.geo = kwargs.get("geo", None)
-        self.has_map = kwargs.get("has_map", None)
-        self.type = kwargs.get("type", None)
-        self.name = kwargs.get("name", None)
-
-
-class ReceiptCard(Model):
-    """A receipt card.
-
-    :param title: Title of the card
-    :type title: str
-    :param facts: Array of Fact objects
-    :type facts: list[~botframework.connector.models.Fact]
-    :param items: Array of Receipt Items
-    :type items: list[~botframework.connector.models.ReceiptItem]
-    :param tap: This action will be activated when user taps on the card
-    :type tap: ~botframework.connector.models.CardAction
-    :param total: Total amount of money paid (or to be paid)
-    :type total: str
-    :param tax: Total amount of tax paid (or to be paid)
-    :type tax: str
-    :param vat: Total amount of VAT paid (or to be paid)
-    :type vat: str
-    :param buttons: Set of actions applicable to the current card
-    :type buttons: list[~botframework.connector.models.CardAction]
-    """
-
-    _attribute_map = {
-        "title": {"key": "title", "type": "str"},
-        "facts": {"key": "facts", "type": "[Fact]"},
-        "items": {"key": "items", "type": "[ReceiptItem]"},
-        "tap": {"key": "tap", "type": "CardAction"},
-        "total": {"key": "total", "type": "str"},
-        "tax": {"key": "tax", "type": "str"},
-        "vat": {"key": "vat", "type": "str"},
-        "buttons": {"key": "buttons", "type": "[CardAction]"},
-    }
-
-    def __init__(self, **kwargs):
-        super(ReceiptCard, self).__init__(**kwargs)
-        self.title = kwargs.get("title", None)
-        self.facts = kwargs.get("facts", None)
-        self.items = kwargs.get("items", None)
-        self.tap = kwargs.get("tap", None)
-        self.total = kwargs.get("total", None)
-        self.tax = kwargs.get("tax", None)
-        self.vat = kwargs.get("vat", None)
-        self.buttons = kwargs.get("buttons", None)
-
-
-class ReceiptItem(Model):
-    """An item on a receipt card.
-
-    :param title: Title of the Card
-    :type title: str
-    :param subtitle: Subtitle appears just below Title field, differs from
-     Title in font styling only
-    :type subtitle: str
-    :param text: Text field appears just below subtitle, differs from Subtitle
-     in font styling only
-    :type text: str
-    :param image: Image
-    :type image: ~botframework.connector.models.CardImage
-    :param price: Amount with currency
-    :type price: str
-    :param quantity: Number of items of given kind
-    :type quantity: str
-    :param tap: This action will be activated when user taps on the Item
-     bubble.
-    :type tap: ~botframework.connector.models.CardAction
-    """
-
-    _attribute_map = {
-        "title": {"key": "title", "type": "str"},
-        "subtitle": {"key": "subtitle", "type": "str"},
-        "text": {"key": "text", "type": "str"},
-        "image": {"key": "image", "type": "CardImage"},
-        "price": {"key": "price", "type": "str"},
-        "quantity": {"key": "quantity", "type": "str"},
-        "tap": {"key": "tap", "type": "CardAction"},
-    }
-
-    def __init__(self, **kwargs):
-        super(ReceiptItem, self).__init__(**kwargs)
-        self.title = kwargs.get("title", None)
-        self.subtitle = kwargs.get("subtitle", None)
-        self.text = kwargs.get("text", None)
-        self.image = kwargs.get("image", None)
-        self.price = kwargs.get("price", None)
-        self.quantity = kwargs.get("quantity", None)
-        self.tap = kwargs.get("tap", None)
-
-
-class ResourceResponse(Model):
-    """A response containing a resource ID.
-
-    :param id: Id of the resource
-    :type id: str
-    """
-
-    _attribute_map = {"id": {"key": "id", "type": "str"}}
-
-    def __init__(self, **kwargs):
-        super(ResourceResponse, self).__init__(**kwargs)
-        self.id = kwargs.get("id", None)
-
-
-class SemanticAction(Model):
-    """Represents a reference to a programmatic action.
-
-    :param id: ID of this action
-    :type id: str
-    :param entities: Entities associated with this action
-    :type entities: dict[str, ~botframework.connector.models.Entity]
-    :param state: State of this action. Allowed values: `start`, `continue`, `done`
-    :type state: str or ~botframework.connector.models.SemanticActionStates
-    """
-
-    _attribute_map = {
-        "id": {"key": "id", "type": "str"},
-        "entities": {"key": "entities", "type": "{Entity}"},
-        "state": {"key": "state", "type": "str"},
-    }
-
-    def __init__(self, **kwargs):
-        super(SemanticAction, self).__init__(**kwargs)
-        self.id = kwargs.get("id", None)
-        self.entities = kwargs.get("entities", None)
-        self.state = kwargs.get("state", None)
-
-
-class SigninCard(Model):
-    """A card representing a request to sign in.
-
-    :param text: Text for signin request
-    :type text: str
-    :param buttons: Action to use to perform signin
-    :type buttons: list[~botframework.connector.models.CardAction]
-    """
-
-    _attribute_map = {
-        "text": {"key": "text", "type": "str"},
-        "buttons": {"key": "buttons", "type": "[CardAction]"},
-    }
-
-    def __init__(self, **kwargs):
-        super(SigninCard, self).__init__(**kwargs)
-        self.text = kwargs.get("text", None)
-        self.buttons = kwargs.get("buttons", None)
-
-
-class SuggestedActions(Model):
-    """SuggestedActions that can be performed.
-
-    :param to: Ids of the recipients that the actions should be shown to.
-     These Ids are relative to the channelId and a subset of all recipients of
-     the activity
-    :type to: list[str]
-    :param actions: Actions that can be shown to the user
-    :type actions: list[~botframework.connector.models.CardAction]
-    """
-
-    _attribute_map = {
-        "to": {"key": "to", "type": "[str]"},
-        "actions": {"key": "actions", "type": "[CardAction]"},
-    }
-
-    def __init__(self, **kwargs):
-        super(SuggestedActions, self).__init__(**kwargs)
-        self.to = kwargs.get("to", None)
-        self.actions = kwargs.get("actions", None)
-
-
-class TextHighlight(Model):
-    """Refers to a substring of content within another field.
-
-    :param text: Defines the snippet of text to highlight
-    :type text: str
-    :param occurrence: Occurrence of the text field within the referenced
-     text, if multiple exist.
-    :type occurrence: int
-    """
-
-    _attribute_map = {
-        "text": {"key": "text", "type": "str"},
-        "occurrence": {"key": "occurrence", "type": "int"},
-    }
-
-    def __init__(self, **kwargs):
-        super(TextHighlight, self).__init__(**kwargs)
-        self.text = kwargs.get("text", None)
-        self.occurrence = kwargs.get("occurrence", None)
-
-
-class Thing(Model):
-    """Thing (entity type: "https://schema.org/Thing").
-
-    :param type: The type of the thing
-    :type type: str
-    :param name: The name of the thing
-    :type name: str
-    """
-
-    _attribute_map = {
-        "type": {"key": "type", "type": "str"},
-        "name": {"key": "name", "type": "str"},
-    }
-
-    def __init__(self, **kwargs):
-        super(Thing, self).__init__(**kwargs)
-        self.type = kwargs.get("type", None)
-        self.name = kwargs.get("name", None)
-
-
-class ThumbnailCard(Model):
-    """A thumbnail card (card with a single, small thumbnail image).
-
-    :param title: Title of the card
-    :type title: str
-    :param subtitle: Subtitle of the card
-    :type subtitle: str
-    :param text: Text for the card
-    :type text: str
-    :param images: Array of images for the card
-    :type images: list[~botframework.connector.models.CardImage]
-    :param buttons: Set of actions applicable to the current card
-    :type buttons: list[~botframework.connector.models.CardAction]
-    :param tap: This action will be activated when user taps on the card
-     itself
-    :type tap: ~botframework.connector.models.CardAction
-    """
-
-    _attribute_map = {
-        "title": {"key": "title", "type": "str"},
-        "subtitle": {"key": "subtitle", "type": "str"},
-        "text": {"key": "text", "type": "str"},
-        "images": {"key": "images", "type": "[CardImage]"},
-        "buttons": {"key": "buttons", "type": "[CardAction]"},
-        "tap": {"key": "tap", "type": "CardAction"},
-    }
-
-    def __init__(self, **kwargs):
-        super(ThumbnailCard, self).__init__(**kwargs)
-        self.title = kwargs.get("title", None)
-        self.subtitle = kwargs.get("subtitle", None)
-        self.text = kwargs.get("text", None)
-        self.images = kwargs.get("images", None)
-        self.buttons = kwargs.get("buttons", None)
-        self.tap = kwargs.get("tap", None)
-
-
-class ThumbnailUrl(Model):
-    """Thumbnail URL.
-
-    :param url: URL pointing to the thumbnail to use for media content
-    :type url: str
-    :param alt: HTML alt text to include on this thumbnail image
-    :type alt: str
-    """
-
-    _attribute_map = {
-        "url": {"key": "url", "type": "str"},
-        "alt": {"key": "alt", "type": "str"},
-    }
-
-    def __init__(self, **kwargs):
-        super(ThumbnailUrl, self).__init__(**kwargs)
-        self.url = kwargs.get("url", None)
-        self.alt = kwargs.get("alt", None)
-
-
-class TokenRequest(Model):
-    """A request to receive a user token.
-
-    :param provider: The provider to request a user token from
-    :type provider: str
-    :param settings: A collection of settings for the specific provider for
-     this request
-    :type settings: dict[str, object]
-    """
-
-    _attribute_map = {
-        "provider": {"key": "provider", "type": "str"},
-        "settings": {"key": "settings", "type": "{object}"},
-    }
-
-    def __init__(self, **kwargs):
-        super(TokenRequest, self).__init__(**kwargs)
-        self.provider = kwargs.get("provider", None)
-        self.settings = kwargs.get("settings", None)
-
-
-class TokenResponse(Model):
-    """A response that includes a user token.
-
-    :param connection_name: The connection name
-    :type connection_name: str
-    :param token: The user token
-    :type token: str
-    :param expiration: Expiration for the token, in ISO 8601 format (e.g.
-     "2007-04-05T14:30Z")
-    :type expiration: str
-    :param channel_id: The channelId of the TokenResponse
-    :type channel_id: str
-    """
-
-    _attribute_map = {
-        "connection_name": {"key": "connectionName", "type": "str"},
-        "token": {"key": "token", "type": "str"},
-        "expiration": {"key": "expiration", "type": "str"},
-        "channel_id": {"key": "channelId", "type": "str"},
-    }
-
-    def __init__(self, **kwargs):
-        super(TokenResponse, self).__init__(**kwargs)
-        self.connection_name = kwargs.get("connection_name", None)
-        self.token = kwargs.get("token", None)
-        self.expiration = kwargs.get("expiration", None)
-        self.channel_id = kwargs.get("channel_id", None)
-
-
-class Transcript(Model):
-    """Transcript.
-
-    :param activities: A collection of Activities that conforms to the
-     Transcript schema.
-    :type activities: list[~botframework.connector.models.Activity]
-    """
-
-    _attribute_map = {"activities": {"key": "activities", "type": "[Activity]"}}
-
-    def __init__(self, **kwargs):
-        super(Transcript, self).__init__(**kwargs)
-        self.activities = kwargs.get("activities", None)
-
-
-class VideoCard(Model):
-    """Video card.
-
-    :param title: Title of this card
-    :type title: str
-    :param subtitle: Subtitle of this card
-    :type subtitle: str
-    :param text: Text of this card
-    :type text: str
-    :param image: Thumbnail placeholder
-    :type image: ~botframework.connector.models.ThumbnailUrl
-    :param media: Media URLs for this card. When this field contains more than
-     one URL, each URL is an alternative format of the same content.
-    :type media: list[~botframework.connector.models.MediaUrl]
-    :param buttons: Actions on this card
-    :type buttons: list[~botframework.connector.models.CardAction]
-    :param shareable: This content may be shared with others (default:true)
-    :type shareable: bool
-    :param autoloop: Should the client loop playback at end of content
-     (default:true)
-    :type autoloop: bool
-    :param autostart: Should the client automatically start playback of media
-     in this card (default:true)
-    :type autostart: bool
-    :param aspect: Aspect ratio of thumbnail/media placeholder. Allowed values
-     are "16:9" and "4:3"
-    :type aspect: str
-    :param duration: Describes the length of the media content without
-     requiring a receiver to open the content. Formatted as an ISO 8601
-     Duration field.
-    :type duration: str
-    :param value: Supplementary parameter for this card
-    :type value: object
-    """
-
-    _attribute_map = {
-        "title": {"key": "title", "type": "str"},
-        "subtitle": {"key": "subtitle", "type": "str"},
-        "text": {"key": "text", "type": "str"},
-        "image": {"key": "image", "type": "ThumbnailUrl"},
-        "media": {"key": "media", "type": "[MediaUrl]"},
-        "buttons": {"key": "buttons", "type": "[CardAction]"},
-        "shareable": {"key": "shareable", "type": "bool"},
-        "autoloop": {"key": "autoloop", "type": "bool"},
-        "autostart": {"key": "autostart", "type": "bool"},
-        "aspect": {"key": "aspect", "type": "str"},
-        "duration": {"key": "duration", "type": "str"},
-        "value": {"key": "value", "type": "object"},
-    }
-
-    def __init__(self, **kwargs):
-        super(VideoCard, self).__init__(**kwargs)
-        self.title = kwargs.get("title", None)
-        self.subtitle = kwargs.get("subtitle", None)
-        self.text = kwargs.get("text", None)
-        self.image = kwargs.get("image", None)
-        self.media = kwargs.get("media", None)
-        self.buttons = kwargs.get("buttons", None)
-        self.shareable = kwargs.get("shareable", None)
-        self.autoloop = kwargs.get("autoloop", None)
-        self.autostart = kwargs.get("autostart", None)
-        self.aspect = kwargs.get("aspect", None)
-        self.duration = kwargs.get("duration", None)
-        self.value = kwargs.get("value", None)
diff --git a/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py b/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py
index 58caa1567..472ff51ce 100644
--- a/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py
+++ b/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py
@@ -1,18 +1,114 @@
-# coding=utf-8
-# --------------------------------------------------------------------------
 # Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License. See License.txt in the project root for
-# license information.
-#
-# Code generated by Microsoft (R) AutoRest Code Generator.
-# Changes may cause incorrect behavior and will be lost if the code is
-# regenerated.
-# --------------------------------------------------------------------------
+# Licensed under the MIT License.
 
+from botbuilder.schema._connector_client_enums import ActivityTypes
+from datetime import datetime
+from enum import Enum
 from msrest.serialization import Model
 from msrest.exceptions import HttpOperationError
 
 
+class ActivityEventNames(str, Enum):
+    continue_conversation = "ContinueConversation"
+    create_conversation = "CreateConversation"
+
+
+class ConversationReference(Model):
+    """An object relating to a particular point in a conversation.
+
+    :param activity_id: (Optional) ID of the activity to refer to
+    :type activity_id: str
+    :param user: (Optional) User participating in this conversation
+    :type user: ~botframework.connector.models.ChannelAccount
+    :param bot: Bot participating in this conversation
+    :type bot: ~botframework.connector.models.ChannelAccount
+    :param conversation: Conversation reference
+    :type conversation: ~botframework.connector.models.ConversationAccount
+    :param channel_id: Channel ID
+    :type channel_id: str
+    :param locale: A locale name for the contents of the text field.
+        The locale name is a combination of an ISO 639 two- or three-letter
+        culture code associated with a language and an ISO 3166 two-letter
+        subculture code associated with a country or region.
+        The locale name can also correspond to a valid BCP-47 language tag.
+    :type locale: str
+    :param service_url: Service endpoint where operations concerning the
+     referenced conversation may be performed
+    :type service_url: str
+    """
+
+    _attribute_map = {
+        "activity_id": {"key": "activityId", "type": "str"},
+        "user": {"key": "user", "type": "ChannelAccount"},
+        "bot": {"key": "bot", "type": "ChannelAccount"},
+        "conversation": {"key": "conversation", "type": "ConversationAccount"},
+        "channel_id": {"key": "channelId", "type": "str"},
+        "locale": {"key": "locale", "type": "str"},
+        "service_url": {"key": "serviceUrl", "type": "str"},
+    }
+
+    def __init__(
+        self,
+        *,
+        activity_id: str = None,
+        user=None,
+        bot=None,
+        conversation=None,
+        channel_id: str = None,
+        locale: str = None,
+        service_url: str = None,
+        **kwargs
+    ) -> None:
+        super(ConversationReference, self).__init__(**kwargs)
+        self.activity_id = activity_id
+        self.user = user
+        self.bot = bot
+        self.conversation = conversation
+        self.channel_id = channel_id
+        self.locale = locale
+        self.service_url = service_url
+
+
+class Mention(Model):
+    """Mention information (entity type: "mention").
+
+    :param mentioned: The mentioned user
+    :type mentioned: ~botframework.connector.models.ChannelAccount
+    :param text: Sub Text which represents the mention (can be null or empty)
+    :type text: str
+    :param type: Type of this entity (RFC 3987 IRI)
+    :type type: str
+    """
+
+    _attribute_map = {
+        "mentioned": {"key": "mentioned", "type": "ChannelAccount"},
+        "text": {"key": "text", "type": "str"},
+        "type": {"key": "type", "type": "str"},
+    }
+
+    def __init__(
+        self, *, mentioned=None, text: str = None, type: str = None, **kwargs
+    ) -> None:
+        super(Mention, self).__init__(**kwargs)
+        self.mentioned = mentioned
+        self.text = text
+        self.type = type
+
+
+class ResourceResponse(Model):
+    """A response containing a resource ID.
+
+    :param id: Id of the resource
+    :type id: str
+    """
+
+    _attribute_map = {"id": {"key": "id", "type": "str"}}
+
+    def __init__(self, *, id: str = None, **kwargs) -> None:
+        super(ResourceResponse, self).__init__(**kwargs)
+        self.id = id
+
+
 class Activity(Model):
     """An Activity is the basic communication type for the Bot Framework 3.0
     protocol.
@@ -133,7 +229,7 @@ class Activity(Model):
     :param delivery_mode: A delivery hint to signal to the recipient alternate
      delivery paths for the activity.
      The default delivery mode is "default". Possible values include: 'normal',
-     'notification'
+     'notification', 'expectReplies', 'ephemeral'
     :type delivery_mode: str or ~botframework.connector.models.DeliveryModes
     :param listen_for: List of phrases and references that speech and language
      priming systems should listen for
@@ -287,6 +383,460 @@ def __init__(
         self.semantic_action = semantic_action
         self.caller_id = caller_id
 
+    def apply_conversation_reference(
+        self, reference: ConversationReference, is_incoming: bool = False
+    ):
+        """
+        Updates this activity with the delivery information from an existing ConversationReference
+
+        :param reference: The existing conversation reference.
+        :param is_incoming: Optional, True to treat the activity as an
+        incoming activity, where the bot is the recipient; otherwise, False.
+        Default is False, and the activity will show the bot as the sender.
+
+        :returns: his activity, updated with the delivery information.
+
+        .. remarks::
+            Call GetConversationReference on an incoming
+            activity to get a conversation reference that you can then use to update an
+            outgoing activity with the correct delivery information.
+        """
+        self.channel_id = reference.channel_id
+        self.service_url = reference.service_url
+        self.conversation = reference.conversation
+
+        if reference.locale is not None:
+            self.locale = reference.locale
+
+        if is_incoming:
+            self.from_property = reference.user
+            self.recipient = reference.bot
+
+            if reference.activity_id is not None:
+                self.id = reference.activity_id
+        else:
+            self.from_property = reference.bot
+            self.recipient = reference.user
+
+            if reference.activity_id is not None:
+                self.reply_to_id = reference.activity_id
+
+        return self
+
+    def as_contact_relation_update_activity(self):
+        """
+        Returns this activity as a ContactRelationUpdateActivity object;
+        or None, if this is not that type of activity.
+
+        :returns: This activity as a message activity; or None.
+        """
+        return (
+            self if self.__is_activity(ActivityTypes.contact_relation_update) else None
+        )
+
+    def as_conversation_update_activity(self):
+        """
+        Returns this activity as a ConversationUpdateActivity object;
+        or None, if this is not that type of activity.
+
+        :returns: This activity as a conversation update activity; or None.
+        """
+        return self if self.__is_activity(ActivityTypes.conversation_update) else None
+
+    def as_end_of_conversation_activity(self):
+        """
+        Returns this activity as an EndOfConversationActivity object;
+        or None, if this is not that type of activity.
+
+        :returns: This activity as an end of conversation activity; or None.
+        """
+        return self if self.__is_activity(ActivityTypes.end_of_conversation) else None
+
+    def as_event_activity(self):
+        """
+        Returns this activity as an EventActivity object;
+        or None, if this is not that type of activity.
+
+        :returns: This activity as an event activity; or None.
+        """
+        return self if self.__is_activity(ActivityTypes.event) else None
+
+    def as_handoff_activity(self):
+        """
+        Returns this activity as a HandoffActivity object;
+        or None, if this is not that type of activity.
+
+        :returns: This activity as a handoff activity; or None.
+        """
+        return self if self.__is_activity(ActivityTypes.handoff) else None
+
+    def as_installation_update_activity(self):
+        """
+        Returns this activity as an InstallationUpdateActivity object;
+        or None, if this is not that type of activity.
+
+        :returns: This activity as an installation update activity; or None.
+        """
+        return self if self.__is_activity(ActivityTypes.installation_update) else None
+
+    def as_invoke_activity(self):
+        """
+        Returns this activity as an InvokeActivity object;
+        or None, if this is not that type of activity.
+
+        :returns: This activity as an invoke activity; or None.
+        """
+        return self if self.__is_activity(ActivityTypes.invoke) else None
+
+    def as_message_activity(self):
+        """
+        Returns this activity as a MessageActivity object;
+        or None, if this is not that type of activity.
+
+        :returns: This activity as a message activity; or None.
+        """
+        return self if self.__is_activity(ActivityTypes.message) else None
+
+    def as_message_delete_activity(self):
+        """
+        Returns this activity as a MessageDeleteActivity object;
+        or None, if this is not that type of activity.
+
+        :returns: This activity as a message delete request; or None.
+        """
+        return self if self.__is_activity(ActivityTypes.message_delete) else None
+
+    def as_message_reaction_activity(self):
+        """
+        Returns this activity as a MessageReactionActivity object;
+        or None, if this is not that type of activity.
+
+        :return: This activity as a message reaction activity; or None.
+        """
+        return self if self.__is_activity(ActivityTypes.message_reaction) else None
+
+    def as_message_update_activity(self):
+        """
+        Returns this activity as an MessageUpdateActivity object;
+        or None, if this is not that type of activity.
+
+        :returns: This activity as a message update request; or None.
+        """
+        return self if self.__is_activity(ActivityTypes.message_update) else None
+
+    def as_suggestion_activity(self):
+        """
+        Returns this activity as a SuggestionActivity object;
+        or None, if this is not that type of activity.
+
+        :returns: This activity as a suggestion activity; or None.
+        """
+        return self if self.__is_activity(ActivityTypes.suggestion) else None
+
+    def as_trace_activity(self):
+        """
+        Returns this activity as a TraceActivity object;
+        or None, if this is not that type of activity.
+
+        :returns: This activity as a trace activity; or None.
+        """
+        return self if self.__is_activity(ActivityTypes.trace) else None
+
+    def as_typing_activity(self):
+        """
+        Returns this activity as a TypingActivity object;
+        or null, if this is not that type of activity.
+
+        :returns: This activity as a typing activity; or null.
+        """
+        return self if self.__is_activity(ActivityTypes.typing) else None
+
+    @staticmethod
+    def create_contact_relation_update_activity():
+        """
+        Creates an instance of the :class:`Activity` class as aContactRelationUpdateActivity object.
+
+        :returns: The new contact relation update activity.
+        """
+        return Activity(type=ActivityTypes.contact_relation_update)
+
+    @staticmethod
+    def create_conversation_update_activity():
+        """
+        Creates an instance of the :class:`Activity` class as a ConversationUpdateActivity object.
+
+        :returns: The new conversation update activity.
+        """
+        return Activity(type=ActivityTypes.conversation_update)
+
+    @staticmethod
+    def create_end_of_conversation_activity():
+        """
+        Creates an instance of the :class:`Activity` class as an EndOfConversationActivity object.
+
+        :returns: The new end of conversation activity.
+        """
+        return Activity(type=ActivityTypes.end_of_conversation)
+
+    @staticmethod
+    def create_event_activity():
+        """
+        Creates an instance of the :class:`Activity` class as an EventActivity object.
+
+        :returns: The new event activity.
+        """
+        return Activity(type=ActivityTypes.event)
+
+    @staticmethod
+    def create_handoff_activity():
+        """
+        Creates an instance of the :class:`Activity` class as a HandoffActivity object.
+
+        :returns: The new handoff activity.
+        """
+        return Activity(type=ActivityTypes.handoff)
+
+    @staticmethod
+    def create_invoke_activity():
+        """
+        Creates an instance of the :class:`Activity` class as an InvokeActivity object.
+
+        :returns: The new invoke activity.
+        """
+        return Activity(type=ActivityTypes.invoke)
+
+    @staticmethod
+    def create_message_activity():
+        """
+        Creates an instance of the :class:`Activity` class as a MessageActivity object.
+
+        :returns: The new message activity.
+        """
+        return Activity(type=ActivityTypes.message)
+
+    def create_reply(self, text: str = None, locale: str = None):
+        """
+        Creates a new message activity as a response to this activity.
+
+        :param text: The text of the reply.
+        :param locale: The language code for the text.
+
+        :returns: The new message activity.
+
+        .. remarks::
+            The new activity sets up routing information based on this activity.
+        """
+        return Activity(
+            type=ActivityTypes.message,
+            timestamp=datetime.utcnow(),
+            from_property=ChannelAccount(
+                id=self.recipient.id if self.recipient else None,
+                name=self.recipient.name if self.recipient else None,
+            ),
+            recipient=ChannelAccount(
+                id=self.from_property.id if self.from_property else None,
+                name=self.from_property.name if self.from_property else None,
+            ),
+            reply_to_id=self.id,
+            service_url=self.service_url,
+            channel_id=self.channel_id,
+            conversation=ConversationAccount(
+                is_group=self.conversation.is_group,
+                id=self.conversation.id,
+                name=self.conversation.name,
+            ),
+            text=text if text else "",
+            locale=locale if locale else self.locale,
+            attachments=[],
+            entities=[],
+        )
+
+    def create_trace(
+        self, name: str, value: object = None, value_type: str = None, label: str = None
+    ):
+        """
+        Creates a new trace activity based on this activity.
+
+        :param name: The name of the trace operation to create.
+        :param value: Optional, the content for this trace operation.
+        :param value_type: Optional, identifier for the format of the value
+        Default is the name of type of the value.
+        :param label: Optional, a descriptive label for this trace operation.
+
+        :returns: The new trace activity.
+        """
+        if not value_type and value:
+            value_type = type(value)
+
+        return Activity(
+            type=ActivityTypes.trace,
+            timestamp=datetime.utcnow(),
+            from_property=ChannelAccount(
+                id=self.recipient.id if self.recipient else None,
+                name=self.recipient.name if self.recipient else None,
+            ),
+            recipient=ChannelAccount(
+                id=self.from_property.id if self.from_property else None,
+                name=self.from_property.name if self.from_property else None,
+            ),
+            reply_to_id=self.id,
+            service_url=self.service_url,
+            channel_id=self.channel_id,
+            conversation=ConversationAccount(
+                is_group=self.conversation.is_group,
+                id=self.conversation.id,
+                name=self.conversation.name,
+            ),
+            name=name,
+            label=label,
+            value_type=value_type,
+            value=value,
+        ).as_trace_activity()
+
+    @staticmethod
+    def create_trace_activity(
+        name: str, value: object = None, value_type: str = None, label: str = None
+    ):
+        """
+        Creates an instance of the :class:`Activity` class as a TraceActivity object.
+
+        :param name: The name of the trace operation to create.
+        :param value: Optional, the content for this trace operation.
+        :param value_type: Optional, identifier for the format of the value.
+        Default is the name of type of the value.
+        :param label: Optional, a descriptive label for this trace operation.
+
+        :returns: The new trace activity.
+        """
+        if not value_type and value:
+            value_type = type(value)
+
+        return Activity(
+            type=ActivityTypes.trace,
+            name=name,
+            label=label,
+            value_type=value_type,
+            value=value,
+        )
+
+    @staticmethod
+    def create_typing_activity():
+        """
+        Creates an instance of the :class:`Activity` class as a TypingActivity object.
+
+        :returns: The new typing activity.
+        """
+        return Activity(type=ActivityTypes.typing)
+
+    def get_conversation_reference(self):
+        """
+        Creates a ConversationReference based on this activity.
+
+        :returns: A conversation reference for the conversation that contains this activity.
+        """
+        return ConversationReference(
+            activity_id=self.id,
+            user=self.from_property,
+            bot=self.recipient,
+            conversation=self.conversation,
+            channel_id=self.channel_id,
+            locale=self.locale,
+            service_url=self.service_url,
+        )
+
+    def get_mentions(self) -> [Mention]:
+        """
+        Resolves the mentions from the entities of this activity.
+
+        :returns: The array of mentions; or an empty array, if none are found.
+
+        .. remarks::
+            This method is defined on the :class:`Activity` class, but is only intended
+            for use with a message activity, where the activity Activity.Type is set to
+            ActivityTypes.Message.
+        """
+        _list = self.entities
+        return [x for x in _list if str(x.type).lower() == "mention"]
+
+    def get_reply_conversation_reference(
+        self, reply: ResourceResponse
+    ) -> ConversationReference:
+        """
+        Create a ConversationReference based on this Activity's Conversation info
+        and the ResourceResponse from sending an activity.
+
+        :param reply: ResourceResponse returned from send_activity.
+
+        :return: A ConversationReference that can be stored and used later to delete or update the activity.
+        """
+        reference = self.get_conversation_reference()
+        reference.activity_id = reply.id
+        return reference
+
+    def has_content(self) -> bool:
+        """
+        Indicates whether this activity has content.
+
+        :returns: True, if this activity has any content to send; otherwise, false.
+
+        .. remarks::
+            This method is defined on the :class:`Activity` class, but is only intended
+            for use with a message activity, where the activity Activity.Type is set to
+            ActivityTypes.Message.
+        """
+        if self.text and self.text.strip():
+            return True
+
+        if self.summary and self.summary.strip():
+            return True
+
+        if self.attachments and len(self.attachments) > 0:
+            return True
+
+        if self.channel_data:
+            return True
+
+        return False
+
+    def is_from_streaming_connection(self) -> bool:
+        """
+        Determine if the Activity was sent via an Http/Https connection or Streaming
+        This can be determined by looking at the service_url property:
+        (1) All channels that send messages via http/https are not streaming
+        (2) Channels that send messages via streaming have a ServiceUrl that does not begin with http/https.
+
+        :returns: True if the Activity originated from a streaming connection.
+        """
+        if self.service_url:
+            return not self.service_url.lower().startswith("http")
+        return False
+
+    def __is_activity(self, activity_type: str) -> bool:
+        """
+        Indicates whether this activity is of a specified activity type.
+
+        :param activity_type: The activity type to check for.
+        :return: True if this activity is of the specified activity type; otherwise, False.
+        """
+        if self.type is None:
+            return False
+
+        type_attribute = str(self.type).lower()
+        activity_type = str(activity_type).lower()
+
+        result = type_attribute.startswith(activity_type)
+
+        if result:
+            result = len(type_attribute) == len(activity_type)
+
+            if not result:
+                result = (
+                    len(type_attribute) > len(activity_type)
+                    and type_attribute[len(activity_type)] == "/"
+                )
+
+        return result
+
 
 class AnimationCard(Model):
     """An animation card (Ex: gif or short video clip).
@@ -627,7 +1177,7 @@ class CardAction(Model):
 
     :param type: The type of action implemented by this button. Possible
      values include: 'openUrl', 'imBack', 'postBack', 'playAudio', 'playVideo',
-     'showImage', 'downloadFile', 'signin', 'call', 'payment', 'messageBack'
+     'showImage', 'downloadFile', 'signin', 'call', 'messageBack'
     :type type: str or ~botframework.connector.models.ActionTypes
     :param title: Text description which appears on the button
     :type title: str
@@ -644,6 +1194,8 @@ class CardAction(Model):
     :type value: object
     :param channel_data: Channel-specific data associated with this action
     :type channel_data: object
+    :param image_alt_text: Alternate image text to be used in place of the `image` field
+    :type image_alt_text: str
     """
 
     _attribute_map = {
@@ -654,6 +1206,7 @@ class CardAction(Model):
         "display_text": {"key": "displayText", "type": "str"},
         "value": {"key": "value", "type": "object"},
         "channel_data": {"key": "channelData", "type": "object"},
+        "image_alt_text": {"key": "imageAltText", "type": "str"},
     }
 
     def __init__(
@@ -666,6 +1219,7 @@ def __init__(
         display_text: str = None,
         value=None,
         channel_data=None,
+        image_alt_text: str = None,
         **kwargs
     ) -> None:
         super(CardAction, self).__init__(**kwargs)
@@ -676,6 +1230,7 @@ def __init__(
         self.display_text = display_text
         self.value = value
         self.channel_data = channel_data
+        self.image_alt_text = image_alt_text
 
 
 class CardImage(Model):
@@ -758,8 +1313,8 @@ class ConversationAccount(Model):
     :param aad_object_id: This account's object ID within Azure Active
      Directory (AAD)
     :type aad_object_id: str
-    :param role: Role of the entity behind the account (Example: User, Bot,
-     etc.). Possible values include: 'user', 'bot'
+    :param role: Role of the entity behind the account (Example: User, Bot, Skill
+     etc.). Possible values include: 'user', 'bot', 'skill'
     :type role: str or ~botframework.connector.models.RoleTypes
     :param tenant_id: This conversation's tenant ID
     :type tenant_id: str
@@ -876,53 +1431,6 @@ def __init__(
         self.tenant_id = tenant_id
 
 
-class ConversationReference(Model):
-    """An object relating to a particular point in a conversation.
-
-    :param activity_id: (Optional) ID of the activity to refer to
-    :type activity_id: str
-    :param user: (Optional) User participating in this conversation
-    :type user: ~botframework.connector.models.ChannelAccount
-    :param bot: Bot participating in this conversation
-    :type bot: ~botframework.connector.models.ChannelAccount
-    :param conversation: Conversation reference
-    :type conversation: ~botframework.connector.models.ConversationAccount
-    :param channel_id: Channel ID
-    :type channel_id: str
-    :param service_url: Service endpoint where operations concerning the
-     referenced conversation may be performed
-    :type service_url: str
-    """
-
-    _attribute_map = {
-        "activity_id": {"key": "activityId", "type": "str"},
-        "user": {"key": "user", "type": "ChannelAccount"},
-        "bot": {"key": "bot", "type": "ChannelAccount"},
-        "conversation": {"key": "conversation", "type": "ConversationAccount"},
-        "channel_id": {"key": "channelId", "type": "str"},
-        "service_url": {"key": "serviceUrl", "type": "str"},
-    }
-
-    def __init__(
-        self,
-        *,
-        activity_id: str = None,
-        user=None,
-        bot=None,
-        conversation=None,
-        channel_id: str = None,
-        service_url: str = None,
-        **kwargs
-    ) -> None:
-        super(ConversationReference, self).__init__(**kwargs)
-        self.activity_id = activity_id
-        self.user = user
-        self.bot = bot
-        self.conversation = conversation
-        self.channel_id = channel_id
-        self.service_url = service_url
-
-
 class ConversationResourceResponse(Model):
     """A response containing a resource.
 
@@ -978,6 +1486,21 @@ def __init__(
         self.conversations = conversations
 
 
+class ExpectedReplies(Model):
+    """ExpectedReplies.
+
+    :param activities: A collection of Activities that conforms to the
+     ExpectedReplies schema.
+    :type activities: list[~botframework.connector.models.Activity]
+    """
+
+    _attribute_map = {"activities": {"key": "activities", "type": "[Activity]"}}
+
+    def __init__(self, *, activities=None, **kwargs) -> None:
+        super(ExpectedReplies, self).__init__(**kwargs)
+        self.activities = activities
+
+
 class Entity(Model):
     """Metadata object pertaining to an activity.
 
@@ -1298,32 +1821,6 @@ def __init__(self, *, url: str = None, profile: str = None, **kwargs) -> None:
         self.profile = profile
 
 
-class Mention(Model):
-    """Mention information (entity type: "mention").
-
-    :param mentioned: The mentioned user
-    :type mentioned: ~botframework.connector.models.ChannelAccount
-    :param text: Sub Text which represents the mention (can be null or empty)
-    :type text: str
-    :param type: Type of this entity (RFC 3987 IRI)
-    :type type: str
-    """
-
-    _attribute_map = {
-        "mentioned": {"key": "mentioned", "type": "ChannelAccount"},
-        "text": {"key": "text", "type": "str"},
-        "type": {"key": "type", "type": "str"},
-    }
-
-    def __init__(
-        self, *, mentioned=None, text: str = None, type: str = None, **kwargs
-    ) -> None:
-        super(Mention, self).__init__(**kwargs)
-        self.mentioned = mentioned
-        self.text = text
-        self.type = type
-
-
 class MessageReaction(Model):
     """Message reaction object.
 
@@ -1339,38 +1836,6 @@ def __init__(self, *, type=None, **kwargs) -> None:
         self.type = type
 
 
-class MicrosoftPayMethodData(Model):
-    """W3C Payment Method Data for Microsoft Pay.
-
-    :param merchant_id: Microsoft Pay Merchant ID
-    :type merchant_id: str
-    :param supported_networks: Supported payment networks (e.g., "visa" and
-     "mastercard")
-    :type supported_networks: list[str]
-    :param supported_types: Supported payment types (e.g., "credit")
-    :type supported_types: list[str]
-    """
-
-    _attribute_map = {
-        "merchant_id": {"key": "merchantId", "type": "str"},
-        "supported_networks": {"key": "supportedNetworks", "type": "[str]"},
-        "supported_types": {"key": "supportedTypes", "type": "[str]"},
-    }
-
-    def __init__(
-        self,
-        *,
-        merchant_id: str = None,
-        supported_networks=None,
-        supported_types=None,
-        **kwargs
-    ) -> None:
-        super(MicrosoftPayMethodData, self).__init__(**kwargs)
-        self.merchant_id = merchant_id
-        self.supported_networks = supported_networks
-        self.supported_types = supported_types
-
-
 class OAuthCard(Model):
     """A card representing a request to perform a sign in via OAuth.
 
@@ -1386,15 +1851,23 @@ class OAuthCard(Model):
         "text": {"key": "text", "type": "str"},
         "connection_name": {"key": "connectionName", "type": "str"},
         "buttons": {"key": "buttons", "type": "[CardAction]"},
+        "token_exchange_resource": {"key": "tokenExchangeResource", "type": "object"},
     }
 
     def __init__(
-        self, *, text: str = None, connection_name: str = None, buttons=None, **kwargs
+        self,
+        *,
+        text: str = None,
+        connection_name: str = None,
+        buttons=None,
+        token_exchange_resource=None,
+        **kwargs
     ) -> None:
         super(OAuthCard, self).__init__(**kwargs)
         self.text = text
         self.connection_name = connection_name
         self.buttons = buttons
+        self.token_exchange_resource = token_exchange_resource
 
 
 class PagedMembersResult(Model):
@@ -1419,544 +1892,6 @@ def __init__(
         self.members = members
 
 
-class PaymentAddress(Model):
-    """Address within a Payment Request.
-
-    :param country: This is the CLDR (Common Locale Data Repository) region
-     code. For example, US, GB, CN, or JP
-    :type country: str
-    :param address_line: This is the most specific part of the address. It can
-     include, for example, a street name, a house number, apartment number, a
-     rural delivery route, descriptive instructions, or a post office box
-     number.
-    :type address_line: list[str]
-    :param region: This is the top level administrative subdivision of the
-     country. For example, this can be a state, a province, an oblast, or a
-     prefecture.
-    :type region: str
-    :param city: This is the city/town portion of the address.
-    :type city: str
-    :param dependent_locality: This is the dependent locality or sublocality
-     within a city. For example, used for neighborhoods, boroughs, districts,
-     or UK dependent localities.
-    :type dependent_locality: str
-    :param postal_code: This is the postal code or ZIP code, also known as PIN
-     code in India.
-    :type postal_code: str
-    :param sorting_code: This is the sorting code as used in, for example,
-     France.
-    :type sorting_code: str
-    :param language_code: This is the BCP-47 language code for the address.
-     It's used to determine the field separators and the order of fields when
-     formatting the address for display.
-    :type language_code: str
-    :param organization: This is the organization, firm, company, or
-     institution at this address.
-    :type organization: str
-    :param recipient: This is the name of the recipient or contact person.
-    :type recipient: str
-    :param phone: This is the phone number of the recipient or contact person.
-    :type phone: str
-    """
-
-    _attribute_map = {
-        "country": {"key": "country", "type": "str"},
-        "address_line": {"key": "addressLine", "type": "[str]"},
-        "region": {"key": "region", "type": "str"},
-        "city": {"key": "city", "type": "str"},
-        "dependent_locality": {"key": "dependentLocality", "type": "str"},
-        "postal_code": {"key": "postalCode", "type": "str"},
-        "sorting_code": {"key": "sortingCode", "type": "str"},
-        "language_code": {"key": "languageCode", "type": "str"},
-        "organization": {"key": "organization", "type": "str"},
-        "recipient": {"key": "recipient", "type": "str"},
-        "phone": {"key": "phone", "type": "str"},
-    }
-
-    def __init__(
-        self,
-        *,
-        country: str = None,
-        address_line=None,
-        region: str = None,
-        city: str = None,
-        dependent_locality: str = None,
-        postal_code: str = None,
-        sorting_code: str = None,
-        language_code: str = None,
-        organization: str = None,
-        recipient: str = None,
-        phone: str = None,
-        **kwargs
-    ) -> None:
-        super(PaymentAddress, self).__init__(**kwargs)
-        self.country = country
-        self.address_line = address_line
-        self.region = region
-        self.city = city
-        self.dependent_locality = dependent_locality
-        self.postal_code = postal_code
-        self.sorting_code = sorting_code
-        self.language_code = language_code
-        self.organization = organization
-        self.recipient = recipient
-        self.phone = phone
-
-
-class PaymentCurrencyAmount(Model):
-    """Supplies monetary amounts.
-
-    :param currency: A currency identifier
-    :type currency: str
-    :param value: Decimal monetary value
-    :type value: str
-    :param currency_system: Currency system
-    :type currency_system: str
-    """
-
-    _attribute_map = {
-        "currency": {"key": "currency", "type": "str"},
-        "value": {"key": "value", "type": "str"},
-        "currency_system": {"key": "currencySystem", "type": "str"},
-    }
-
-    def __init__(
-        self,
-        *,
-        currency: str = None,
-        value: str = None,
-        currency_system: str = None,
-        **kwargs
-    ) -> None:
-        super(PaymentCurrencyAmount, self).__init__(**kwargs)
-        self.currency = currency
-        self.value = value
-        self.currency_system = currency_system
-
-
-class PaymentDetails(Model):
-    """Provides information about the requested transaction.
-
-    :param total: Contains the total amount of the payment request
-    :type total: ~botframework.connector.models.PaymentItem
-    :param display_items: Contains line items for the payment request that the
-     user agent may display
-    :type display_items: list[~botframework.connector.models.PaymentItem]
-    :param shipping_options: A sequence containing the different shipping
-     options for the user to choose from
-    :type shipping_options:
-     list[~botframework.connector.models.PaymentShippingOption]
-    :param modifiers: Contains modifiers for particular payment method
-     identifiers
-    :type modifiers:
-     list[~botframework.connector.models.PaymentDetailsModifier]
-    :param error: Error description
-    :type error: str
-    """
-
-    _attribute_map = {
-        "total": {"key": "total", "type": "PaymentItem"},
-        "display_items": {"key": "displayItems", "type": "[PaymentItem]"},
-        "shipping_options": {
-            "key": "shippingOptions",
-            "type": "[PaymentShippingOption]",
-        },
-        "modifiers": {"key": "modifiers", "type": "[PaymentDetailsModifier]"},
-        "error": {"key": "error", "type": "str"},
-    }
-
-    def __init__(
-        self,
-        *,
-        total=None,
-        display_items=None,
-        shipping_options=None,
-        modifiers=None,
-        error: str = None,
-        **kwargs
-    ) -> None:
-        super(PaymentDetails, self).__init__(**kwargs)
-        self.total = total
-        self.display_items = display_items
-        self.shipping_options = shipping_options
-        self.modifiers = modifiers
-        self.error = error
-
-
-class PaymentDetailsModifier(Model):
-    """Provides details that modify the PaymentDetails based on payment method
-    identifier.
-
-    :param supported_methods: Contains a sequence of payment method
-     identifiers
-    :type supported_methods: list[str]
-    :param total: This value overrides the total field in the PaymentDetails
-     dictionary for the payment method identifiers in the supportedMethods
-     field
-    :type total: ~botframework.connector.models.PaymentItem
-    :param additional_display_items: Provides additional display items that
-     are appended to the displayItems field in the PaymentDetails dictionary
-     for the payment method identifiers in the supportedMethods field
-    :type additional_display_items:
-     list[~botframework.connector.models.PaymentItem]
-    :param data: A JSON-serializable object that provides optional information
-     that might be needed by the supported payment methods
-    :type data: object
-    """
-
-    _attribute_map = {
-        "supported_methods": {"key": "supportedMethods", "type": "[str]"},
-        "total": {"key": "total", "type": "PaymentItem"},
-        "additional_display_items": {
-            "key": "additionalDisplayItems",
-            "type": "[PaymentItem]",
-        },
-        "data": {"key": "data", "type": "object"},
-    }
-
-    def __init__(
-        self,
-        *,
-        supported_methods=None,
-        total=None,
-        additional_display_items=None,
-        data=None,
-        **kwargs
-    ) -> None:
-        super(PaymentDetailsModifier, self).__init__(**kwargs)
-        self.supported_methods = supported_methods
-        self.total = total
-        self.additional_display_items = additional_display_items
-        self.data = data
-
-
-class PaymentItem(Model):
-    """Indicates what the payment request is for and the value asked for.
-
-    :param label: Human-readable description of the item
-    :type label: str
-    :param amount: Monetary amount for the item
-    :type amount: ~botframework.connector.models.PaymentCurrencyAmount
-    :param pending: When set to true this flag means that the amount field is
-     not final.
-    :type pending: bool
-    """
-
-    _attribute_map = {
-        "label": {"key": "label", "type": "str"},
-        "amount": {"key": "amount", "type": "PaymentCurrencyAmount"},
-        "pending": {"key": "pending", "type": "bool"},
-    }
-
-    def __init__(
-        self, *, label: str = None, amount=None, pending: bool = None, **kwargs
-    ) -> None:
-        super(PaymentItem, self).__init__(**kwargs)
-        self.label = label
-        self.amount = amount
-        self.pending = pending
-
-
-class PaymentMethodData(Model):
-    """Indicates a set of supported payment methods and any associated payment
-    method specific data for those methods.
-
-    :param supported_methods: Required sequence of strings containing payment
-     method identifiers for payment methods that the merchant web site accepts
-    :type supported_methods: list[str]
-    :param data: A JSON-serializable object that provides optional information
-     that might be needed by the supported payment methods
-    :type data: object
-    """
-
-    _attribute_map = {
-        "supported_methods": {"key": "supportedMethods", "type": "[str]"},
-        "data": {"key": "data", "type": "object"},
-    }
-
-    def __init__(self, *, supported_methods=None, data=None, **kwargs) -> None:
-        super(PaymentMethodData, self).__init__(**kwargs)
-        self.supported_methods = supported_methods
-        self.data = data
-
-
-class PaymentOptions(Model):
-    """Provides information about the options desired for the payment request.
-
-    :param request_payer_name: Indicates whether the user agent should collect
-     and return the payer's name as part of the payment request
-    :type request_payer_name: bool
-    :param request_payer_email: Indicates whether the user agent should
-     collect and return the payer's email address as part of the payment
-     request
-    :type request_payer_email: bool
-    :param request_payer_phone: Indicates whether the user agent should
-     collect and return the payer's phone number as part of the payment request
-    :type request_payer_phone: bool
-    :param request_shipping: Indicates whether the user agent should collect
-     and return a shipping address as part of the payment request
-    :type request_shipping: bool
-    :param shipping_type: If requestShipping is set to true, then the
-     shippingType field may be used to influence the way the user agent
-     presents the user interface for gathering the shipping address
-    :type shipping_type: str
-    """
-
-    _attribute_map = {
-        "request_payer_name": {"key": "requestPayerName", "type": "bool"},
-        "request_payer_email": {"key": "requestPayerEmail", "type": "bool"},
-        "request_payer_phone": {"key": "requestPayerPhone", "type": "bool"},
-        "request_shipping": {"key": "requestShipping", "type": "bool"},
-        "shipping_type": {"key": "shippingType", "type": "str"},
-    }
-
-    def __init__(
-        self,
-        *,
-        request_payer_name: bool = None,
-        request_payer_email: bool = None,
-        request_payer_phone: bool = None,
-        request_shipping: bool = None,
-        shipping_type: str = None,
-        **kwargs
-    ) -> None:
-        super(PaymentOptions, self).__init__(**kwargs)
-        self.request_payer_name = request_payer_name
-        self.request_payer_email = request_payer_email
-        self.request_payer_phone = request_payer_phone
-        self.request_shipping = request_shipping
-        self.shipping_type = shipping_type
-
-
-class PaymentRequest(Model):
-    """A request to make a payment.
-
-    :param id: ID of this payment request
-    :type id: str
-    :param method_data: Allowed payment methods for this request
-    :type method_data: list[~botframework.connector.models.PaymentMethodData]
-    :param details: Details for this request
-    :type details: ~botframework.connector.models.PaymentDetails
-    :param options: Provides information about the options desired for the
-     payment request
-    :type options: ~botframework.connector.models.PaymentOptions
-    :param expires: Expiration for this request, in ISO 8601 duration format
-     (e.g., 'P1D')
-    :type expires: str
-    """
-
-    _attribute_map = {
-        "id": {"key": "id", "type": "str"},
-        "method_data": {"key": "methodData", "type": "[PaymentMethodData]"},
-        "details": {"key": "details", "type": "PaymentDetails"},
-        "options": {"key": "options", "type": "PaymentOptions"},
-        "expires": {"key": "expires", "type": "str"},
-    }
-
-    def __init__(
-        self,
-        *,
-        id: str = None,
-        method_data=None,
-        details=None,
-        options=None,
-        expires: str = None,
-        **kwargs
-    ) -> None:
-        super(PaymentRequest, self).__init__(**kwargs)
-        self.id = id
-        self.method_data = method_data
-        self.details = details
-        self.options = options
-        self.expires = expires
-
-
-class PaymentRequestComplete(Model):
-    """Payload delivered when completing a payment request.
-
-    :param id: Payment request ID
-    :type id: str
-    :param payment_request: Initial payment request
-    :type payment_request: ~botframework.connector.models.PaymentRequest
-    :param payment_response: Corresponding payment response
-    :type payment_response: ~botframework.connector.models.PaymentResponse
-    """
-
-    _attribute_map = {
-        "id": {"key": "id", "type": "str"},
-        "payment_request": {"key": "paymentRequest", "type": "PaymentRequest"},
-        "payment_response": {"key": "paymentResponse", "type": "PaymentResponse"},
-    }
-
-    def __init__(
-        self, *, id: str = None, payment_request=None, payment_response=None, **kwargs
-    ) -> None:
-        super(PaymentRequestComplete, self).__init__(**kwargs)
-        self.id = id
-        self.payment_request = payment_request
-        self.payment_response = payment_response
-
-
-class PaymentRequestCompleteResult(Model):
-    """Result from a completed payment request.
-
-    :param result: Result of the payment request completion
-    :type result: str
-    """
-
-    _attribute_map = {"result": {"key": "result", "type": "str"}}
-
-    def __init__(self, *, result: str = None, **kwargs) -> None:
-        super(PaymentRequestCompleteResult, self).__init__(**kwargs)
-        self.result = result
-
-
-class PaymentRequestUpdate(Model):
-    """An update to a payment request.
-
-    :param id: ID for the payment request to update
-    :type id: str
-    :param details: Update payment details
-    :type details: ~botframework.connector.models.PaymentDetails
-    :param shipping_address: Updated shipping address
-    :type shipping_address: ~botframework.connector.models.PaymentAddress
-    :param shipping_option: Updated shipping options
-    :type shipping_option: str
-    """
-
-    _attribute_map = {
-        "id": {"key": "id", "type": "str"},
-        "details": {"key": "details", "type": "PaymentDetails"},
-        "shipping_address": {"key": "shippingAddress", "type": "PaymentAddress"},
-        "shipping_option": {"key": "shippingOption", "type": "str"},
-    }
-
-    def __init__(
-        self,
-        *,
-        id: str = None,
-        details=None,
-        shipping_address=None,
-        shipping_option: str = None,
-        **kwargs
-    ) -> None:
-        super(PaymentRequestUpdate, self).__init__(**kwargs)
-        self.id = id
-        self.details = details
-        self.shipping_address = shipping_address
-        self.shipping_option = shipping_option
-
-
-class PaymentRequestUpdateResult(Model):
-    """A result object from a Payment Request Update invoke operation.
-
-    :param details: Update payment details
-    :type details: ~botframework.connector.models.PaymentDetails
-    """
-
-    _attribute_map = {"details": {"key": "details", "type": "PaymentDetails"}}
-
-    def __init__(self, *, details=None, **kwargs) -> None:
-        super(PaymentRequestUpdateResult, self).__init__(**kwargs)
-        self.details = details
-
-
-class PaymentResponse(Model):
-    """A PaymentResponse is returned when a user has selected a payment method and
-    approved a payment request.
-
-    :param method_name: The payment method identifier for the payment method
-     that the user selected to fulfil the transaction
-    :type method_name: str
-    :param details: A JSON-serializable object that provides a payment method
-     specific message used by the merchant to process the transaction and
-     determine successful fund transfer
-    :type details: object
-    :param shipping_address: If the requestShipping flag was set to true in
-     the PaymentOptions passed to the PaymentRequest constructor, then
-     shippingAddress will be the full and final shipping address chosen by the
-     user
-    :type shipping_address: ~botframework.connector.models.PaymentAddress
-    :param shipping_option: If the requestShipping flag was set to true in the
-     PaymentOptions passed to the PaymentRequest constructor, then
-     shippingOption will be the id attribute of the selected shipping option
-    :type shipping_option: str
-    :param payer_email: If the requestPayerEmail flag was set to true in the
-     PaymentOptions passed to the PaymentRequest constructor, then payerEmail
-     will be the email address chosen by the user
-    :type payer_email: str
-    :param payer_phone: If the requestPayerPhone flag was set to true in the
-     PaymentOptions passed to the PaymentRequest constructor, then payerPhone
-     will be the phone number chosen by the user
-    :type payer_phone: str
-    """
-
-    _attribute_map = {
-        "method_name": {"key": "methodName", "type": "str"},
-        "details": {"key": "details", "type": "object"},
-        "shipping_address": {"key": "shippingAddress", "type": "PaymentAddress"},
-        "shipping_option": {"key": "shippingOption", "type": "str"},
-        "payer_email": {"key": "payerEmail", "type": "str"},
-        "payer_phone": {"key": "payerPhone", "type": "str"},
-    }
-
-    def __init__(
-        self,
-        *,
-        method_name: str = None,
-        details=None,
-        shipping_address=None,
-        shipping_option: str = None,
-        payer_email: str = None,
-        payer_phone: str = None,
-        **kwargs
-    ) -> None:
-        super(PaymentResponse, self).__init__(**kwargs)
-        self.method_name = method_name
-        self.details = details
-        self.shipping_address = shipping_address
-        self.shipping_option = shipping_option
-        self.payer_email = payer_email
-        self.payer_phone = payer_phone
-
-
-class PaymentShippingOption(Model):
-    """Describes a shipping option.
-
-    :param id: String identifier used to reference this PaymentShippingOption
-    :type id: str
-    :param label: Human-readable description of the item
-    :type label: str
-    :param amount: Contains the monetary amount for the item
-    :type amount: ~botframework.connector.models.PaymentCurrencyAmount
-    :param selected: Indicates whether this is the default selected
-     PaymentShippingOption
-    :type selected: bool
-    """
-
-    _attribute_map = {
-        "id": {"key": "id", "type": "str"},
-        "label": {"key": "label", "type": "str"},
-        "amount": {"key": "amount", "type": "PaymentCurrencyAmount"},
-        "selected": {"key": "selected", "type": "bool"},
-    }
-
-    def __init__(
-        self,
-        *,
-        id: str = None,
-        label: str = None,
-        amount=None,
-        selected: bool = None,
-        **kwargs
-    ) -> None:
-        super(PaymentShippingOption, self).__init__(**kwargs)
-        self.id = id
-        self.label = label
-        self.amount = amount
-        self.selected = selected
-
-
 class Place(Model):
     """Place (entity type: "https://schema.org/Place").
 
@@ -2111,20 +2046,6 @@ def __init__(
         self.tap = tap
 
 
-class ResourceResponse(Model):
-    """A response containing a resource ID.
-
-    :param id: Id of the resource
-    :type id: str
-    """
-
-    _attribute_map = {"id": {"key": "id", "type": "str"}}
-
-    def __init__(self, *, id: str = None, **kwargs) -> None:
-        super(ResourceResponse, self).__init__(**kwargs)
-        self.id = id
-
-
 class SemanticAction(Model):
     """Represents a reference to a programmatic action.
 
@@ -2299,6 +2220,119 @@ def __init__(self, *, url: str = None, alt: str = None, **kwargs) -> None:
         self.alt = alt
 
 
+class TokenExchangeInvokeRequest(Model):
+    """TokenExchangeInvokeRequest.
+
+    :param id: The id from the OAuthCard.
+    :type id: str
+    :param connection_name: The connection name.
+    :type connection_name: str
+    :param token: The user token that can be exchanged.
+    :type token: str
+    :param properties: Extension data for overflow of properties.
+    :type properties: dict[str, object]
+    """
+
+    _attribute_map = {
+        "id": {"key": "id", "type": "str"},
+        "connection_name": {"key": "connectionName", "type": "str"},
+        "token": {"key": "token", "type": "str"},
+        "properties": {"key": "properties", "type": "{object}"},
+    }
+
+    def __init__(
+        self,
+        *,
+        id: str = None,
+        connection_name: str = None,
+        token: str = None,
+        properties=None,
+        **kwargs
+    ) -> None:
+        super(TokenExchangeInvokeRequest, self).__init__(**kwargs)
+        self.id = id
+        self.connection_name = connection_name
+        self.token = token
+        self.properties = properties
+
+
+class TokenExchangeInvokeResponse(Model):
+    """TokenExchangeInvokeResponse.
+
+    :param id: The id from the OAuthCard.
+    :type id: str
+    :param connection_name: The connection name.
+    :type connection_name: str
+    :param failure_detail: The details of why the token exchange failed.
+    :type failure_detail: str
+    :param properties: Extension data for overflow of properties.
+    :type properties: dict[str, object]
+    """
+
+    _attribute_map = {
+        "id": {"key": "id", "type": "str"},
+        "connection_name": {"key": "connectionName", "type": "str"},
+        "failure_detail": {"key": "failureDetail", "type": "str"},
+        "properties": {"key": "properties", "type": "{object}"},
+    }
+
+    def __init__(
+        self,
+        *,
+        id: str = None,
+        connection_name: str = None,
+        failure_detail: str = None,
+        properties=None,
+        **kwargs
+    ) -> None:
+        super(TokenExchangeInvokeResponse, self).__init__(**kwargs)
+        self.id = id
+        self.connection_name = connection_name
+        self.failure_detail = failure_detail
+        self.properties = properties
+
+
+class TokenExchangeState(Model):
+    """TokenExchangeState
+
+    :param connection_name: The connection name that was used.
+    :type connection_name: str
+    :param conversation: Gets or sets a reference to the conversation.
+    :type conversation: ~botframework.connector.models.ConversationReference
+    :param relates_to: Gets or sets a reference to a related parent conversation for this token exchange.
+    :type relates_to: ~botframework.connector.models.ConversationReference
+    :param bot_ur: The URL of the bot messaging endpoint.
+    :type bot_ur: str
+    :param ms_app_id: The bot's registered application ID.
+    :type ms_app_id: str
+    """
+
+    _attribute_map = {
+        "connection_name": {"key": "connectionName", "type": "str"},
+        "conversation": {"key": "conversation", "type": "ConversationReference"},
+        "relates_to": {"key": "relatesTo", "type": "ConversationReference"},
+        "bot_url": {"key": "connectionName", "type": "str"},
+        "ms_app_id": {"key": "msAppId", "type": "str"},
+    }
+
+    def __init__(
+        self,
+        *,
+        connection_name: str = None,
+        conversation=None,
+        relates_to=None,
+        bot_url: str = None,
+        ms_app_id: str = None,
+        **kwargs
+    ) -> None:
+        super(TokenExchangeState, self).__init__(**kwargs)
+        self.connection_name = connection_name
+        self.conversation = conversation
+        self.relates_to = relates_to
+        self.bot_url = bot_url
+        self.ms_app_id = ms_app_id
+
+
 class TokenRequest(Model):
     """A request to receive a user token.
 
diff --git a/libraries/botbuilder-schema/botbuilder/schema/_sign_in_enums.py b/libraries/botbuilder-schema/botbuilder/schema/_sign_in_enums.py
new file mode 100644
index 000000000..015e5a733
--- /dev/null
+++ b/libraries/botbuilder-schema/botbuilder/schema/_sign_in_enums.py
@@ -0,0 +1,14 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from enum import Enum
+
+
+class SignInConstants(str, Enum):
+
+    # Name for the signin invoke to verify the 6-digit authentication code as part of sign-in.
+    verify_state_operation_name = "signin/verifyState"
+    # Name for signin invoke to perform a token exchange.
+    token_exchange_operation_name = "signin/tokenExchange"
+    # The EventActivity name when a token is sent to the bot.
+    token_response_event_name = "tokens/response"
diff --git a/libraries/botbuilder-schema/botbuilder/schema/callerid_constants.py b/libraries/botbuilder-schema/botbuilder/schema/callerid_constants.py
new file mode 100644
index 000000000..3b2131306
--- /dev/null
+++ b/libraries/botbuilder-schema/botbuilder/schema/callerid_constants.py
@@ -0,0 +1,22 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from enum import Enum
+
+
+class CallerIdConstants(str, Enum):
+    public_azure_channel = "urn:botframework:azure"
+    """
+    The caller ID for any Bot Framework channel.
+    """
+
+    us_gov_channel = "urn:botframework:azureusgov"
+    """
+    The caller ID for any Bot Framework US Government cloud channel.
+    """
+
+    bot_to_bot_prefix = "urn:botframework:aadappid:"
+    """
+    The caller ID prefix when a bot initiates a request to another bot.
+    This prefix will be followed by the Azure Active Directory App ID of the bot that initiated the call.
+    """
diff --git a/libraries/botbuilder-schema/botbuilder/schema/health_results.py b/libraries/botbuilder-schema/botbuilder/schema/health_results.py
new file mode 100644
index 000000000..6205e68cb
--- /dev/null
+++ b/libraries/botbuilder-schema/botbuilder/schema/health_results.py
@@ -0,0 +1,32 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from typing import List
+from msrest.serialization import Model
+
+
+class HealthResults(Model):
+    _attribute_map = {
+        "success": {"key": "success", "type": "bool"},
+        "authorization": {"key": "authorization", "type": "str"},
+        "user_agent": {"key": "user-agent", "type": "str"},
+        "messages": {"key": "messages", "type": "[str]"},
+        "diagnostics": {"key": "diagnostics", "type": "object"},
+    }
+
+    def __init__(
+        self,
+        *,
+        success: bool = None,
+        authorization: str = None,
+        user_agent: str = None,
+        messages: List[str] = None,
+        diagnostics: object = None,
+        **kwargs
+    ) -> None:
+        super(HealthResults, self).__init__(**kwargs)
+        self.success = success
+        self.authorization = authorization
+        self.user_agent = user_agent
+        self.messages = messages
+        self.diagnostics = diagnostics
diff --git a/libraries/botbuilder-schema/botbuilder/schema/healthcheck_response.py b/libraries/botbuilder-schema/botbuilder/schema/healthcheck_response.py
new file mode 100644
index 000000000..e5ebea7e3
--- /dev/null
+++ b/libraries/botbuilder-schema/botbuilder/schema/healthcheck_response.py
@@ -0,0 +1,16 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from msrest.serialization import Model
+
+from botbuilder.schema import HealthResults
+
+
+class HealthCheckResponse(Model):
+    _attribute_map = {
+        "health_results": {"key": "healthResults", "type": "HealthResults"},
+    }
+
+    def __init__(self, *, health_results: HealthResults = None, **kwargs) -> None:
+        super(HealthCheckResponse, self).__init__(**kwargs)
+        self.health_results = health_results
diff --git a/libraries/botbuilder-schema/botbuilder/schema/speech_constants.py b/libraries/botbuilder-schema/botbuilder/schema/speech_constants.py
new file mode 100644
index 000000000..0fbc396e6
--- /dev/null
+++ b/libraries/botbuilder-schema/botbuilder/schema/speech_constants.py
@@ -0,0 +1,14 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+
+class SpeechConstants:
+    """
+    Defines constants that can be used in the processing of speech interactions.
+    """
+
+    EMPTY_SPEAK_TAG = ''
+    """
+    The xml tag structure to indicate an empty speak tag, to be used in the 'speak' property of an Activity.
+    When set this indicates to the channel that speech should not be generated.
+    """
diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py b/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py
new file mode 100644
index 000000000..b6116a3ec
--- /dev/null
+++ b/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py
@@ -0,0 +1,128 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from ._models_py3 import AppBasedLinkQuery
+from ._models_py3 import ChannelInfo
+from ._models_py3 import ConversationList
+from ._models_py3 import FileConsentCard
+from ._models_py3 import FileConsentCardResponse
+from ._models_py3 import FileDownloadInfo
+from ._models_py3 import FileInfoCard
+from ._models_py3 import FileUploadInfo
+from ._models_py3 import MessageActionsPayload
+from ._models_py3 import MessageActionsPayloadApp
+from ._models_py3 import MessageActionsPayloadAttachment
+from ._models_py3 import MessageActionsPayloadBody
+from ._models_py3 import MessageActionsPayloadConversation
+from ._models_py3 import MessageActionsPayloadFrom
+from ._models_py3 import MessageActionsPayloadMention
+from ._models_py3 import MessageActionsPayloadReaction
+from ._models_py3 import MessageActionsPayloadUser
+from ._models_py3 import MessagingExtensionAction
+from ._models_py3 import MessagingExtensionActionResponse
+from ._models_py3 import MessagingExtensionAttachment
+from ._models_py3 import MessagingExtensionParameter
+from ._models_py3 import MessagingExtensionQuery
+from ._models_py3 import MessagingExtensionQueryOptions
+from ._models_py3 import MessagingExtensionResponse
+from ._models_py3 import MessagingExtensionResult
+from ._models_py3 import MessagingExtensionSuggestedAction
+from ._models_py3 import NotificationInfo
+from ._models_py3 import O365ConnectorCard
+from ._models_py3 import O365ConnectorCardActionBase
+from ._models_py3 import O365ConnectorCardActionCard
+from ._models_py3 import O365ConnectorCardActionQuery
+from ._models_py3 import O365ConnectorCardDateInput
+from ._models_py3 import O365ConnectorCardFact
+from ._models_py3 import O365ConnectorCardHttpPOST
+from ._models_py3 import O365ConnectorCardImage
+from ._models_py3 import O365ConnectorCardInputBase
+from ._models_py3 import O365ConnectorCardMultichoiceInput
+from ._models_py3 import O365ConnectorCardMultichoiceInputChoice
+from ._models_py3 import O365ConnectorCardOpenUri
+from ._models_py3 import O365ConnectorCardOpenUriTarget
+from ._models_py3 import O365ConnectorCardSection
+from ._models_py3 import O365ConnectorCardTextInput
+from ._models_py3 import O365ConnectorCardViewAction
+from ._models_py3 import SigninStateVerificationQuery
+from ._models_py3 import TaskModuleContinueResponse
+from ._models_py3 import TaskModuleMessageResponse
+from ._models_py3 import TaskModuleRequest
+from ._models_py3 import TaskModuleRequestContext
+from ._models_py3 import TaskModuleResponse
+from ._models_py3 import TaskModuleResponseBase
+from ._models_py3 import TaskModuleTaskInfo
+from ._models_py3 import TeamDetails
+from ._models_py3 import TeamInfo
+from ._models_py3 import TeamsChannelAccount
+from ._models_py3 import TeamsChannelData
+from ._models_py3 import TeamsPagedMembersResult
+from ._models_py3 import TenantInfo
+from ._models_py3 import TeamsMeetingInfo
+from ._models_py3 import TeamsMeetingParticipant
+from ._models_py3 import MeetingParticipantInfo
+from ._models_py3 import CacheInfo
+
+__all__ = [
+    "AppBasedLinkQuery",
+    "ChannelInfo",
+    "ConversationList",
+    "FileConsentCard",
+    "FileConsentCardResponse",
+    "FileDownloadInfo",
+    "FileInfoCard",
+    "FileUploadInfo",
+    "MessageActionsPayload",
+    "MessageActionsPayloadApp",
+    "MessageActionsPayloadAttachment",
+    "MessageActionsPayloadBody",
+    "MessageActionsPayloadConversation",
+    "MessageActionsPayloadFrom",
+    "MessageActionsPayloadMention",
+    "MessageActionsPayloadReaction",
+    "MessageActionsPayloadUser",
+    "MessagingExtensionAction",
+    "MessagingExtensionActionResponse",
+    "MessagingExtensionAttachment",
+    "MessagingExtensionParameter",
+    "MessagingExtensionQuery",
+    "MessagingExtensionQueryOptions",
+    "MessagingExtensionResponse",
+    "MessagingExtensionResult",
+    "MessagingExtensionSuggestedAction",
+    "NotificationInfo",
+    "O365ConnectorCard",
+    "O365ConnectorCardActionBase",
+    "O365ConnectorCardActionCard",
+    "O365ConnectorCardActionQuery",
+    "O365ConnectorCardDateInput",
+    "O365ConnectorCardFact",
+    "O365ConnectorCardHttpPOST",
+    "O365ConnectorCardImage",
+    "O365ConnectorCardInputBase",
+    "O365ConnectorCardMultichoiceInput",
+    "O365ConnectorCardMultichoiceInputChoice",
+    "O365ConnectorCardOpenUri",
+    "O365ConnectorCardOpenUriTarget",
+    "O365ConnectorCardSection",
+    "O365ConnectorCardTextInput",
+    "O365ConnectorCardViewAction",
+    "SigninStateVerificationQuery",
+    "TaskModuleContinueResponse",
+    "TaskModuleMessageResponse",
+    "TaskModuleRequest",
+    "TaskModuleRequestContext",
+    "TaskModuleResponse",
+    "TaskModuleResponseBase",
+    "TaskModuleTaskInfo",
+    "TeamDetails",
+    "TeamInfo",
+    "TeamsChannelAccount",
+    "TeamsChannelData",
+    "TeamsPagedMembersResult",
+    "TenantInfo",
+    "TeamsMeetingInfo",
+    "TeamsMeetingParticipant",
+    "MeetingParticipantInfo",
+    "CacheInfo",
+]
diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py b/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py
new file mode 100644
index 000000000..e4d16baf8
--- /dev/null
+++ b/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py
@@ -0,0 +1,2060 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from msrest.serialization import Model
+from botbuilder.schema import (
+    Attachment,
+    ChannelAccount,
+    PagedMembersResult,
+    ConversationAccount,
+)
+
+
+class TaskModuleRequest(Model):
+    """Task module invoke request value payload.
+
+    :param data: User input data. Free payload with key-value pairs.
+    :type data: object
+    :param context: Current user context, i.e., the current theme
+    :type context:
+     ~botframework.connector.teams.models.TaskModuleRequestContext
+    """
+
+    _attribute_map = {
+        "data": {"key": "data", "type": "object"},
+        "context": {"key": "context", "type": "TaskModuleRequestContext"},
+    }
+
+    def __init__(self, *, data=None, context=None, **kwargs) -> None:
+        super(TaskModuleRequest, self).__init__(**kwargs)
+        self.data = data
+        self.context = context
+
+
+class AppBasedLinkQuery(Model):
+    """Invoke request body type for app-based link query.
+
+    :param url: Url queried by user
+    :type url: str
+    :param state: The magic code for OAuth Flow
+    :type state: str
+    """
+
+    _attribute_map = {
+        "url": {"key": "url", "type": "str"},
+        "state": {"key": "state", "type": "str"},
+    }
+
+    def __init__(self, *, url: str = None, state: str = None, **kwargs) -> None:
+        super(AppBasedLinkQuery, self).__init__(**kwargs)
+        self.url = url
+        self.state = state
+
+
+class ChannelInfo(Model):
+    """A channel info object which describes the channel.
+
+    :param id: Unique identifier representing a channel
+    :type id: str
+    :param name: Name of the channel
+    :type name: str
+    """
+
+    _attribute_map = {
+        "id": {"key": "id", "type": "str"},
+        "name": {"key": "name", "type": "str"},
+    }
+
+    def __init__(self, *, id: str = None, name: str = None, **kwargs) -> None:
+        super(ChannelInfo, self).__init__(**kwargs)
+        self.id = id
+        self.name = name
+
+
+class CacheInfo(Model):
+    """A cache info object which notifies Teams how long an object should be cached for.
+
+    :param cache_type: Type of Cache Info
+    :type cache_type: str
+    :param cache_duration: Duration of the Cached Info.
+    :type cache_duration: int
+    """
+
+    _attribute_map = {
+        "cache_type": {"key": "cacheType", "type": "str"},
+        "cache_duration": {"key": "cacheDuration", "type": "int"},
+    }
+
+    def __init__(
+        self, *, cache_type: str = None, cache_duration: int = None, **kwargs
+    ) -> None:
+        super(CacheInfo, self).__init__(**kwargs)
+        self.cache_type = cache_type
+        self.cache_duration = cache_duration
+
+
+class ConversationList(Model):
+    """List of channels under a team.
+
+    :param conversations:
+    :type conversations:
+     list[~botframework.connector.teams.models.ChannelInfo]
+    """
+
+    _attribute_map = {
+        "conversations": {"key": "conversations", "type": "[ChannelInfo]"},
+    }
+
+    def __init__(self, *, conversations=None, **kwargs) -> None:
+        super(ConversationList, self).__init__(**kwargs)
+        self.conversations = conversations
+
+
+class FileConsentCard(Model):
+    """File consent card attachment.
+
+    :param description: File description.
+    :type description: str
+    :param size_in_bytes: Size of the file to be uploaded in Bytes.
+    :type size_in_bytes: long
+    :param accept_context: Context sent back to the Bot if user consented to
+     upload. This is free flow schema and is sent back in Value field of
+     Activity.
+    :type accept_context: object
+    :param decline_context: Context sent back to the Bot if user declined.
+     This is free flow schema and is sent back in Value field of Activity.
+    :type decline_context: object
+    """
+
+    _attribute_map = {
+        "description": {"key": "description", "type": "str"},
+        "size_in_bytes": {"key": "sizeInBytes", "type": "long"},
+        "accept_context": {"key": "acceptContext", "type": "object"},
+        "decline_context": {"key": "declineContext", "type": "object"},
+    }
+
+    def __init__(
+        self,
+        *,
+        description: str = None,
+        size_in_bytes: int = None,
+        accept_context=None,
+        decline_context=None,
+        **kwargs
+    ) -> None:
+        super(FileConsentCard, self).__init__(**kwargs)
+        self.description = description
+        self.size_in_bytes = size_in_bytes
+        self.accept_context = accept_context
+        self.decline_context = decline_context
+
+
+class FileConsentCardResponse(Model):
+    """Represents the value of the invoke activity sent when the user acts on a
+    file consent card.
+
+    :param action: The action the user took. Possible values include:
+     'accept', 'decline'
+    :type action: str
+    :param context: The context associated with the action.
+    :type context: object
+    :param upload_info: If the user accepted the file, contains information
+     about the file to be uploaded.
+    :type upload_info: ~botframework.connector.teams.models.FileUploadInfo
+    """
+
+    _attribute_map = {
+        "action": {"key": "action", "type": "str"},
+        "context": {"key": "context", "type": "object"},
+        "upload_info": {"key": "uploadInfo", "type": "FileUploadInfo"},
+    }
+
+    def __init__(
+        self, *, action=None, context=None, upload_info=None, **kwargs
+    ) -> None:
+        super(FileConsentCardResponse, self).__init__(**kwargs)
+        self.action = action
+        self.context = context
+        self.upload_info = upload_info
+
+
+class FileDownloadInfo(Model):
+    """File download info attachment.
+
+    :param download_url: File download url.
+    :type download_url: str
+    :param unique_id: Unique Id for the file.
+    :type unique_id: str
+    :param file_type: Type of file.
+    :type file_type: str
+    :param etag: ETag for the file.
+    :type etag: object
+    """
+
+    _attribute_map = {
+        "download_url": {"key": "downloadUrl", "type": "str"},
+        "unique_id": {"key": "uniqueId", "type": "str"},
+        "file_type": {"key": "fileType", "type": "str"},
+        "etag": {"key": "etag", "type": "object"},
+    }
+
+    def __init__(
+        self,
+        *,
+        download_url: str = None,
+        unique_id: str = None,
+        file_type: str = None,
+        etag=None,
+        **kwargs
+    ) -> None:
+        super(FileDownloadInfo, self).__init__(**kwargs)
+        self.download_url = download_url
+        self.unique_id = unique_id
+        self.file_type = file_type
+        self.etag = etag
+
+
+class FileInfoCard(Model):
+    """File info card.
+
+    :param unique_id: Unique Id for the file.
+    :type unique_id: str
+    :param file_type: Type of file.
+    :type file_type: str
+    :param etag: ETag for the file.
+    :type etag: object
+    """
+
+    _attribute_map = {
+        "unique_id": {"key": "uniqueId", "type": "str"},
+        "file_type": {"key": "fileType", "type": "str"},
+        "etag": {"key": "etag", "type": "object"},
+    }
+
+    def __init__(
+        self, *, unique_id: str = None, file_type: str = None, etag=None, **kwargs
+    ) -> None:
+        super(FileInfoCard, self).__init__(**kwargs)
+        self.unique_id = unique_id
+        self.file_type = file_type
+        self.etag = etag
+
+
+class FileUploadInfo(Model):
+    """Information about the file to be uploaded.
+
+    :param name: Name of the file.
+    :type name: str
+    :param upload_url: URL to an upload session that the bot can use to set
+     the file contents.
+    :type upload_url: str
+    :param content_url: URL to file.
+    :type content_url: str
+    :param unique_id: ID that uniquely identifies the file.
+    :type unique_id: str
+    :param file_type: Type of the file.
+    :type file_type: str
+    """
+
+    _attribute_map = {
+        "name": {"key": "name", "type": "str"},
+        "upload_url": {"key": "uploadUrl", "type": "str"},
+        "content_url": {"key": "contentUrl", "type": "str"},
+        "unique_id": {"key": "uniqueId", "type": "str"},
+        "file_type": {"key": "fileType", "type": "str"},
+    }
+
+    def __init__(
+        self,
+        *,
+        name: str = None,
+        upload_url: str = None,
+        content_url: str = None,
+        unique_id: str = None,
+        file_type: str = None,
+        **kwargs
+    ) -> None:
+        super(FileUploadInfo, self).__init__(**kwargs)
+        self.name = name
+        self.upload_url = upload_url
+        self.content_url = content_url
+        self.unique_id = unique_id
+        self.file_type = file_type
+
+
+class MessageActionsPayloadApp(Model):
+    """Represents an application entity.
+
+    :param application_identity_type: The type of application. Possible values
+     include: 'aadApplication', 'bot', 'tenantBot', 'office365Connector',
+     'webhook'
+    :type application_identity_type: str or
+     ~botframework.connector.teams.models.enum
+    :param id: The id of the application.
+    :type id: str
+    :param display_name: The plaintext display name of the application.
+    :type display_name: str
+    """
+
+    _attribute_map = {
+        "application_identity_type": {"key": "applicationIdentityType", "type": "str"},
+        "id": {"key": "id", "type": "str"},
+        "display_name": {"key": "displayName", "type": "str"},
+    }
+
+    def __init__(
+        self,
+        *,
+        application_identity_type=None,
+        id: str = None,
+        display_name: str = None,
+        **kwargs
+    ) -> None:
+        super(MessageActionsPayloadApp, self).__init__(**kwargs)
+        self.application_identity_type = application_identity_type
+        self.id = id
+        self.display_name = display_name
+
+
+class MessageActionsPayloadAttachment(Model):
+    """Represents the attachment in a message.
+
+    :param id: The id of the attachment.
+    :type id: str
+    :param content_type: The type of the attachment.
+    :type content_type: str
+    :param content_url: The url of the attachment, in case of a external link.
+    :type content_url: str
+    :param content: The content of the attachment, in case of a code snippet,
+     email, or file.
+    :type content: object
+    :param name: The plaintext display name of the attachment.
+    :type name: str
+    :param thumbnail_url: The url of a thumbnail image that might be embedded
+     in the attachment, in case of a card.
+    :type thumbnail_url: str
+    """
+
+    _attribute_map = {
+        "id": {"key": "id", "type": "str"},
+        "content_type": {"key": "contentType", "type": "str"},
+        "content_url": {"key": "contentUrl", "type": "str"},
+        "content": {"key": "content", "type": "object"},
+        "name": {"key": "name", "type": "str"},
+        "thumbnail_url": {"key": "thumbnailUrl", "type": "str"},
+    }
+
+    def __init__(
+        self,
+        *,
+        id: str = None,
+        content_type: str = None,
+        content_url: str = None,
+        content=None,
+        name: str = None,
+        thumbnail_url: str = None,
+        **kwargs
+    ) -> None:
+        super(MessageActionsPayloadAttachment, self).__init__(**kwargs)
+        self.id = id
+        self.content_type = content_type
+        self.content_url = content_url
+        self.content = content
+        self.name = name
+        self.thumbnail_url = thumbnail_url
+
+
+class MessageActionsPayloadBody(Model):
+    """Plaintext/HTML representation of the content of the message.
+
+    :param content_type: Type of the content. Possible values include: 'html',
+     'text'
+    :type content_type: str
+    :param content: The content of the body.
+    :type content: str
+    """
+
+    _attribute_map = {
+        "content_type": {"key": "contentType", "type": "str"},
+        "content": {"key": "content", "type": "str"},
+    }
+
+    def __init__(self, *, content_type=None, content: str = None, **kwargs) -> None:
+        super(MessageActionsPayloadBody, self).__init__(**kwargs)
+        self.content_type = content_type
+        self.content = content
+
+
+class MessageActionsPayloadConversation(Model):
+    """Represents a team or channel entity.
+
+    :param conversation_identity_type: The type of conversation, whether a
+     team or channel. Possible values include: 'team', 'channel'
+    :type conversation_identity_type: str or
+     ~botframework.connector.teams.models.enum
+    :param id: The id of the team or channel.
+    :type id: str
+    :param display_name: The plaintext display name of the team or channel
+     entity.
+    :type display_name: str
+    """
+
+    _attribute_map = {
+        "conversation_identity_type": {
+            "key": "conversationIdentityType",
+            "type": "str",
+        },
+        "id": {"key": "id", "type": "str"},
+        "display_name": {"key": "displayName", "type": "str"},
+    }
+
+    def __init__(
+        self,
+        *,
+        conversation_identity_type=None,
+        id: str = None,
+        display_name: str = None,
+        **kwargs
+    ) -> None:
+        super(MessageActionsPayloadConversation, self).__init__(**kwargs)
+        self.conversation_identity_type = conversation_identity_type
+        self.id = id
+        self.display_name = display_name
+
+
+class MessageActionsPayloadFrom(Model):
+    """Represents a user, application, or conversation type that either sent or
+    was referenced in a message.
+
+    :param user: Represents details of the user.
+    :type user: ~botframework.connector.teams.models.MessageActionsPayloadUser
+    :param application: Represents details of the app.
+    :type application:
+     ~botframework.connector.teams.models.MessageActionsPayloadApp
+    :param conversation: Represents details of the converesation.
+    :type conversation:
+     ~botframework.connector.teams.models.MessageActionsPayloadConversation
+    """
+
+    _attribute_map = {
+        "user": {"key": "user", "type": "MessageActionsPayloadUser"},
+        "application": {"key": "application", "type": "MessageActionsPayloadApp"},
+        "conversation": {
+            "key": "conversation",
+            "type": "MessageActionsPayloadConversation",
+        },
+    }
+
+    def __init__(
+        self, *, user=None, application=None, conversation=None, **kwargs
+    ) -> None:
+        super(MessageActionsPayloadFrom, self).__init__(**kwargs)
+        self.user = user
+        self.application = application
+        self.conversation = conversation
+
+
+class MessageActionsPayloadMention(Model):
+    """Represents the entity that was mentioned in the message.
+
+    :param id: The id of the mentioned entity.
+    :type id: int
+    :param mention_text: The plaintext display name of the mentioned entity.
+    :type mention_text: str
+    :param mentioned: Provides more details on the mentioned entity.
+    :type mentioned:
+     ~botframework.connector.teams.models.MessageActionsPayloadFrom
+    """
+
+    _attribute_map = {
+        "id": {"key": "id", "type": "int"},
+        "mention_text": {"key": "mentionText", "type": "str"},
+        "mentioned": {"key": "mentioned", "type": "MessageActionsPayloadFrom"},
+    }
+
+    def __init__(
+        self, *, id: int = None, mention_text: str = None, mentioned=None, **kwargs
+    ) -> None:
+        super(MessageActionsPayloadMention, self).__init__(**kwargs)
+        self.id = id
+        self.mention_text = mention_text
+        self.mentioned = mentioned
+
+
+class MessageActionsPayloadReaction(Model):
+    """Represents the reaction of a user to a message.
+
+    :param reaction_type: The type of reaction given to the message. Possible
+     values include: 'like', 'heart', 'laugh', 'surprised', 'sad', 'angry'
+    :type reaction_type: str
+    :param created_date_time: Timestamp of when the user reacted to the
+     message.
+    :type created_date_time: str
+    :param user: The user with which the reaction is associated.
+    :type user: ~botframework.connector.teams.models.MessageActionsPayloadFrom
+    """
+
+    _attribute_map = {
+        "reaction_type": {"key": "reactionType", "type": "str"},
+        "created_date_time": {"key": "createdDateTime", "type": "str"},
+        "user": {"key": "user", "type": "MessageActionsPayloadFrom"},
+    }
+
+    def __init__(
+        self, *, reaction_type=None, created_date_time: str = None, user=None, **kwargs
+    ) -> None:
+        super(MessageActionsPayloadReaction, self).__init__(**kwargs)
+        self.reaction_type = reaction_type
+        self.created_date_time = created_date_time
+        self.user = user
+
+
+class MessageActionsPayloadUser(Model):
+    """Represents a user entity.
+
+    :param user_identity_type: The identity type of the user. Possible values
+     include: 'aadUser', 'onPremiseAadUser', 'anonymousGuest', 'federatedUser'
+    :type user_identity_type: str
+    :param id: The id of the user.
+    :type id: str
+    :param display_name: The plaintext display name of the user.
+    :type display_name: str
+    """
+
+    _attribute_map = {
+        "user_identity_type": {"key": "userIdentityType", "type": "str"},
+        "id": {"key": "id", "type": "str"},
+        "display_name": {"key": "displayName", "type": "str"},
+    }
+
+    def __init__(
+        self,
+        *,
+        user_identity_type=None,
+        id: str = None,
+        display_name: str = None,
+        **kwargs
+    ) -> None:
+        super(MessageActionsPayloadUser, self).__init__(**kwargs)
+        self.user_identity_type = user_identity_type
+        self.id = id
+        self.display_name = display_name
+
+
+class MessageActionsPayload(Model):
+    """Represents the individual message within a chat or channel where a message
+    actions is taken.
+
+    :param id: Unique id of the message.
+    :type id: str
+    :param reply_to_id: Id of the parent/root message of the thread.
+    :type reply_to_id: str
+    :param message_type: Type of message - automatically set to message.
+     Possible values include: 'message'
+    :type message_type: str
+    :param created_date_time: Timestamp of when the message was created.
+    :type created_date_time: str
+    :param last_modified_date_time: Timestamp of when the message was edited
+     or updated.
+    :type last_modified_date_time: str
+    :param deleted: Indicates whether a message has been soft deleted.
+    :type deleted: bool
+    :param subject: Subject line of the message.
+    :type subject: str
+    :param summary: Summary text of the message that could be used for
+     notifications.
+    :type summary: str
+    :param importance: The importance of the message. Possible values include:
+     'normal', 'high', 'urgent'
+    :type importance: str
+    :param locale: Locale of the message set by the client.
+    :type locale: str
+    :param link_to_message: Link back to the message.
+    :type link_to_message: str
+    :param from_property: Sender of the message.
+    :type from_property:
+     ~botframework.connector.teams.models.MessageActionsPayloadFrom
+    :param body: Plaintext/HTML representation of the content of the message.
+    :type body: ~botframework.connector.teams.models.MessageActionsPayloadBody
+    :param attachment_layout: How the attachment(s) are displayed in the
+     message.
+    :type attachment_layout: str
+    :param attachments: Attachments in the message - card, image, file, etc.
+    :type attachments:
+     list[~botframework.connector.teams.models.MessageActionsPayloadAttachment]
+    :param mentions: List of entities mentioned in the message.
+    :type mentions:
+     list[~botframework.connector.teams.models.MessageActionsPayloadMention]
+    :param reactions: Reactions for the message.
+    :type reactions:
+     list[~botframework.connector.teams.models.MessageActionsPayloadReaction]
+    """
+
+    _attribute_map = {
+        "id": {"key": "id", "type": "str"},
+        "reply_to_id": {"key": "replyToId", "type": "str"},
+        "message_type": {"key": "messageType", "type": "str"},
+        "created_date_time": {"key": "createdDateTime", "type": "str"},
+        "last_modified_date_time": {"key": "lastModifiedDateTime", "type": "str"},
+        "deleted": {"key": "deleted", "type": "bool"},
+        "subject": {"key": "subject", "type": "str"},
+        "summary": {"key": "summary", "type": "str"},
+        "importance": {"key": "importance", "type": "str"},
+        "locale": {"key": "locale", "type": "str"},
+        "link_to_message": {"key": "linkToMessage", "type": "str"},
+        "from_property": {"key": "from", "type": "MessageActionsPayloadFrom"},
+        "body": {"key": "body", "type": "MessageActionsPayloadBody"},
+        "attachment_layout": {"key": "attachmentLayout", "type": "str"},
+        "attachments": {
+            "key": "attachments",
+            "type": "[MessageActionsPayloadAttachment]",
+        },
+        "mentions": {"key": "mentions", "type": "[MessageActionsPayloadMention]"},
+        "reactions": {"key": "reactions", "type": "[MessageActionsPayloadReaction]"},
+    }
+
+    def __init__(
+        self,
+        *,
+        id: str = None,
+        reply_to_id: str = None,
+        message_type=None,
+        created_date_time: str = None,
+        last_modified_date_time: str = None,
+        deleted: bool = None,
+        subject: str = None,
+        summary: str = None,
+        importance=None,
+        locale: str = None,
+        link_to_message: str = None,
+        from_property=None,
+        body=None,
+        attachment_layout: str = None,
+        attachments=None,
+        mentions=None,
+        reactions=None,
+        **kwargs
+    ) -> None:
+        super(MessageActionsPayload, self).__init__(**kwargs)
+        self.id = id
+        self.reply_to_id = reply_to_id
+        self.message_type = message_type
+        self.created_date_time = created_date_time
+        self.last_modified_date_time = last_modified_date_time
+        self.deleted = deleted
+        self.subject = subject
+        self.summary = summary
+        self.importance = importance
+        self.locale = locale
+        self.link_to_message = link_to_message
+        self.from_property = from_property
+        self.body = body
+        self.attachment_layout = attachment_layout
+        self.attachments = attachments
+        self.mentions = mentions
+        self.reactions = reactions
+
+
+class MessagingExtensionAction(TaskModuleRequest):
+    """Messaging extension action.
+
+    :param data: User input data. Free payload with key-value pairs.
+    :type data: object
+    :param context: Current user context, i.e., the current theme
+    :type context:
+     ~botframework.connector.teams.models.TaskModuleRequestContext
+    :param command_id: Id of the command assigned by Bot
+    :type command_id: str
+    :param command_context: The context from which the command originates.
+     Possible values include: 'message', 'compose', 'commandbox'
+    :type command_context: str
+    :param bot_message_preview_action: Bot message preview action taken by
+     user. Possible values include: 'edit', 'send'
+    :type bot_message_preview_action: str or
+     ~botframework.connector.teams.models.enum
+    :param bot_activity_preview:
+    :type bot_activity_preview:
+     list[~botframework.schema.models.Activity]
+    :param message_payload: Message content sent as part of the command
+     request.
+    :type message_payload:
+     ~botframework.connector.teams.models.MessageActionsPayload
+    """
+
+    _attribute_map = {
+        "data": {"key": "data", "type": "object"},
+        "context": {"key": "context", "type": "TaskModuleRequestContext"},
+        "command_id": {"key": "commandId", "type": "str"},
+        "command_context": {"key": "commandContext", "type": "str"},
+        "bot_message_preview_action": {"key": "botMessagePreviewAction", "type": "str"},
+        "bot_activity_preview": {"key": "botActivityPreview", "type": "[Activity]"},
+        "message_payload": {"key": "messagePayload", "type": "MessageActionsPayload"},
+    }
+
+    def __init__(
+        self,
+        *,
+        data=None,
+        context=None,
+        command_id: str = None,
+        command_context=None,
+        bot_message_preview_action=None,
+        bot_activity_preview=None,
+        message_payload=None,
+        **kwargs
+    ) -> None:
+        super(MessagingExtensionAction, self).__init__(
+            data=data, context=context, **kwargs
+        )
+        self.command_id = command_id
+        self.command_context = command_context
+        self.bot_message_preview_action = bot_message_preview_action
+        self.bot_activity_preview = bot_activity_preview
+        self.message_payload = message_payload
+
+
+class MessagingExtensionActionResponse(Model):
+    """Response of messaging extension action.
+
+    :param task: The JSON for the Adaptive card to appear in the task module.
+    :type task: ~botframework.connector.teams.models.TaskModuleResponseBase
+    :param compose_extension:
+    :type compose_extension:
+     ~botframework.connector.teams.models.MessagingExtensionResult
+    :param cache_info: CacheInfo for this MessagingExtensionActionResponse.
+    :type cache_info: ~botframework.connector.teams.models.CacheInfo
+    """
+
+    _attribute_map = {
+        "task": {"key": "task", "type": "TaskModuleResponseBase"},
+        "compose_extension": {
+            "key": "composeExtension",
+            "type": "MessagingExtensionResult",
+        },
+        "cache_info": {"key": "cacheInfo", "type": "CacheInfo"},
+    }
+
+    def __init__(
+        self,
+        *,
+        task=None,
+        compose_extension=None,
+        cache_info: CacheInfo = None,
+        **kwargs
+    ) -> None:
+        super(MessagingExtensionActionResponse, self).__init__(**kwargs)
+        self.task = task
+        self.compose_extension = compose_extension
+        self.cache_info = cache_info
+
+
+class MessagingExtensionAttachment(Attachment):
+    """Messaging extension attachment.
+
+    :param content_type: mimetype/Contenttype for the file
+    :type content_type: str
+    :param content_url: Content Url
+    :type content_url: str
+    :param content: Embedded content
+    :type content: object
+    :param name: (OPTIONAL) The name of the attachment
+    :type name: str
+    :param thumbnail_url: (OPTIONAL) Thumbnail associated with attachment
+    :type thumbnail_url: str
+    :param preview:
+    :type preview: ~botframework.connector.teams.models.Attachment
+    """
+
+    _attribute_map = {
+        "content_type": {"key": "contentType", "type": "str"},
+        "content_url": {"key": "contentUrl", "type": "str"},
+        "content": {"key": "content", "type": "object"},
+        "name": {"key": "name", "type": "str"},
+        "thumbnail_url": {"key": "thumbnailUrl", "type": "str"},
+        "preview": {"key": "preview", "type": "Attachment"},
+    }
+
+    def __init__(
+        self,
+        *,
+        content_type: str = None,
+        content_url: str = None,
+        content=None,
+        name: str = None,
+        thumbnail_url: str = None,
+        preview=None,
+        **kwargs
+    ) -> None:
+        super(MessagingExtensionAttachment, self).__init__(
+            content_type=content_type,
+            content_url=content_url,
+            content=content,
+            name=name,
+            thumbnail_url=thumbnail_url,
+            **kwargs
+        )
+        self.preview = preview
+
+
+class MessagingExtensionParameter(Model):
+    """Messaging extension query parameters.
+
+    :param name: Name of the parameter
+    :type name: str
+    :param value: Value of the parameter
+    :type value: object
+    """
+
+    _attribute_map = {
+        "name": {"key": "name", "type": "str"},
+        "value": {"key": "value", "type": "object"},
+    }
+
+    def __init__(self, *, name: str = None, value=None, **kwargs) -> None:
+        super(MessagingExtensionParameter, self).__init__(**kwargs)
+        self.name = name
+        self.value = value
+
+
+class MessagingExtensionQuery(Model):
+    """Messaging extension query.
+
+    :param command_id: Id of the command assigned by Bot
+    :type command_id: str
+    :param parameters: Parameters for the query
+    :type parameters:
+     list[~botframework.connector.teams.models.MessagingExtensionParameter]
+    :param query_options:
+    :type query_options:
+     ~botframework.connector.teams.models.MessagingExtensionQueryOptions
+    :param state: State parameter passed back to the bot after
+     authentication/configuration flow
+    :type state: str
+    """
+
+    _attribute_map = {
+        "command_id": {"key": "commandId", "type": "str"},
+        "parameters": {"key": "parameters", "type": "[MessagingExtensionParameter]"},
+        "query_options": {
+            "key": "queryOptions",
+            "type": "MessagingExtensionQueryOptions",
+        },
+        "state": {"key": "state", "type": "str"},
+    }
+
+    def __init__(
+        self,
+        *,
+        command_id: str = None,
+        parameters=None,
+        query_options=None,
+        state: str = None,
+        **kwargs
+    ) -> None:
+        super(MessagingExtensionQuery, self).__init__(**kwargs)
+        self.command_id = command_id
+        self.parameters = parameters
+        self.query_options = query_options
+        self.state = state
+
+
+class MessagingExtensionQueryOptions(Model):
+    """Messaging extension query options.
+
+    :param skip: Number of entities to skip
+    :type skip: int
+    :param count: Number of entities to fetch
+    :type count: int
+    """
+
+    _attribute_map = {
+        "skip": {"key": "skip", "type": "int"},
+        "count": {"key": "count", "type": "int"},
+    }
+
+    def __init__(self, *, skip: int = None, count: int = None, **kwargs) -> None:
+        super(MessagingExtensionQueryOptions, self).__init__(**kwargs)
+        self.skip = skip
+        self.count = count
+
+
+class MessagingExtensionResponse(Model):
+    """Messaging extension response.
+
+    :param compose_extension:
+    :type compose_extension: ~botframework.connector.teams.models.MessagingExtensionResult
+    :param cache_info: CacheInfo for this MessagingExtensionResponse.
+    :type cache_info: ~botframework.connector.teams.models.CacheInfo
+    """
+
+    _attribute_map = {
+        "compose_extension": {
+            "key": "composeExtension",
+            "type": "MessagingExtensionResult",
+        },
+        "cache_info": {"key": "cacheInfo", "type": CacheInfo},
+    }
+
+    def __init__(self, *, compose_extension=None, cache_info=None, **kwargs) -> None:
+        super(MessagingExtensionResponse, self).__init__(**kwargs)
+        self.compose_extension = compose_extension
+        self.cache_info = cache_info
+
+
+class MessagingExtensionResult(Model):
+    """Messaging extension result.
+
+    :param attachment_layout: Hint for how to deal with multiple attachments.
+     Possible values include: 'list', 'grid'
+    :type attachment_layout: str
+    :param type: The type of the result. Possible values include: 'result',
+     'auth', 'config', 'message', 'botMessagePreview'
+    :type type: str
+    :param attachments: (Only when type is result) Attachments
+    :type attachments:
+     list[~botframework.connector.teams.models.MessagingExtensionAttachment]
+    :param suggested_actions:
+    :type suggested_actions:
+     ~botframework.connector.teams.models.MessagingExtensionSuggestedAction
+    :param text: (Only when type is message) Text
+    :type text: str
+    :param activity_preview: (Only when type is botMessagePreview) Message
+     activity to preview
+    :type activity_preview: ~botframework.connector.teams.models.Activity
+    """
+
+    _attribute_map = {
+        "attachment_layout": {"key": "attachmentLayout", "type": "str"},
+        "type": {"key": "type", "type": "str"},
+        "attachments": {"key": "attachments", "type": "[MessagingExtensionAttachment]"},
+        "suggested_actions": {
+            "key": "suggestedActions",
+            "type": "MessagingExtensionSuggestedAction",
+        },
+        "text": {"key": "text", "type": "str"},
+        "activity_preview": {"key": "activityPreview", "type": "Activity"},
+    }
+
+    def __init__(
+        self,
+        *,
+        attachment_layout=None,
+        type=None,
+        attachments=None,
+        suggested_actions=None,
+        text: str = None,
+        activity_preview=None,
+        **kwargs
+    ) -> None:
+        super(MessagingExtensionResult, self).__init__(**kwargs)
+        self.attachment_layout = attachment_layout
+        self.type = type
+        self.attachments = attachments
+        self.suggested_actions = suggested_actions
+        self.text = text
+        self.activity_preview = activity_preview
+
+
+class MessagingExtensionSuggestedAction(Model):
+    """Messaging extension Actions (Only when type is auth or config).
+
+    :param actions: Actions
+    :type actions: list[~botframework.connector.teams.models.CardAction]
+    """
+
+    _attribute_map = {
+        "actions": {"key": "actions", "type": "[CardAction]"},
+    }
+
+    def __init__(self, *, actions=None, **kwargs) -> None:
+        super(MessagingExtensionSuggestedAction, self).__init__(**kwargs)
+        self.actions = actions
+
+
+class NotificationInfo(Model):
+    """Specifies if a notification is to be sent for the mentions.
+
+    :param alert: true if notification is to be sent to the user, false
+     otherwise.
+    :type alert: bool
+    """
+
+    _attribute_map = {
+        "alert": {"key": "alert", "type": "bool"},
+        "alert_in_meeting": {"key": "alertInMeeting", "type": "bool"},
+        "external_resource_url": {"key": "externalResourceUrl", "type": "str"},
+    }
+
+    def __init__(
+        self,
+        *,
+        alert: bool = None,
+        alert_in_meeting: bool = None,
+        external_resource_url: str = None,
+        **kwargs
+    ) -> None:
+        super(NotificationInfo, self).__init__(**kwargs)
+        self.alert = alert
+        self.alert_in_meeting = alert_in_meeting
+        self.external_resource_url = external_resource_url
+
+
+class O365ConnectorCard(Model):
+    """O365 connector card.
+
+    :param title: Title of the item
+    :type title: str
+    :param text: Text for the card
+    :type text: str
+    :param summary: Summary for the card
+    :type summary: str
+    :param theme_color: Theme color for the card
+    :type theme_color: str
+    :param sections: Set of sections for the current card
+    :type sections:
+     list[~botframework.connector.teams.models.O365ConnectorCardSection]
+    :param potential_action: Set of actions for the current card
+    :type potential_action:
+     list[~botframework.connector.teams.models.O365ConnectorCardActionBase]
+    """
+
+    _attribute_map = {
+        "title": {"key": "title", "type": "str"},
+        "text": {"key": "text", "type": "str"},
+        "summary": {"key": "summary", "type": "str"},
+        "theme_color": {"key": "themeColor", "type": "str"},
+        "sections": {"key": "sections", "type": "[O365ConnectorCardSection]"},
+        "potential_action": {
+            "key": "potentialAction",
+            "type": "[O365ConnectorCardActionBase]",
+        },
+    }
+
+    def __init__(
+        self,
+        *,
+        title: str = None,
+        text: str = None,
+        summary: str = None,
+        theme_color: str = None,
+        sections=None,
+        potential_action=None,
+        **kwargs
+    ) -> None:
+        super(O365ConnectorCard, self).__init__(**kwargs)
+        self.title = title
+        self.text = text
+        self.summary = summary
+        self.theme_color = theme_color
+        self.sections = sections
+        self.potential_action = potential_action
+
+
+class O365ConnectorCardInputBase(Model):
+    """O365 connector card input for ActionCard action.
+
+    :param type: Input type name. Possible values include: 'textInput',
+     'dateInput', 'multichoiceInput'
+    :type type: str
+    :param id: Input Id. It must be unique per entire O365 connector card.
+    :type id: str
+    :param is_required: Define if this input is a required field. Default
+     value is false.
+    :type is_required: bool
+    :param title: Input title that will be shown as the placeholder
+    :type title: str
+    :param value: Default value for this input field
+    :type value: str
+    """
+
+    _attribute_map = {
+        "type": {"key": "@type", "type": "str"},
+        "id": {"key": "id", "type": "str"},
+        "is_required": {"key": "isRequired", "type": "bool"},
+        "title": {"key": "title", "type": "str"},
+        "value": {"key": "value", "type": "str"},
+    }
+
+    def __init__(
+        self,
+        *,
+        type=None,
+        id: str = None,
+        is_required: bool = None,
+        title: str = None,
+        value: str = None,
+        **kwargs
+    ) -> None:
+        super(O365ConnectorCardInputBase, self).__init__(**kwargs)
+        self.type = type
+        self.id = id
+        self.is_required = is_required
+        self.title = title
+        self.value = value
+
+
+class O365ConnectorCardActionBase(Model):
+    """O365 connector card action base.
+
+    :param type: Type of the action. Possible values include: 'ViewAction',
+     'OpenUri', 'HttpPOST', 'ActionCard'
+    :type type: str
+    :param name: Name of the action that will be used as button title
+    :type name: str
+    :param id: Action Id
+    :type id: str
+    """
+
+    _attribute_map = {
+        "type": {"key": "@type", "type": "str"},
+        "name": {"key": "name", "type": "str"},
+        "id": {"key": "@id", "type": "str"},
+    }
+
+    def __init__(
+        self, *, type=None, name: str = None, id: str = None, **kwargs
+    ) -> None:
+        super(O365ConnectorCardActionBase, self).__init__(**kwargs)
+        self.type = type
+        self.name = name
+        self.id = id
+
+
+class O365ConnectorCardActionCard(O365ConnectorCardActionBase):
+    """O365 connector card ActionCard action.
+
+    :param type: Type of the action. Possible values include: 'ViewAction',
+     'OpenUri', 'HttpPOST', 'ActionCard'
+    :type type: str
+    :param name: Name of the action that will be used as button title
+    :type name: str
+    :param id: Action Id
+    :type id: str
+    :param inputs: Set of inputs contained in this ActionCard whose each item
+     can be in any subtype of O365ConnectorCardInputBase
+    :type inputs:
+     list[~botframework.connector.teams.models.O365ConnectorCardInputBase]
+    :param actions: Set of actions contained in this ActionCard whose each
+     item can be in any subtype of O365ConnectorCardActionBase except
+     O365ConnectorCardActionCard, as nested ActionCard is forbidden.
+    :type actions:
+     list[~botframework.connector.teams.models.O365ConnectorCardActionBase]
+    """
+
+    _attribute_map = {
+        "type": {"key": "@type", "type": "str"},
+        "name": {"key": "name", "type": "str"},
+        "id": {"key": "@id", "type": "str"},
+        "inputs": {"key": "inputs", "type": "[O365ConnectorCardInputBase]"},
+        "actions": {"key": "actions", "type": "[O365ConnectorCardActionBase]"},
+    }
+
+    def __init__(
+        self,
+        *,
+        type=None,
+        name: str = None,
+        id: str = None,
+        inputs=None,
+        actions=None,
+        **kwargs
+    ) -> None:
+        super(O365ConnectorCardActionCard, self).__init__(
+            type=type, name=name, id=id, **kwargs
+        )
+        self.inputs = inputs
+        self.actions = actions
+
+
+class O365ConnectorCardActionQuery(Model):
+    """O365 connector card HttpPOST invoke query.
+
+    :param body: The results of body string defined in
+     IO365ConnectorCardHttpPOST with substituted input values
+    :type body: str
+    :param action_id: Action Id associated with the HttpPOST action button
+     triggered, defined in O365ConnectorCardActionBase.
+    :type action_id: str
+    """
+
+    _attribute_map = {
+        "body": {"key": "body", "type": "str"},
+        "action_id": {"key": "actionId", "type": "str"},
+    }
+
+    def __init__(self, *, body: str = None, actionId: str = None, **kwargs) -> None:
+        super(O365ConnectorCardActionQuery, self).__init__(**kwargs)
+        self.body = body
+        # This is how it comes in from Teams
+        self.action_id = actionId
+
+
+class O365ConnectorCardDateInput(O365ConnectorCardInputBase):
+    """O365 connector card date input.
+
+    :param type: Input type name. Possible values include: 'textInput',
+     'dateInput', 'multichoiceInput'
+    :type type: str
+    :param id: Input Id. It must be unique per entire O365 connector card.
+    :type id: str
+    :param is_required: Define if this input is a required field. Default
+     value is false.
+    :type is_required: bool
+    :param title: Input title that will be shown as the placeholder
+    :type title: str
+    :param value: Default value for this input field
+    :type value: str
+    :param include_time: Include time input field. Default value  is false
+     (date only).
+    :type include_time: bool
+    """
+
+    _attribute_map = {
+        "type": {"key": "@type", "type": "str"},
+        "id": {"key": "id", "type": "str"},
+        "is_required": {"key": "isRequired", "type": "bool"},
+        "title": {"key": "title", "type": "str"},
+        "value": {"key": "value", "type": "str"},
+        "include_time": {"key": "includeTime", "type": "bool"},
+    }
+
+    def __init__(
+        self,
+        *,
+        type=None,
+        id: str = None,
+        is_required: bool = None,
+        title: str = None,
+        value: str = None,
+        include_time: bool = None,
+        **kwargs
+    ) -> None:
+        super(O365ConnectorCardDateInput, self).__init__(
+            type=type,
+            id=id,
+            is_required=is_required,
+            title=title,
+            value=value,
+            **kwargs
+        )
+        self.include_time = include_time
+
+
+class O365ConnectorCardFact(Model):
+    """O365 connector card fact.
+
+    :param name: Display name of the fact
+    :type name: str
+    :param value: Display value for the fact
+    :type value: str
+    """
+
+    _attribute_map = {
+        "name": {"key": "name", "type": "str"},
+        "value": {"key": "value", "type": "str"},
+    }
+
+    def __init__(self, *, name: str = None, value: str = None, **kwargs) -> None:
+        super(O365ConnectorCardFact, self).__init__(**kwargs)
+        self.name = name
+        self.value = value
+
+
+class O365ConnectorCardHttpPOST(O365ConnectorCardActionBase):
+    """O365 connector card HttpPOST action.
+
+    :param type: Type of the action. Possible values include: 'ViewAction',
+     'OpenUri', 'HttpPOST', 'ActionCard'
+    :type type: str
+    :param name: Name of the action that will be used as button title
+    :type name: str
+    :param id: Action Id
+    :type id: str
+    :param body: Content to be posted back to bots via invoke
+    :type body: str
+    """
+
+    _attribute_map = {
+        "type": {"key": "@type", "type": "str"},
+        "name": {"key": "name", "type": "str"},
+        "id": {"key": "@id", "type": "str"},
+        "body": {"key": "body", "type": "str"},
+    }
+
+    def __init__(
+        self, *, type=None, name: str = None, id: str = None, body: str = None, **kwargs
+    ) -> None:
+        super(O365ConnectorCardHttpPOST, self).__init__(
+            type=type, name=name, id=id, **kwargs
+        )
+        self.body = body
+
+
+class O365ConnectorCardImage(Model):
+    """O365 connector card image.
+
+    :param image: URL for the image
+    :type image: str
+    :param title: Alternative text for the image
+    :type title: str
+    """
+
+    _attribute_map = {
+        "image": {"key": "image", "type": "str"},
+        "title": {"key": "title", "type": "str"},
+    }
+
+    def __init__(self, *, image: str = None, title: str = None, **kwargs) -> None:
+        super(O365ConnectorCardImage, self).__init__(**kwargs)
+        self.image = image
+        self.title = title
+
+
+class O365ConnectorCardMultichoiceInput(O365ConnectorCardInputBase):
+    """O365 connector card multiple choice input.
+
+    :param type: Input type name. Possible values include: 'textInput',
+     'dateInput', 'multichoiceInput'
+    :type type: str
+    :param id: Input Id. It must be unique per entire O365 connector card.
+    :type id: str
+    :param is_required: Define if this input is a required field. Default
+     value is false.
+    :type is_required: bool
+    :param title: Input title that will be shown as the placeholder
+    :type title: str
+    :param value: Default value for this input field
+    :type value: str
+    :param choices: Set of choices whose each item can be in any subtype of
+     O365ConnectorCardMultichoiceInputChoice.
+    :type choices:
+     list[~botframework.connector.teams.models.O365ConnectorCardMultichoiceInputChoice]
+    :param style: Choice item rendering style. Default value is 'compact'.
+     Possible values include: 'compact', 'expanded'
+    :type style: str
+    :param is_multi_select: Define if this input field allows multiple
+     selections. Default value is false.
+    :type is_multi_select: bool
+    """
+
+    _attribute_map = {
+        "type": {"key": "@type", "type": "str"},
+        "id": {"key": "id", "type": "str"},
+        "is_required": {"key": "isRequired", "type": "bool"},
+        "title": {"key": "title", "type": "str"},
+        "value": {"key": "value", "type": "str"},
+        "choices": {
+            "key": "choices",
+            "type": "[O365ConnectorCardMultichoiceInputChoice]",
+        },
+        "style": {"key": "style", "type": "str"},
+        "is_multi_select": {"key": "isMultiSelect", "type": "bool"},
+    }
+
+    def __init__(
+        self,
+        *,
+        type=None,
+        id: str = None,
+        is_required: bool = None,
+        title: str = None,
+        value: str = None,
+        choices=None,
+        style=None,
+        is_multi_select: bool = None,
+        **kwargs
+    ) -> None:
+        super(O365ConnectorCardMultichoiceInput, self).__init__(
+            type=type,
+            id=id,
+            is_required=is_required,
+            title=title,
+            value=value,
+            **kwargs
+        )
+        self.choices = choices
+        self.style = style
+        self.is_multi_select = is_multi_select
+
+
+class O365ConnectorCardMultichoiceInputChoice(Model):
+    """O365O365 connector card multiple choice input item.
+
+    :param display: The text rendered on ActionCard.
+    :type display: str
+    :param value: The value received as results.
+    :type value: str
+    """
+
+    _attribute_map = {
+        "display": {"key": "display", "type": "str"},
+        "value": {"key": "value", "type": "str"},
+    }
+
+    def __init__(self, *, display: str = None, value: str = None, **kwargs) -> None:
+        super(O365ConnectorCardMultichoiceInputChoice, self).__init__(**kwargs)
+        self.display = display
+        self.value = value
+
+
+class O365ConnectorCardOpenUri(O365ConnectorCardActionBase):
+    """O365 connector card OpenUri action.
+
+    :param type: Type of the action. Possible values include: 'ViewAction',
+     'OpenUri', 'HttpPOST', 'ActionCard'
+    :type type: str
+    :param name: Name of the action that will be used as button title
+    :type name: str
+    :param id: Action Id
+    :type id: str
+    :param targets: Target os / urls
+    :type targets:
+     list[~botframework.connector.teams.models.O365ConnectorCardOpenUriTarget]
+    """
+
+    _attribute_map = {
+        "type": {"key": "@type", "type": "str"},
+        "name": {"key": "name", "type": "str"},
+        "id": {"key": "@id", "type": "str"},
+        "targets": {"key": "targets", "type": "[O365ConnectorCardOpenUriTarget]"},
+    }
+
+    def __init__(
+        self, *, type=None, name: str = None, id: str = None, targets=None, **kwargs
+    ) -> None:
+        super(O365ConnectorCardOpenUri, self).__init__(
+            type=type, name=name, id=id, **kwargs
+        )
+        self.targets = targets
+
+
+class O365ConnectorCardOpenUriTarget(Model):
+    """O365 connector card OpenUri target.
+
+    :param os: Target operating system. Possible values include: 'default',
+     'iOS', 'android', 'windows'
+    :type os: str
+    :param uri: Target url
+    :type uri: str
+    """
+
+    _attribute_map = {
+        "os": {"key": "os", "type": "str"},
+        "uri": {"key": "uri", "type": "str"},
+    }
+
+    def __init__(self, *, os=None, uri: str = None, **kwargs) -> None:
+        super(O365ConnectorCardOpenUriTarget, self).__init__(**kwargs)
+        self.os = os
+        self.uri = uri
+
+
+class O365ConnectorCardSection(Model):
+    """O365 connector card section.
+
+    :param title: Title of the section
+    :type title: str
+    :param text: Text for the section
+    :type text: str
+    :param activity_title: Activity title
+    :type activity_title: str
+    :param activity_subtitle: Activity subtitle
+    :type activity_subtitle: str
+    :param activity_text: Activity text
+    :type activity_text: str
+    :param activity_image: Activity image
+    :type activity_image: str
+    :param activity_image_type: Describes how Activity image is rendered.
+     Possible values include: 'avatar', 'article'
+    :type activity_image_type: str or
+     ~botframework.connector.teams.models.enum
+    :param markdown: Use markdown for all text contents. Default value is
+     true.
+    :type markdown: bool
+    :param facts: Set of facts for the current section
+    :type facts:
+     list[~botframework.connector.teams.models.O365ConnectorCardFact]
+    :param images: Set of images for the current section
+    :type images:
+     list[~botframework.connector.teams.models.O365ConnectorCardImage]
+    :param potential_action: Set of actions for the current section
+    :type potential_action:
+     list[~botframework.connector.teams.models.O365ConnectorCardActionBase]
+    """
+
+    _attribute_map = {
+        "title": {"key": "title", "type": "str"},
+        "text": {"key": "text", "type": "str"},
+        "activity_title": {"key": "activityTitle", "type": "str"},
+        "activity_subtitle": {"key": "activitySubtitle", "type": "str"},
+        "activity_text": {"key": "activityText", "type": "str"},
+        "activity_image": {"key": "activityImage", "type": "str"},
+        "activity_image_type": {"key": "activityImageType", "type": "str"},
+        "markdown": {"key": "markdown", "type": "bool"},
+        "facts": {"key": "facts", "type": "[O365ConnectorCardFact]"},
+        "images": {"key": "images", "type": "[O365ConnectorCardImage]"},
+        "potential_action": {
+            "key": "potentialAction",
+            "type": "[O365ConnectorCardActionBase]",
+        },
+    }
+
+    def __init__(
+        self,
+        *,
+        title: str = None,
+        text: str = None,
+        activity_title: str = None,
+        activity_subtitle: str = None,
+        activity_text: str = None,
+        activity_image: str = None,
+        activity_image_type=None,
+        markdown: bool = None,
+        facts=None,
+        images=None,
+        potential_action=None,
+        **kwargs
+    ) -> None:
+        super(O365ConnectorCardSection, self).__init__(**kwargs)
+        self.title = title
+        self.text = text
+        self.activity_title = activity_title
+        self.activity_subtitle = activity_subtitle
+        self.activity_text = activity_text
+        self.activity_image = activity_image
+        self.activity_image_type = activity_image_type
+        self.markdown = markdown
+        self.facts = facts
+        self.images = images
+        self.potential_action = potential_action
+
+
+class O365ConnectorCardTextInput(O365ConnectorCardInputBase):
+    """O365 connector card text input.
+
+    :param type: Input type name. Possible values include: 'textInput',
+     'dateInput', 'multichoiceInput'
+    :type type: str
+    :param id: Input Id. It must be unique per entire O365 connector card.
+    :type id: str
+    :param is_required: Define if this input is a required field. Default
+     value is false.
+    :type is_required: bool
+    :param title: Input title that will be shown as the placeholder
+    :type title: str
+    :param value: Default value for this input field
+    :type value: str
+    :param is_multiline: Define if text input is allowed for multiple lines.
+     Default value is false.
+    :type is_multiline: bool
+    :param max_length: Maximum length of text input. Default value is
+     unlimited.
+    :type max_length: float
+    """
+
+    _attribute_map = {
+        "type": {"key": "@type", "type": "str"},
+        "id": {"key": "id", "type": "str"},
+        "is_required": {"key": "isRequired", "type": "bool"},
+        "title": {"key": "title", "type": "str"},
+        "value": {"key": "value", "type": "str"},
+        "is_multiline": {"key": "isMultiline", "type": "bool"},
+        "max_length": {"key": "maxLength", "type": "float"},
+    }
+
+    def __init__(
+        self,
+        *,
+        type=None,
+        id: str = None,
+        is_required: bool = None,
+        title: str = None,
+        value: str = None,
+        is_multiline: bool = None,
+        max_length: float = None,
+        **kwargs
+    ) -> None:
+        super(O365ConnectorCardTextInput, self).__init__(
+            type=type,
+            id=id,
+            is_required=is_required,
+            title=title,
+            value=value,
+            **kwargs
+        )
+        self.is_multiline = is_multiline
+        self.max_length = max_length
+
+
+class O365ConnectorCardViewAction(O365ConnectorCardActionBase):
+    """O365 connector card ViewAction action.
+
+    :param type: Type of the action. Possible values include: 'ViewAction',
+     'OpenUri', 'HttpPOST', 'ActionCard'
+    :type type: str
+    :param name: Name of the action that will be used as button title
+    :type name: str
+    :param id: Action Id
+    :type id: str
+    :param target: Target urls, only the first url effective for card button
+    :type target: list[str]
+    """
+
+    _attribute_map = {
+        "type": {"key": "@type", "type": "str"},
+        "name": {"key": "name", "type": "str"},
+        "id": {"key": "@id", "type": "str"},
+        "target": {"key": "target", "type": "[str]"},
+    }
+
+    def __init__(
+        self, *, type=None, name: str = None, id: str = None, target=None, **kwargs
+    ) -> None:
+        super(O365ConnectorCardViewAction, self).__init__(
+            type=type, name=name, id=id, **kwargs
+        )
+        self.target = target
+
+
+class SigninStateVerificationQuery(Model):
+    """Signin state (part of signin action auth flow) verification invoke query.
+
+    :param state:  The state string originally received when the signin web
+     flow is finished with a state posted back to client via tab SDK
+     microsoftTeams.authentication.notifySuccess(state)
+    :type state: str
+    """
+
+    _attribute_map = {
+        "state": {"key": "state", "type": "str"},
+    }
+
+    def __init__(self, *, state: str = None, **kwargs) -> None:
+        super(SigninStateVerificationQuery, self).__init__(**kwargs)
+        self.state = state
+
+
+class TaskModuleResponseBase(Model):
+    """Base class for Task Module responses.
+
+    :param type: Choice of action options when responding to the task/submit
+     message. Possible values include: 'message', 'continue'
+    :type type: str
+    """
+
+    _attribute_map = {
+        "type": {"key": "type", "type": "str"},
+    }
+
+    def __init__(self, *, type=None, **kwargs) -> None:
+        super(TaskModuleResponseBase, self).__init__(**kwargs)
+        self.type = type
+
+
+class TaskModuleContinueResponse(TaskModuleResponseBase):
+    """Task Module Response with continue action.
+
+    :param value: The JSON for the Adaptive card to appear in the task module.
+    :type value: ~botframework.connector.teams.models.TaskModuleTaskInfo
+    """
+
+    _attribute_map = {
+        "type": {"key": "type", "type": "str"},
+        "value": {"key": "value", "type": "TaskModuleTaskInfo"},
+    }
+
+    def __init__(self, *, value=None, **kwargs) -> None:
+        super(TaskModuleContinueResponse, self).__init__(type="continue", **kwargs)
+        self.value = value
+
+
+class TaskModuleMessageResponse(TaskModuleResponseBase):
+    """Task Module response with message action.
+
+    :param value: Teams will display the value of value in a popup message
+     box.
+    :type value: str
+    """
+
+    _attribute_map = {
+        "type": {"key": "type", "type": "str"},
+        "value": {"key": "value", "type": "str"},
+    }
+
+    def __init__(self, *, value: str = None, **kwargs) -> None:
+        super(TaskModuleMessageResponse, self).__init__(type="message", **kwargs)
+        self.value = value
+
+
+class TaskModuleRequestContext(Model):
+    """Current user context, i.e., the current theme.
+
+    :param theme:
+    :type theme: str
+    """
+
+    _attribute_map = {
+        "theme": {"key": "theme", "type": "str"},
+    }
+
+    def __init__(self, *, theme: str = None, **kwargs) -> None:
+        super(TaskModuleRequestContext, self).__init__(**kwargs)
+        self.theme = theme
+
+
+class TaskModuleResponse(Model):
+    """Envelope for Task Module Response.
+
+    :param task: The JSON for the Adaptive card to appear in the task module.
+    :type task: ~botframework.connector.teams.models.TaskModuleResponseBase
+    :param cache_info: CacheInfo for this TaskModuleResponse.
+    :type cache_info: ~botframework.connector.teams.models.CacheInfo
+    """
+
+    _attribute_map = {
+        "task": {"key": "task", "type": "TaskModuleResponseBase"},
+        "cache_info": {"key": "cacheInfo", "type": "CacheInfo"},
+    }
+
+    def __init__(self, *, task=None, cache_info=None, **kwargs) -> None:
+        super(TaskModuleResponse, self).__init__(**kwargs)
+        self.task = task
+        self.cache_info = cache_info
+
+
+class TaskModuleTaskInfo(Model):
+    """Metadata for a Task Module.
+
+    :param title: Appears below the app name and to the right of the app icon.
+    :type title: str
+    :param height: This can be a number, representing the task module's height
+     in pixels, or a string, one of: small, medium, large.
+    :type height: object
+    :param width: This can be a number, representing the task module's width
+     in pixels, or a string, one of: small, medium, large.
+    :type width: object
+    :param url: The URL of what is loaded as an iframe inside the task module.
+     One of url or card is required.
+    :type url: str
+    :param card: The JSON for the Adaptive card to appear in the task module.
+    :type card: ~botframework.connector.teams.models.Attachment
+    :param fallback_url: If a client does not support the task module feature,
+     this URL is opened in a browser tab.
+    :type fallback_url: str
+    :param completion_bot_id: If a client does not support the task module
+     feature, this URL is opened in a browser tab.
+    :type completion_bot_id: str
+    """
+
+    _attribute_map = {
+        "title": {"key": "title", "type": "str"},
+        "height": {"key": "height", "type": "object"},
+        "width": {"key": "width", "type": "object"},
+        "url": {"key": "url", "type": "str"},
+        "card": {"key": "card", "type": "Attachment"},
+        "fallback_url": {"key": "fallbackUrl", "type": "str"},
+        "completion_bot_id": {"key": "completionBotId", "type": "str"},
+    }
+
+    def __init__(
+        self,
+        *,
+        title: str = None,
+        height=None,
+        width=None,
+        url: str = None,
+        card=None,
+        fallback_url: str = None,
+        completion_bot_id: str = None,
+        **kwargs
+    ) -> None:
+        super(TaskModuleTaskInfo, self).__init__(**kwargs)
+        self.title = title
+        self.height = height
+        self.width = width
+        self.url = url
+        self.card = card
+        self.fallback_url = fallback_url
+        self.completion_bot_id = completion_bot_id
+
+
+class TeamDetails(Model):
+    """Details related to a team.
+
+    :param id: Unique identifier representing a team
+    :type id: str
+    :param name: Name of team.
+    :type name: str
+    :param aad_group_id: Azure Active Directory (AAD) Group Id for the team.
+    :type aad_group_id: str
+    :param channel_count: The count of channels in the team.
+    :type channel_count: int
+    :param member_count: The count of members in the team.
+    :type member_count: int
+    """
+
+    _attribute_map = {
+        "id": {"key": "id", "type": "str"},
+        "name": {"key": "name", "type": "str"},
+        "aad_group_id": {"key": "aadGroupId", "type": "str"},
+        "channel_count": {"key": "channelCount", "type": "int"},
+        "member_count": {"key": "memberCount", "type": "int"},
+    }
+
+    def __init__(
+        self,
+        *,
+        id: str = None,
+        name: str = None,
+        aad_group_id: str = None,
+        member_count: int = None,
+        channel_count: int = None,
+        **kwargs
+    ) -> None:
+        super(TeamDetails, self).__init__(**kwargs)
+        self.id = id
+        self.name = name
+        self.aad_group_id = aad_group_id
+        self.channel_count = channel_count
+        self.member_count = member_count
+
+
+class TeamInfo(Model):
+    """Describes a team.
+
+    :param id: Unique identifier representing a team
+    :type id: str
+    :param name: Name of team.
+    :type name: str
+    :param name: Azure AD Teams group ID.
+    :type name: str
+    """
+
+    _attribute_map = {
+        "id": {"key": "id", "type": "str"},
+        "name": {"key": "name", "type": "str"},
+        "aad_group_id": {"key": "aadGroupId", "type": "str"},
+    }
+
+    def __init__(
+        self, *, id: str = None, name: str = None, aad_group_id: str = None, **kwargs
+    ) -> None:
+        super(TeamInfo, self).__init__(**kwargs)
+        self.id = id
+        self.name = name
+        self.aad_group_id = aad_group_id
+
+
+class TeamsChannelAccount(ChannelAccount):
+    """Teams channel account detailing user Azure Active Directory details.
+
+    :param id: Channel id for the user or bot on this channel (Example:
+     joe@smith.com, or @joesmith or 123456)
+    :type id: str
+    :param name: Display friendly name
+    :type name: str
+    :param given_name: Given name part of the user name.
+    :type given_name: str
+    :param surname: Surname part of the user name.
+    :type surname: str
+    :param email: Email Id of the user.
+    :type email: str
+    :param user_principal_name: Unique user principal name.
+    :type user_principal_name: str
+    :param tenant_id: Tenant Id of the user.
+    :type tenant_id: str
+    :param user_role: User Role of the user.
+    :type user_role: str
+    """
+
+    _attribute_map = {
+        "id": {"key": "id", "type": "str"},
+        "name": {"key": "name", "type": "str"},
+        "given_name": {"key": "givenName", "type": "str"},
+        "surname": {"key": "surname", "type": "str"},
+        "email": {"key": "email", "type": "str"},
+        "user_principal_name": {"key": "userPrincipalName", "type": "str"},
+        "aad_object_id": {"key": "objectId", "type": "str"},
+        "tenant_id": {"key": "tenantId", "type": "str"},
+        "user_role": {"key": "userRole", "type": "str"},
+    }
+
+    def __init__(
+        self,
+        *,
+        id: str = None,
+        name: str = None,
+        given_name: str = None,
+        surname: str = None,
+        email: str = None,
+        user_principal_name: str = None,
+        tenant_id: str = None,
+        user_role: str = None,
+        **kwargs
+    ) -> None:
+        super(TeamsChannelAccount, self).__init__(id=id, name=name, **kwargs)
+        self.given_name = given_name
+        self.surname = surname
+        self.email = email
+        self.user_principal_name = user_principal_name
+        self.tenant_id = tenant_id
+        self.user_role = user_role
+
+
+class TeamsPagedMembersResult(PagedMembersResult):
+    """Page of members for Teams.
+
+    :param continuation_token: Paging token
+    :type continuation_token: str
+    :param members: The Teams Channel Accounts.
+    :type members: list[~botframework.connector.models.TeamsChannelAccount]
+    """
+
+    _attribute_map = {
+        "continuation_token": {"key": "continuationToken", "type": "str"},
+        "members": {"key": "members", "type": "[TeamsChannelAccount]"},
+    }
+
+    def __init__(
+        self,
+        *,
+        continuation_token: str = None,
+        members: [TeamsChannelAccount] = None,
+        **kwargs
+    ) -> None:
+        super(TeamsPagedMembersResult, self).__init__(
+            continuation_token=continuation_token, members=members, **kwargs
+        )
+        self.continuation_token = continuation_token
+        self.members = members
+
+
+class TeamsChannelData(Model):
+    """Channel data specific to messages received in Microsoft Teams.
+
+    :param channel: Information about the channel in which the message was
+     sent
+    :type channel: ~botframework.connector.teams.models.ChannelInfo
+    :param event_type: Type of event.
+    :type event_type: str
+    :param team: Information about the team in which the message was sent
+    :type team: ~botframework.connector.teams.models.TeamInfo
+    :param notification: Notification settings for the message
+    :type notification: ~botframework.connector.teams.models.NotificationInfo
+    :param tenant: Information about the tenant in which the message was sent
+    :type tenant: ~botframework.connector.teams.models.TenantInfo
+    :param meeting: Information about the meeting in which the message was sent
+    :type meeting: ~botframework.connector.teams.models.TeamsMeetingInfo
+    """
+
+    _attribute_map = {
+        "channel": {"key": "channel", "type": "ChannelInfo"},
+        "eventType": {"key": "eventType", "type": "str"},
+        "team": {"key": "team", "type": "TeamInfo"},
+        "notification": {"key": "notification", "type": "NotificationInfo"},
+        "tenant": {"key": "tenant", "type": "TenantInfo"},
+        "meeting": {"key": "meeting", "type": "TeamsMeetingInfo"},
+    }
+
+    def __init__(
+        self,
+        *,
+        channel=None,
+        eventType: str = None,
+        team=None,
+        notification=None,
+        tenant=None,
+        meeting=None,
+        **kwargs
+    ) -> None:
+        super(TeamsChannelData, self).__init__(**kwargs)
+        self.channel = channel
+        # doing camel case here since that's how the data comes in
+        self.event_type = eventType
+        self.team = team
+        self.notification = notification
+        self.tenant = tenant
+        self.meeting = meeting
+
+
+class TenantInfo(Model):
+    """Describes a tenant.
+
+    :param id: Unique identifier representing a tenant
+    :type id: str
+    """
+
+    _attribute_map = {
+        "id": {"key": "id", "type": "str"},
+    }
+
+    def __init__(self, *, id: str = None, **kwargs) -> None:
+        super(TenantInfo, self).__init__(**kwargs)
+        self.id = id
+
+
+class TeamsMeetingInfo(Model):
+    """Describes a Teams Meeting.
+
+    :param id: Unique identifier representing a meeting
+    :type id: str
+    """
+
+    _attribute_map = {
+        "id": {"key": "id", "type": "str"},
+    }
+
+    def __init__(self, *, id: str = None, **kwargs) -> None:
+        super(TeamsMeetingInfo, self).__init__(**kwargs)
+        self.id = id
+
+
+class MeetingParticipantInfo(Model):
+    """Teams meeting participant details.
+
+    :param role: Role of the participant in the current meeting.
+    :type role: str
+    :param in_meeting: True, if the participant is in the meeting.
+    :type in_meeting: bool
+    """
+
+    _attribute_map = {
+        "role": {"key": "role", "type": "str"},
+        "in_meeting": {"key": "inMeeting", "type": "bool"},
+    }
+
+    def __init__(self, *, role: str = None, in_meeting: bool = None, **kwargs) -> None:
+        super(MeetingParticipantInfo, self).__init__(**kwargs)
+        self.role = role
+        self.in_meeting = in_meeting
+
+
+class TeamsMeetingParticipant(Model):
+    """Teams participant channel account detailing user Azure Active Directory and meeting participant details.
+
+    :param user: Teams Channel Account information for this meeting participant
+    :type user: TeamsChannelAccount
+    :param meeting: >Information specific to this participant in the specific meeting.
+    :type meeting: MeetingParticipantInfo
+    :param conversation: Conversation Account for the meeting.
+    :type conversation: ConversationAccount
+    """
+
+    _attribute_map = {
+        "user": {"key": "user", "type": "TeamsChannelAccount"},
+        "meeting": {"key": "meeting", "type": "MeetingParticipantInfo"},
+        "conversation": {"key": "conversation", "type": "ConversationAccount"},
+    }
+
+    def __init__(
+        self,
+        *,
+        user: TeamsChannelAccount = None,
+        meeting: MeetingParticipantInfo = None,
+        conversation: ConversationAccount = None,
+        **kwargs
+    ) -> None:
+        super(TeamsMeetingParticipant, self).__init__(**kwargs)
+        self.user = user
+        self.meeting = meeting
+        self.conversation = conversation
diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/additional_properties.py b/libraries/botbuilder-schema/botbuilder/schema/teams/additional_properties.py
new file mode 100644
index 000000000..e9c7544d7
--- /dev/null
+++ b/libraries/botbuilder-schema/botbuilder/schema/teams/additional_properties.py
@@ -0,0 +1,19 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+
+class ContentType:
+    O365_CONNECTOR_CARD = "application/vnd.microsoft.teams.card.o365connector"
+    FILE_CONSENT_CARD = "application/vnd.microsoft.teams.card.file.consent"
+    FILE_DOWNLOAD_INFO = "application/vnd.microsoft.teams.file.download.info"
+    FILE_INFO_CARD = "application/vnd.microsoft.teams.card.file.info"
+
+
+class Type:
+    O365_CONNECTOR_CARD_VIEWACTION = "ViewAction"
+    O365_CONNECTOR_CARD_OPEN_URI = "OpenUri"
+    O365_CONNECTOR_CARD_HTTP_POST = "HttpPOST"
+    O365_CONNECTOR_CARD_ACTION_CARD = "ActionCard"
+    O365_CONNECTOR_CARD_TEXT_INPUT = "TextInput"
+    O365_CONNECTOR_CARD_DATE_INPUT = "DateInput"
+    O365_CONNECTOR_CARD_MULTICHOICE_INPUT = "MultichoiceInput"
diff --git a/libraries/botbuilder-schema/requirements.txt b/libraries/botbuilder-schema/requirements.txt
index bd81a57e3..0361325e5 100644
--- a/libraries/botbuilder-schema/requirements.txt
+++ b/libraries/botbuilder-schema/requirements.txt
@@ -1 +1,2 @@
-msrest>=0.6.6
\ No newline at end of file
+aiounittest==1.3.0
+msrest==0.6.10
\ No newline at end of file
diff --git a/libraries/botbuilder-schema/setup.py b/libraries/botbuilder-schema/setup.py
index 50af773a6..06dc7339e 100644
--- a/libraries/botbuilder-schema/setup.py
+++ b/libraries/botbuilder-schema/setup.py
@@ -1,37 +1,37 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import os
-from setuptools import setup
-
-NAME = "botbuilder-schema"
-VERSION = os.environ["packageVersion"] if "packageVersion" in os.environ else "4.4.0b1"
-REQUIRES = ["msrest>=0.6.6"]
-
-root = os.path.abspath(os.path.dirname(__file__))
-
-with open(os.path.join(root, "README.rst"), encoding="utf-8") as f:
-    long_description = f.read()
-
-setup(
-    name=NAME,
-    version=VERSION,
-    description="BotBuilder Schema",
-    author="Microsoft",
-    url="https://github.com/Microsoft/botbuilder-python",
-    keywords=["BotBuilderSchema", "bots", "ai", "botframework", "botbuilder"],
-    long_description=long_description,
-    long_description_content_type="text/x-rst",
-    license="MIT",
-    install_requires=REQUIRES,
-    packages=["botbuilder.schema"],
-    include_package_data=True,
-    classifiers=[
-        "Programming Language :: Python :: 3.7",
-        "Intended Audience :: Developers",
-        "License :: OSI Approved :: MIT License",
-        "Operating System :: OS Independent",
-        "Development Status :: 3 - Alpha",
-        "Topic :: Scientific/Engineering :: Artificial Intelligence",
-    ],
-)
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+from setuptools import setup
+
+NAME = "botbuilder-schema"
+VERSION = os.environ["packageVersion"] if "packageVersion" in os.environ else "4.12.0"
+REQUIRES = ["msrest==0.6.10"]
+
+root = os.path.abspath(os.path.dirname(__file__))
+
+with open(os.path.join(root, "README.rst"), encoding="utf-8") as f:
+    long_description = f.read()
+
+setup(
+    name=NAME,
+    version=VERSION,
+    description="BotBuilder Schema",
+    author="Microsoft",
+    url="https://github.com/Microsoft/botbuilder-python",
+    keywords=["BotBuilderSchema", "bots", "ai", "botframework", "botbuilder"],
+    long_description=long_description,
+    long_description_content_type="text/x-rst",
+    license="MIT",
+    install_requires=REQUIRES,
+    packages=["botbuilder.schema", "botbuilder.schema.teams",],
+    include_package_data=True,
+    classifiers=[
+        "Programming Language :: Python :: 3.7",
+        "Intended Audience :: Developers",
+        "License :: OSI Approved :: MIT License",
+        "Operating System :: OS Independent",
+        "Development Status :: 5 - Production/Stable",
+        "Topic :: Scientific/Engineering :: Artificial Intelligence",
+    ],
+)
diff --git a/libraries/botbuilder-schema/tests/teams/test_message_actions_payload.py b/libraries/botbuilder-schema/tests/teams/test_message_actions_payload.py
new file mode 100644
index 000000000..ef0cc55ee
--- /dev/null
+++ b/libraries/botbuilder-schema/tests/teams/test_message_actions_payload.py
@@ -0,0 +1,146 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import aiounittest
+from botframework.connector.models import (
+    MessageActionsPayloadFrom,
+    MessageActionsPayloadBody,
+    MessageActionsPayloadAttachment,
+    MessageActionsPayloadMention,
+    MessageActionsPayloadReaction,
+)
+from botbuilder.schema.teams import MessageActionsPayload
+
+
+class TestingMessageActionsPayload(aiounittest.AsyncTestCase):
+    # Arrange
+    test_id = "01"
+    reply_to_id = "test_reply_to_id"
+    message_type = "test_message_type"
+    created_date_time = "01/01/2000"
+    last_modified_date_time = "01/01/2000"
+    deleted = False
+    subject = "test_subject"
+    summary = "test_summary"
+    importance = "high"
+    locale = "test_locale"
+    link_to_message = "https://teams.microsoft/com/l/message/testing-id"
+    from_property = MessageActionsPayloadFrom()
+    body = MessageActionsPayloadBody
+    attachment_layout = "test_attachment_layout"
+    attachments = [MessageActionsPayloadAttachment()]
+    mentions = [MessageActionsPayloadMention()]
+    reactions = [MessageActionsPayloadReaction()]
+
+    # Act
+    message = MessageActionsPayload(
+        id=test_id,
+        reply_to_id=reply_to_id,
+        message_type=message_type,
+        created_date_time=created_date_time,
+        last_modified_date_time=last_modified_date_time,
+        deleted=deleted,
+        subject=subject,
+        summary=summary,
+        importance=importance,
+        locale=locale,
+        link_to_message=link_to_message,
+        from_property=from_property,
+        body=body,
+        attachment_layout=attachment_layout,
+        attachments=attachments,
+        mentions=mentions,
+        reactions=reactions,
+    )
+
+    def test_assign_id(self, message_action_payload=message, test_id=test_id):
+        # Assert
+        self.assertEqual(message_action_payload.id, test_id)
+
+    def test_assign_reply_to_id(
+        self, message_action_payload=message, reply_to_id=reply_to_id
+    ):
+        # Assert
+        self.assertEqual(message_action_payload.reply_to_id, reply_to_id)
+
+    def test_assign_message_type(
+        self, message_action_payload=message, message_type=message_type
+    ):
+        # Assert
+        self.assertEqual(message_action_payload.message_type, message_type)
+
+    def test_assign_created_date_time(
+        self, message_action_payload=message, created_date_time=created_date_time
+    ):
+        # Assert
+        self.assertEqual(message_action_payload.created_date_time, created_date_time)
+
+    def test_assign_last_modified_date_time(
+        self,
+        message_action_payload=message,
+        last_modified_date_time=last_modified_date_time,
+    ):
+        # Assert
+        self.assertEqual(
+            message_action_payload.last_modified_date_time, last_modified_date_time
+        )
+
+    def test_assign_deleted(self, message_action_payload=message, deleted=deleted):
+        # Assert
+        self.assertEqual(message_action_payload.deleted, deleted)
+
+    def test_assign_subject(self, message_action_payload=message, subject=subject):
+        # Assert
+        self.assertEqual(message_action_payload.subject, subject)
+
+    def test_assign_summary(self, message_action_payload=message, summary=summary):
+        # Assert
+        self.assertEqual(message_action_payload.summary, summary)
+
+    def test_assign_importance(
+        self, message_action_payload=message, importance=importance
+    ):
+        # Assert
+        self.assertEqual(message_action_payload.importance, importance)
+
+    def test_assign_locale(self, message_action_payload=message, locale=locale):
+        # Assert
+        self.assertEqual(message_action_payload.locale, locale)
+
+    def test_assign_link_to_message(
+        self, message_action_payload=message, link_to_message=link_to_message
+    ):
+        # Assert
+        self.assertEqual(message_action_payload.link_to_message, link_to_message)
+
+    def test_assign_from_property(
+        self, message_action_payload=message, from_property=from_property
+    ):
+        # Assert
+        self.assertEqual(message_action_payload.from_property, from_property)
+
+    def test_assign_body(self, message_action_payload=message, body=body):
+        # Assert
+        self.assertEqual(message_action_payload.body, body)
+
+    def test_assign_attachment_layout(
+        self, message_action_payload=message, attachment_layout=attachment_layout
+    ):
+        # Assert
+        self.assertEqual(message_action_payload.attachment_layout, attachment_layout)
+
+    def test_assign_attachments(
+        self, message_action_payload=message, attachments=attachments
+    ):
+        # Assert
+        self.assertEqual(message_action_payload.attachments, attachments)
+
+    def test_assign_mentions(self, message_action_payload=message, mentions=mentions):
+        # Assert
+        self.assertEqual(message_action_payload.mentions, mentions)
+
+    def test_assign_reactions(
+        self, message_action_payload=message, reactions=reactions
+    ):
+        # Assert
+        self.assertEqual(message_action_payload.reactions, reactions)
diff --git a/libraries/botbuilder-schema/tests/test_activity.py b/libraries/botbuilder-schema/tests/test_activity.py
new file mode 100644
index 000000000..513ff06f9
--- /dev/null
+++ b/libraries/botbuilder-schema/tests/test_activity.py
@@ -0,0 +1,745 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import aiounittest
+from botbuilder.schema import (
+    Activity,
+    ConversationReference,
+    ConversationAccount,
+    ChannelAccount,
+    Entity,
+    ResourceResponse,
+    Attachment,
+)
+from botbuilder.schema._connector_client_enums import ActivityTypes
+
+
+class TestActivity(aiounittest.AsyncTestCase):
+    def test_constructor(self):
+        # Arrange
+        activity = Activity()
+
+        # Assert
+        self.assertIsNotNone(activity)
+        self.assertIsNone(activity.type)
+        self.assertIsNone(activity.id)
+        self.assertIsNone(activity.timestamp)
+        self.assertIsNone(activity.local_timestamp)
+        self.assertIsNone(activity.local_timezone)
+        self.assertIsNone(activity.service_url)
+        self.assertIsNone(activity.channel_id)
+        self.assertIsNone(activity.from_property)
+        self.assertIsNone(activity.conversation)
+        self.assertIsNone(activity.recipient)
+        self.assertIsNone(activity.text_format)
+        self.assertIsNone(activity.attachment_layout)
+        self.assertIsNone(activity.members_added)
+        self.assertIsNone(activity.members_removed)
+        self.assertIsNone(activity.reactions_added)
+        self.assertIsNone(activity.reactions_removed)
+        self.assertIsNone(activity.topic_name)
+        self.assertIsNone(activity.history_disclosed)
+        self.assertIsNone(activity.locale)
+        self.assertIsNone(activity.text)
+        self.assertIsNone(activity.speak)
+        self.assertIsNone(activity.input_hint)
+        self.assertIsNone(activity.summary)
+        self.assertIsNone(activity.suggested_actions)
+        self.assertIsNone(activity.attachments)
+        self.assertIsNone(activity.entities)
+        self.assertIsNone(activity.channel_data)
+        self.assertIsNone(activity.action)
+        self.assertIsNone(activity.reply_to_id)
+        self.assertIsNone(activity.label)
+        self.assertIsNone(activity.value_type)
+        self.assertIsNone(activity.value)
+        self.assertIsNone(activity.name)
+        self.assertIsNone(activity.relates_to)
+        self.assertIsNone(activity.code)
+        self.assertIsNone(activity.expiration)
+        self.assertIsNone(activity.importance)
+        self.assertIsNone(activity.delivery_mode)
+        self.assertIsNone(activity.listen_for)
+        self.assertIsNone(activity.text_highlights)
+        self.assertIsNone(activity.semantic_action)
+        self.assertIsNone(activity.caller_id)
+
+    def test_apply_conversation_reference(self):
+        # Arrange
+        activity = self.__create_activity()
+        conversation_reference = ConversationReference(
+            channel_id="123",
+            service_url="serviceUrl",
+            conversation=ConversationAccount(id="456"),
+            user=ChannelAccount(id="abc"),
+            bot=ChannelAccount(id="def"),
+            activity_id="12345",
+            locale="en-uS",
+        )
+
+        # Act
+        activity.apply_conversation_reference(reference=conversation_reference)
+
+        # Assert
+        self.assertEqual(conversation_reference.channel_id, activity.channel_id)
+        self.assertEqual(conversation_reference.locale, activity.locale)
+        self.assertEqual(conversation_reference.service_url, activity.service_url)
+        self.assertEqual(
+            conversation_reference.conversation.id, activity.conversation.id
+        )
+        self.assertEqual(conversation_reference.bot.id, activity.from_property.id)
+        self.assertEqual(conversation_reference.user.id, activity.recipient.id)
+        self.assertEqual(conversation_reference.activity_id, activity.reply_to_id)
+
+    def test_apply_conversation_reference_with_is_incoming_true(self):
+        # Arrange
+        activity = self.__create_activity()
+        conversation_reference = ConversationReference(
+            channel_id="cr_123",
+            service_url="cr_serviceUrl",
+            conversation=ConversationAccount(id="cr_456"),
+            user=ChannelAccount(id="cr_abc"),
+            bot=ChannelAccount(id="cr_def"),
+            activity_id="cr_12345",
+            locale="en-uS",
+        )
+
+        # Act
+        activity.apply_conversation_reference(
+            reference=conversation_reference, is_incoming=True
+        )
+
+        # Assert
+        self.assertEqual(conversation_reference.channel_id, activity.channel_id)
+        self.assertEqual(conversation_reference.locale, activity.locale)
+        self.assertEqual(conversation_reference.service_url, activity.service_url)
+        self.assertEqual(
+            conversation_reference.conversation.id, activity.conversation.id
+        )
+        self.assertEqual(conversation_reference.user.id, activity.from_property.id)
+        self.assertEqual(conversation_reference.bot.id, activity.recipient.id)
+        self.assertEqual(conversation_reference.activity_id, activity.id)
+
+    def test_as_contact_relation_update_activity_return_activity(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.contact_relation_update
+
+        # Act
+        result = activity.as_contact_relation_update_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.contact_relation_update)
+
+    def test_as_contact_relation_update_activity_return_none(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.message
+
+        # Act
+        result = activity.as_contact_relation_update_activity()
+
+        # Assert
+        self.assertIsNone(result)
+
+    def test_as_conversation_update_activity_return_activity(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.conversation_update
+
+        # Act
+        result = activity.as_conversation_update_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.conversation_update)
+
+    def test_as_conversation_update_activity_return_none(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.message
+
+        # Act
+        result = activity.as_conversation_update_activity()
+
+        # Assert
+        self.assertIsNone(result)
+
+    def test_as_end_of_conversation_activity_return_activity(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.end_of_conversation
+
+        # Act
+        result = activity.as_end_of_conversation_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.end_of_conversation)
+
+    def test_as_end_of_conversation_activity_return_none(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.message
+
+        # Act
+        result = activity.as_end_of_conversation_activity()
+
+        # Assert
+        self.assertIsNone(result)
+
+    def test_as_event_activity_return_activity(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.event
+
+        # Act
+        result = activity.as_event_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.event)
+
+    def test_as_event_activity_return_none(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.message
+
+        # Act
+        result = activity.as_event_activity()
+
+        # Assert
+        self.assertIsNone(result)
+
+    def test_as_handoff_activity_return_activity(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.handoff
+
+        # Act
+        result = activity.as_handoff_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.handoff)
+
+    def test_as_handoff_activity_return_none(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.message
+
+        # Act
+        result = activity.as_handoff_activity()
+
+        # Assert
+        self.assertIsNone(result)
+
+    def test_as_installation_update_activity_return_activity(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.installation_update
+
+        # Act
+        result = activity.as_installation_update_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.installation_update)
+
+    def test_as_installation_update_activity_return_none(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.message
+
+        # Act
+        result = activity.as_installation_update_activity()
+
+        # Assert
+        self.assertIsNone(result)
+
+    def test_as_invoke_activity_return_activity(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.invoke
+
+        # Act
+        result = activity.as_invoke_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.invoke)
+
+    def test_as_invoke_activity_return_none(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.message
+
+        # Act
+        result = activity.as_invoke_activity()
+
+        # Assert
+        self.assertIsNone(result)
+
+    def test_as_message_activity_return_activity(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.message
+
+        # Act
+        result = activity.as_message_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.message)
+
+    def test_as_message_activity_return_none(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.invoke
+
+        # Act
+        result = activity.as_message_activity()
+
+        # Assert
+        self.assertIsNone(result)
+
+    def test_as_message_activity_type_none(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = None
+
+        # Act
+        result = activity.as_message_activity()
+
+        # Assert
+        self.assertIsNone(result)
+
+    def test_as_message_delete_activity_return_activity(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.message_delete
+
+        # Act
+        result = activity.as_message_delete_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.message_delete)
+
+    def test_as_message_delete_activity_return_none(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.message
+
+        # Act
+        result = activity.as_message_delete_activity()
+
+        # Assert
+        self.assertIsNone(result)
+
+    def test_as_message_reaction_activity_return_activity(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.message_reaction
+
+        # Act
+        result = activity.as_message_reaction_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.message_reaction)
+
+    def test_as_message_reaction_activity_return_none(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.message
+
+        # Act
+        result = activity.as_message_reaction_activity()
+
+        # Assert
+        self.assertIsNone(result)
+
+    def test_as_message_update_activity_return_activity(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.message_update
+
+        # Act
+        result = activity.as_message_update_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.message_update)
+
+    def test_as_message_update_activity_return_none(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.message
+
+        # Act
+        result = activity.as_message_update_activity()
+
+        # Assert
+        self.assertIsNone(result)
+
+    def test_as_suggestion_activity_return_activity(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.suggestion
+
+        # Act
+        result = activity.as_suggestion_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.suggestion)
+
+    def test_as_suggestion_activity_return_none(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.message
+
+        # Act
+        result = activity.as_suggestion_activity()
+
+        # Assert
+        self.assertIsNone(result)
+
+    def test_as_trace_activity_return_activity(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.trace
+
+        # Act
+        result = activity.as_trace_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.trace)
+
+    def test_as_trace_activity_return_none(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.message
+
+        # Act
+        result = activity.as_trace_activity()
+
+        # Assert
+        self.assertIsNone(result)
+
+    def test_as_typing_activity_return_activity(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.typing
+
+        # Act
+        result = activity.as_typing_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.typing)
+
+    def test_as_typing_activity_return_none(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.message
+
+        # Act
+        result = activity.as_typing_activity()
+
+        # Assert
+        self.assertIsNone(result)
+
+    def test_create_contact_relation_update_activity(self):
+        # Act
+        result = Activity.create_contact_relation_update_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.contact_relation_update)
+
+    def test_create_conversation_update_activity(self):
+        # Act
+        result = Activity.create_conversation_update_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.conversation_update)
+
+    def test_create_end_of_conversation_activity(self):
+        # Act
+        result = Activity.create_end_of_conversation_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.end_of_conversation)
+
+    def test_create_event_activity(self):
+        # Act
+        result = Activity.create_event_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.event)
+
+    def test_create_handoff_activity(self):
+        # Act
+        result = Activity.create_handoff_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.handoff)
+
+    def test_create_invoke_activity(self):
+        # Act
+        result = Activity.create_invoke_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.invoke)
+
+    def test_create_message_activity(self):
+        # Act
+        result = Activity.create_message_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.message)
+
+    def test_create_reply(self):
+        # Arrange
+        activity = self.__create_activity()
+        text = "test reply"
+        locale = "en-us"
+
+        # Act
+        result = activity.create_reply(text=text, locale=locale)
+
+        # Assert
+        self.assertEqual(result.text, text)
+        self.assertEqual(result.locale, locale)
+        self.assertEqual(result.type, ActivityTypes.message)
+
+    def test_create_reply_without_arguments(self):
+        # Arrange
+        activity = self.__create_activity()
+
+        # Act
+        result = activity.create_reply()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.message)
+        self.assertEqual(result.text, "")
+        self.assertEqual(result.locale, activity.locale)
+
+    def test_create_trace(self):
+        # Arrange
+        activity = self.__create_activity()
+        name = "test-activity"
+        value_type = "string"
+        value = "test-value"
+        label = "test-label"
+
+        # Act
+        result = activity.create_trace(
+            name=name, value_type=value_type, value=value, label=label
+        )
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.trace)
+        self.assertEqual(result.name, name)
+        self.assertEqual(result.value_type, value_type)
+        self.assertEqual(result.value, value)
+        self.assertEqual(result.label, label)
+
+    def test_create_trace_activity_no_recipient(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.recipient = None
+
+        # Act
+        result = activity.create_trace("test")
+
+        # Assert
+        self.assertIsNone(result.from_property.id)
+        self.assertIsNone(result.from_property.name)
+
+    def test_crete_trace_activity_no_value_type(self):
+        # Arrange
+        name = "test-activity"
+        value = "test-value"
+        label = "test-label"
+
+        # Act
+        result = Activity.create_trace_activity(name=name, value=value, label=label)
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.trace)
+        self.assertEqual(result.value_type, type(value))
+        self.assertEqual(result.label, label)
+
+    def test_create_trace_activity(self):
+        # Arrange
+        name = "test-activity"
+        value_type = "string"
+        value = "test-value"
+        label = "test-label"
+
+        # Act
+        result = Activity.create_trace_activity(
+            name=name, value_type=value_type, value=value, label=label
+        )
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.trace)
+        self.assertEqual(result.name, name)
+        self.assertEqual(result.value_type, value_type)
+        self.assertEqual(result.label, label)
+
+    def test_create_typing_activity(self):
+        # Act
+        result = Activity.create_typing_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.typing)
+
+    def test_get_conversation_reference(self):
+        # Arrange
+        activity = self.__create_activity()
+
+        # Act
+        result = activity.get_conversation_reference()
+
+        # Assert
+        self.assertEqual(activity.id, result.activity_id)
+        self.assertEqual(activity.from_property.id, result.user.id)
+        self.assertEqual(activity.recipient.id, result.bot.id)
+        self.assertEqual(activity.conversation.id, result.conversation.id)
+        self.assertEqual(activity.channel_id, result.channel_id)
+        self.assertEqual(activity.locale, result.locale)
+        self.assertEqual(activity.service_url, result.service_url)
+
+    def test_get_mentions(self):
+        # Arrange
+        mentions = [Entity(type="mention"), Entity(type="reaction")]
+        activity = Activity(entities=mentions)
+
+        # Act
+        result = Activity.get_mentions(activity)
+
+        # Assert
+        self.assertEqual(len(result), 1)
+        self.assertEqual(result[0].type, "mention")
+
+    def test_get_reply_conversation_reference(self):
+        # Arrange
+        activity = self.__create_activity()
+        reply = ResourceResponse(id="1234")
+
+        # Act
+        result = activity.get_reply_conversation_reference(reply=reply)
+
+        # Assert
+        self.assertEqual(reply.id, result.activity_id)
+        self.assertEqual(activity.from_property.id, result.user.id)
+        self.assertEqual(activity.recipient.id, result.bot.id)
+        self.assertEqual(activity.conversation.id, result.conversation.id)
+        self.assertEqual(activity.channel_id, result.channel_id)
+        self.assertEqual(activity.locale, result.locale)
+        self.assertEqual(activity.service_url, result.service_url)
+
+    def test_has_content_empty(self):
+        # Arrange
+        activity_empty = Activity()
+
+        # Act
+        result_empty = activity_empty.has_content()
+
+        # Assert
+        self.assertEqual(result_empty, False)
+
+    def test_has_content_with_text(self):
+        # Arrange
+        activity_with_text = Activity(text="test-text")
+
+        # Act
+        result_with_text = activity_with_text.has_content()
+
+        # Assert
+        self.assertEqual(result_with_text, True)
+
+    def test_has_content_with_summary(self):
+        # Arrange
+        activity_with_summary = Activity(summary="test-summary")
+
+        # Act
+        result_with_summary = activity_with_summary.has_content()
+
+        # Assert
+        self.assertEqual(result_with_summary, True)
+
+    def test_has_content_with_attachment(self):
+        # Arrange
+        activity_with_attachment = Activity(attachments=[Attachment()])
+
+        # Act
+        result_with_attachment = activity_with_attachment.has_content()
+
+        # Assert
+        self.assertEqual(result_with_attachment, True)
+
+    def test_has_content_with_channel_data(self):
+        # Arrange
+        activity_with_channel_data = Activity(channel_data="test-channel-data")
+
+        # Act
+        result_with_channel_data = activity_with_channel_data.has_content()
+
+        # Assert
+        self.assertEqual(result_with_channel_data, True)
+
+    def test_is_from_streaming_connection(self):
+        # Arrange
+        non_streaming = [
+            "http://yayay.com",
+            "https://yayay.com",
+            "HTTP://yayay.com",
+            "HTTPS://yayay.com",
+        ]
+        streaming = [
+            "urn:botframework:WebSocket:wss://beep.com",
+            "urn:botframework:WebSocket:http://beep.com",
+            "URN:botframework:WebSocket:wss://beep.com",
+            "URN:botframework:WebSocket:http://beep.com",
+        ]
+        activity = self.__create_activity()
+        activity.service_url = None
+
+        # Assert
+        self.assertEqual(activity.is_from_streaming_connection(), False)
+
+        for s in non_streaming:
+            activity.service_url = s
+            self.assertEqual(activity.is_from_streaming_connection(), False)
+
+        for s in streaming:
+            activity.service_url = s
+            self.assertEqual(activity.is_from_streaming_connection(), True)
+
+    @staticmethod
+    def __create_activity() -> Activity:
+        account1 = ChannelAccount(
+            id="ChannelAccount_Id_1",
+            name="ChannelAccount_Name_1",
+            aad_object_id="ChannelAccount_aadObjectId_1",
+            role="ChannelAccount_Role_1",
+        )
+
+        account2 = ChannelAccount(
+            id="ChannelAccount_Id_2",
+            name="ChannelAccount_Name_2",
+            aad_object_id="ChannelAccount_aadObjectId_2",
+            role="ChannelAccount_Role_2",
+        )
+
+        conversation_account = ConversationAccount(
+            conversation_type="a",
+            id="123",
+            is_group=True,
+            name="Name",
+            role="ConversationAccount_Role",
+        )
+
+        activity = Activity(
+            id="123",
+            from_property=account1,
+            recipient=account2,
+            conversation=conversation_account,
+            channel_id="ChannelId123",
+            locale="en-uS",
+            service_url="ServiceUrl123",
+        )
+
+        return activity
diff --git a/libraries/botbuilder-testing/README.rst b/libraries/botbuilder-testing/README.rst
index 76d0531a2..10ded9adb 100644
--- a/libraries/botbuilder-testing/README.rst
+++ b/libraries/botbuilder-testing/README.rst
@@ -1,10 +1,10 @@
 
-============================
+=================================
 BotBuilder-Testing SDK for Python
-============================
+=================================
 
-.. image:: https://fuselabs.visualstudio.com/SDK_v4/_apis/build/status/Python/SDK_v4-Python-CI?branchName=master
-   :target:  https://fuselabs.visualstudio.com/SDK_v4/_apis/build/status/Python/SDK_v4-Python-CI
+.. image:: https://dev.azure.com/FuseLabs/SDK_v4/_apis/build/status/Python/Python-CI-PR-yaml?branchName=master
+   :target:  https://dev.azure.com/FuseLabs/SDK_v4/_apis/build/status/Python/Python-CI-PR-yaml?branchName=master
    :align: right
    :alt: Azure DevOps status for master branch
 .. image:: https://badge.fury.io/py/botbuilder-testing.svg
diff --git a/libraries/botbuilder-testing/botbuilder/testing/__init__.py b/libraries/botbuilder-testing/botbuilder/testing/__init__.py
index 681a168e4..af82e1a65 100644
--- a/libraries/botbuilder-testing/botbuilder/testing/__init__.py
+++ b/libraries/botbuilder-testing/botbuilder/testing/__init__.py
@@ -3,6 +3,7 @@
 
 from .dialog_test_client import DialogTestClient
 from .dialog_test_logger import DialogTestLogger
+from .storage_base_tests import StorageBaseTests
 
 
-__all__ = ["DialogTestClient", "DialogTestLogger"]
+__all__ = ["DialogTestClient", "DialogTestLogger", "StorageBaseTests"]
diff --git a/libraries/botbuilder-testing/botbuilder/testing/about.py b/libraries/botbuilder-testing/botbuilder/testing/about.py
index d44f889b1..4817b2d45 100644
--- a/libraries/botbuilder-testing/botbuilder/testing/about.py
+++ b/libraries/botbuilder-testing/botbuilder/testing/about.py
@@ -1,15 +1,15 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import os
-
-
-__title__ = "botbuilder-testing"
-__version__ = (
-    os.environ["packageVersion"] if "packageVersion" in os.environ else "4.4.0b1"
-)
-__uri__ = "https://www.github.com/Microsoft/botbuilder-python"
-__author__ = "Microsoft"
-__description__ = "Microsoft Bot Framework Bot Builder"
-__summary__ = "Microsoft Bot Framework Bot Builder SDK for Python."
-__license__ = "MIT"
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+
+
+__title__ = "botbuilder-testing"
+__version__ = (
+    os.environ["packageVersion"] if "packageVersion" in os.environ else "4.12.0"
+)
+__uri__ = "https://www.github.com/Microsoft/botbuilder-python"
+__author__ = "Microsoft"
+__description__ = "Microsoft Bot Framework Bot Builder"
+__summary__ = "Microsoft Bot Framework Bot Builder SDK for Python."
+__license__ = "MIT"
diff --git a/libraries/botbuilder-testing/botbuilder/testing/dialog_test_client.py b/libraries/botbuilder-testing/botbuilder/testing/dialog_test_client.py
index 4ead12a3e..3e284b5c9 100644
--- a/libraries/botbuilder-testing/botbuilder/testing/dialog_test_client.py
+++ b/libraries/botbuilder-testing/botbuilder/testing/dialog_test_client.py
@@ -1,6 +1,7 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License.
 
+
 from typing import List, Union
 
 from botbuilder.core import (
@@ -53,6 +54,7 @@ def __init__(
         :type conversation_state: ConversationState
         """
         self.dialog_turn_result: DialogTurnResult = None
+        self.dialog_context = None
         self.conversation_state: ConversationState = (
             ConversationState(MemoryStorage())
             if conversation_state is None
@@ -107,10 +109,10 @@ async def default_callback(turn_context: TurnContext) -> None:
             dialog_set = DialogSet(dialog_state)
             dialog_set.add(target_dialog)
 
-            dialog_context = await dialog_set.create_context(turn_context)
-            self.dialog_turn_result = await dialog_context.continue_dialog()
+            self.dialog_context = await dialog_set.create_context(turn_context)
+            self.dialog_turn_result = await self.dialog_context.continue_dialog()
             if self.dialog_turn_result.status == DialogTurnStatus.Empty:
-                self.dialog_turn_result = await dialog_context.begin_dialog(
+                self.dialog_turn_result = await self.dialog_context.begin_dialog(
                     target_dialog.id, initial_dialog_options
                 )
 
diff --git a/libraries/botbuilder-testing/botbuilder/testing/storage_base_tests.py b/libraries/botbuilder-testing/botbuilder/testing/storage_base_tests.py
new file mode 100644
index 000000000..1a307d336
--- /dev/null
+++ b/libraries/botbuilder-testing/botbuilder/testing/storage_base_tests.py
@@ -0,0 +1,354 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+"""
+Base tests that all storage providers should implement in their own tests.
+They handle the storage-based assertions, internally.
+
+All tests return true if assertions pass to indicate that the code ran to completion, passing internal assertions.
+Therefore, all tests using theses static tests should strictly check that the method returns true.
+
+Note: Python cannot have dicts with properties with a None value like other SDKs can have properties with null values.
+      Because of this, StoreItem tests have "e_tag: *" where the tests in the other SDKs do not.
+      This has also caused us to comment out some parts of these tests where we assert that "e_tag"
+      is None for the same reason. A null e_tag should work just like a * e_tag when writing,
+      as far as the storage adapters are concerened, so this shouldn't cause issues.
+
+
+:Example:
+    async def test_handle_null_keys_when_reading(self):
+        await reset()
+
+        test_ran = await StorageBaseTests.handle_null_keys_when_reading(get_storage())
+
+        assert test_ran
+"""
+import pytest
+from botbuilder.azure import CosmosDbStorage
+from botbuilder.core import (
+    ConversationState,
+    TurnContext,
+    MessageFactory,
+    MemoryStorage,
+)
+from botbuilder.core.adapters import TestAdapter
+from botbuilder.dialogs import (
+    DialogSet,
+    DialogTurnStatus,
+    TextPrompt,
+    PromptValidatorContext,
+    WaterfallStepContext,
+    Dialog,
+    WaterfallDialog,
+    PromptOptions,
+)
+
+
+class StorageBaseTests:
+    # pylint: disable=pointless-string-statement
+    @staticmethod
+    async def return_empty_object_when_reading_unknown_key(storage) -> bool:
+        result = await storage.read(["unknown"])
+
+        assert result is not None
+        assert len(result) == 0
+
+        return True
+
+    @staticmethod
+    async def handle_null_keys_when_reading(storage) -> bool:
+        if isinstance(storage, (CosmosDbStorage, MemoryStorage)):
+            result = await storage.read(None)
+            assert len(result.keys()) == 0
+        # Catch-all
+        else:
+            with pytest.raises(Exception) as err:
+                await storage.read(None)
+            assert err.value.args[0] == "Keys are required when reading"
+
+        return True
+
+    @staticmethod
+    async def handle_null_keys_when_writing(storage) -> bool:
+        with pytest.raises(Exception) as err:
+            await storage.write(None)
+        assert err.value.args[0] == "Changes are required when writing"
+
+        return True
+
+    @staticmethod
+    async def does_not_raise_when_writing_no_items(storage) -> bool:
+        # noinspection PyBroadException
+        try:
+            await storage.write([])
+        except:
+            pytest.fail("Should not raise")
+
+        return True
+
+    @staticmethod
+    async def create_object(storage) -> bool:
+        store_items = {
+            "createPoco": {"id": 1},
+            "createPocoStoreItem": {"id": 2, "e_tag": "*"},
+        }
+
+        await storage.write(store_items)
+
+        read_store_items = await storage.read(store_items.keys())
+
+        assert store_items["createPoco"]["id"] == read_store_items["createPoco"]["id"]
+        assert (
+            store_items["createPocoStoreItem"]["id"]
+            == read_store_items["createPocoStoreItem"]["id"]
+        )
+
+        # If decided to validate e_tag integrity again, uncomment this code
+        # assert read_store_items["createPoco"]["e_tag"] is not None
+        assert read_store_items["createPocoStoreItem"]["e_tag"] is not None
+
+        return True
+
+    @staticmethod
+    async def handle_crazy_keys(storage) -> bool:
+        key = '!@#$%^&*()_+??><":QASD~`'
+        store_item = {"id": 1}
+        store_items = {key: store_item}
+
+        await storage.write(store_items)
+
+        read_store_items = await storage.read(store_items.keys())
+
+        assert read_store_items[key] is not None
+        assert read_store_items[key]["id"] == 1
+
+        return True
+
+    @staticmethod
+    async def update_object(storage) -> bool:
+        original_store_items = {
+            "pocoItem": {"id": 1, "count": 1},
+            "pocoStoreItem": {"id": 1, "count": 1, "e_tag": "*"},
+        }
+
+        # 1st write should work
+        await storage.write(original_store_items)
+
+        loaded_store_items = await storage.read(["pocoItem", "pocoStoreItem"])
+
+        update_poco_item = loaded_store_items["pocoItem"]
+        update_poco_item["e_tag"] = None
+        update_poco_store_item = loaded_store_items["pocoStoreItem"]
+        assert update_poco_store_item["e_tag"] is not None
+
+        # 2nd write should work
+        update_poco_item["count"] += 1
+        update_poco_store_item["count"] += 1
+
+        await storage.write(loaded_store_items)
+
+        reloaded_store_items = await storage.read(loaded_store_items.keys())
+
+        reloaded_update_poco_item = reloaded_store_items["pocoItem"]
+        reloaded_update_poco_store_item = reloaded_store_items["pocoStoreItem"]
+
+        assert reloaded_update_poco_item["count"] == 2
+        assert reloaded_update_poco_store_item["count"] == 2
+
+        # Write with old e_tag should succeed for non-storeItem
+        update_poco_item["count"] = 123
+        await storage.write({"pocoItem": update_poco_item})
+
+        # Write with old eTag should FAIL for storeItem
+        update_poco_store_item["count"] = 123
+
+        """
+        This assert exists in the other SDKs but can't in python, currently
+        due to using "e_tag: *" above (see comment near the top of this file for details).
+
+        with pytest.raises(Exception) as err:
+            await storage.write({"pocoStoreItem": update_poco_store_item})
+        assert err.value is not None
+        """
+
+        reloaded_store_items2 = await storage.read(["pocoItem", "pocoStoreItem"])
+
+        reloaded_poco_item2 = reloaded_store_items2["pocoItem"]
+        reloaded_poco_item2["e_tag"] = None
+        reloaded_poco_store_item2 = reloaded_store_items2["pocoStoreItem"]
+
+        assert reloaded_poco_item2["count"] == 123
+        assert reloaded_poco_store_item2["count"] == 2
+
+        # write with wildcard etag should work
+        reloaded_poco_item2["count"] = 100
+        reloaded_poco_store_item2["count"] = 100
+        reloaded_poco_store_item2["e_tag"] = "*"
+
+        wildcard_etag_dict = {
+            "pocoItem": reloaded_poco_item2,
+            "pocoStoreItem": reloaded_poco_store_item2,
+        }
+
+        await storage.write(wildcard_etag_dict)
+
+        reloaded_store_items3 = await storage.read(["pocoItem", "pocoStoreItem"])
+
+        assert reloaded_store_items3["pocoItem"]["count"] == 100
+        assert reloaded_store_items3["pocoStoreItem"]["count"] == 100
+
+        # Write with empty etag should not work
+        reloaded_store_items4 = await storage.read(["pocoStoreItem"])
+        reloaded_store_item4 = reloaded_store_items4["pocoStoreItem"]
+
+        assert reloaded_store_item4 is not None
+
+        reloaded_store_item4["e_tag"] = ""
+        dict2 = {"pocoStoreItem": reloaded_store_item4}
+
+        with pytest.raises(Exception) as err:
+            await storage.write(dict2)
+        assert err.value is not None
+
+        final_store_items = await storage.read(["pocoItem", "pocoStoreItem"])
+        assert final_store_items["pocoItem"]["count"] == 100
+        assert final_store_items["pocoStoreItem"]["count"] == 100
+
+        return True
+
+    @staticmethod
+    async def delete_object(storage) -> bool:
+        store_items = {"delete1": {"id": 1, "count": 1, "e_tag": "*"}}
+
+        await storage.write(store_items)
+
+        read_store_items = await storage.read(["delete1"])
+
+        assert read_store_items["delete1"]["e_tag"]
+        assert read_store_items["delete1"]["count"] == 1
+
+        await storage.delete(["delete1"])
+
+        reloaded_store_items = await storage.read(["delete1"])
+
+        assert reloaded_store_items.get("delete1", None) is None
+
+        return True
+
+    @staticmethod
+    async def delete_unknown_object(storage) -> bool:
+        # noinspection PyBroadException
+        try:
+            await storage.delete(["unknown_key"])
+        except:
+            pytest.fail("Should not raise")
+
+        return True
+
+    @staticmethod
+    async def perform_batch_operations(storage) -> bool:
+        await storage.write(
+            {"batch1": {"count": 10}, "batch2": {"count": 20}, "batch3": {"count": 30},}
+        )
+
+        result = await storage.read(["batch1", "batch2", "batch3"])
+
+        assert result.get("batch1", None) is not None
+        assert result.get("batch2", None) is not None
+        assert result.get("batch3", None) is not None
+        assert result["batch1"]["count"] == 10
+        assert result["batch2"]["count"] == 20
+        assert result["batch3"]["count"] == 30
+        """
+        If decided to validate e_tag integrity aagain, uncomment this code
+        assert result["batch1"].get("e_tag", None) is not None
+        assert result["batch2"].get("e_tag", None) is not None
+        assert result["batch3"].get("e_tag", None) is not None
+        """
+
+        await storage.delete(["batch1", "batch2", "batch3"])
+
+        result = await storage.read(["batch1", "batch2", "batch3"])
+
+        assert result.get("batch1", None) is None
+        assert result.get("batch2", None) is None
+        assert result.get("batch3", None) is None
+
+        return True
+
+    @staticmethod
+    async def proceeds_through_waterfall(storage) -> bool:
+        convo_state = ConversationState(storage)
+
+        dialog_state = convo_state.create_property("dialogState")
+        dialogs = DialogSet(dialog_state)
+
+        async def exec_test(turn_context: TurnContext) -> None:
+            dialog_context = await dialogs.create_context(turn_context)
+
+            await dialog_context.continue_dialog()
+            if not turn_context.responded:
+                await dialog_context.begin_dialog(WaterfallDialog.__name__)
+            await convo_state.save_changes(turn_context)
+
+        adapter = TestAdapter(exec_test)
+
+        async def prompt_validator(prompt_context: PromptValidatorContext):
+            result = prompt_context.recognized.value
+            if len(result) > 3:
+                succeeded_message = MessageFactory.text(
+                    f"You got it at the {prompt_context.options.number_of_attempts}rd try!"
+                )
+                await prompt_context.context.send_activity(succeeded_message)
+                return True
+
+            reply = MessageFactory.text(
+                f"Please send a name that is longer than 3 characters. {prompt_context.options.number_of_attempts}"
+            )
+            await prompt_context.context.send_activity(reply)
+            return False
+
+        async def step_1(step_context: WaterfallStepContext) -> DialogTurnStatus:
+            assert isinstance(step_context.active_dialog.state["stepIndex"], int)
+            await step_context.context.send_activity("step1")
+            return Dialog.end_of_turn
+
+        async def step_2(step_context: WaterfallStepContext) -> None:
+            assert isinstance(step_context.active_dialog.state["stepIndex"], int)
+            await step_context.prompt(
+                TextPrompt.__name__,
+                PromptOptions(prompt=MessageFactory.text("Please type your name")),
+            )
+
+        async def step_3(step_context: WaterfallStepContext) -> DialogTurnStatus:
+            assert isinstance(step_context.active_dialog.state["stepIndex"], int)
+            await step_context.context.send_activity("step3")
+            return Dialog.end_of_turn
+
+        steps = [step_1, step_2, step_3]
+
+        dialogs.add(WaterfallDialog(WaterfallDialog.__name__, steps))
+
+        dialogs.add(TextPrompt(TextPrompt.__name__, prompt_validator))
+
+        step1 = await adapter.send("hello")
+        step2 = await step1.assert_reply("step1")
+        step3 = await step2.send("hello")
+        step4 = await step3.assert_reply("Please type your name")  # None
+        step5 = await step4.send("hi")
+        step6 = await step5.assert_reply(
+            "Please send a name that is longer than 3 characters. 0"
+        )
+        step7 = await step6.send("hi")
+        step8 = await step7.assert_reply(
+            "Please send a name that is longer than 3 characters. 1"
+        )
+        step9 = await step8.send("hi")
+        step10 = await step9.assert_reply(
+            "Please send a name that is longer than 3 characters. 2"
+        )
+        step11 = await step10.send("Kyle")
+        step12 = await step11.assert_reply("You got it at the 3rd try!")
+        await step12.assert_reply("step3")
+
+        return True
diff --git a/libraries/botbuilder-testing/requirements.txt b/libraries/botbuilder-testing/requirements.txt
index 9eb053052..18a973dc1 100644
--- a/libraries/botbuilder-testing/requirements.txt
+++ b/libraries/botbuilder-testing/requirements.txt
@@ -1,4 +1,4 @@
-botbuilder-schema>=4.4.0b1
-botbuilder-core>=4.4.0b1
-botbuilder-dialogs>=4.4.0b1
-aiounittest>=1.1.0
+botbuilder-schema==4.12.0
+botbuilder-core==4.12.0
+botbuilder-dialogs==4.12.0
+aiounittest==1.3.0
diff --git a/libraries/botbuilder-testing/setup.py b/libraries/botbuilder-testing/setup.py
index 2bdff64d8..a10601cf9 100644
--- a/libraries/botbuilder-testing/setup.py
+++ b/libraries/botbuilder-testing/setup.py
@@ -5,12 +5,12 @@
 from setuptools import setup
 
 REQUIRES = [
-    "botbuilder-schema>=4.4.0b1",
-    "botbuilder-core>=4.4.0b1",
-    "botbuilder-dialogs>=4.4.0b1",
+    "botbuilder-schema==4.12.0",
+    "botbuilder-core==4.12.0",
+    "botbuilder-dialogs==4.12.0",
 ]
 
-TESTS_REQUIRES = ["aiounittest>=1.1.0"]
+TESTS_REQUIRES = ["aiounittest==1.3.0"]
 
 root = os.path.abspath(os.path.dirname(__file__))
 
@@ -41,7 +41,7 @@
         "Intended Audience :: Developers",
         "License :: OSI Approved :: MIT License",
         "Operating System :: OS Independent",
-        "Development Status :: 3 - Alpha",
+        "Development Status :: 5 - Production/Stable",
         "Topic :: Scientific/Engineering :: Artificial Intelligence",
     ],
 )
diff --git a/libraries/botframework-connector/README.rst b/libraries/botframework-connector/README.rst
index fc196f30a..d19a47bba 100644
--- a/libraries/botframework-connector/README.rst
+++ b/libraries/botframework-connector/README.rst
@@ -3,8 +3,8 @@
 Microsoft Bot Framework Connector for Python
 ============================================
 
-.. image:: https://fuselabs.visualstudio.com/SDK_v4/_apis/build/status/Python/SDK_v4-Python-CI?branchName=master
-   :target:  https://fuselabs.visualstudio.com/SDK_v4/_apis/build/status/Python/SDK_v4-Python-CI
+.. image:: https://dev.azure.com/FuseLabs/SDK_v4/_apis/build/status/Python/Python-CI-PR-yaml?branchName=master
+   :target:  https://dev.azure.com/FuseLabs/SDK_v4/_apis/build/status/Python/Python-CI-PR-yaml?branchName=master
    :align: right
    :alt: Azure DevOps status for master branch
 .. image:: https://badge.fury.io/py/botframework-connector.svg
@@ -39,9 +39,9 @@ Client creation (with authentication), conversation initialization and activity
 
 .. code-block:: python
 
-  from microsoft.botbuilder.schema import *
-  from microsoft.botframework.connector import ConnectorClient
-  from microsoft.botframework.connector.auth import MicrosoftTokenAuthentication
+  from botbuilder.schema import *
+  from botframework.connector import ConnectorClient
+  from botframework.connector.auth import MicrosoftAppCredentials
 
   APP_ID = ''
   APP_PASSWORD = ''
@@ -50,7 +50,7 @@ Client creation (with authentication), conversation initialization and activity
   BOT_ID = ''
   RECIPIENT_ID = ''
 
-  credentials = MicrosoftTokenAuthentication(APP_ID, APP_PASSWORD)
+  credentials = MicrosoftAppCredentials(APP_ID, APP_PASSWORD)
   connector = ConnectorClient(credentials, base_url=SERVICE_URL)
 
   conversation = connector.conversations.create_conversation(ConversationParameters(
@@ -130,4 +130,4 @@ Licensed under the MIT_ License.
 .. _MSRC PGP: https://technet.microsoft.com/en-us/security/dn606155
 .. _Security TechCenter: https://github.com/Microsoft/vscode/blob/master/LICENSE.txt
 
-.. `_
\ No newline at end of file
+.. `_
diff --git a/libraries/botframework-connector/botframework/connector/__init__.py b/libraries/botframework-connector/botframework/connector/__init__.py
index 47e6ad952..519f0ab2e 100644
--- a/libraries/botframework-connector/botframework/connector/__init__.py
+++ b/libraries/botframework-connector/botframework/connector/__init__.py
@@ -3,10 +3,6 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License. See License.txt in the project root for
 # license information.
-#
-# Code generated by Microsoft (R) AutoRest Code Generator.
-# Changes may cause incorrect behavior and will be lost if the code is
-# regenerated.
 # --------------------------------------------------------------------------
 
 from .channels import Channels
diff --git a/libraries/botframework-connector/botframework/connector/_configuration.py b/libraries/botframework-connector/botframework/connector/_configuration.py
index 33f23fd21..ce9a8c1d7 100644
--- a/libraries/botframework-connector/botframework/connector/_configuration.py
+++ b/libraries/botframework-connector/botframework/connector/_configuration.py
@@ -3,10 +3,6 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License. See License.txt in the project root for
 # license information.
-#
-# Code generated by Microsoft (R) AutoRest Code Generator.
-# Changes may cause incorrect behavior and will be lost if the code is
-# regenerated.
 # --------------------------------------------------------------------------
 
 from msrest import Configuration
diff --git a/libraries/botframework-connector/botframework/connector/aio/__init__.py b/libraries/botframework-connector/botframework/connector/aio/__init__.py
index 04c1b91a5..e8f4fa483 100644
--- a/libraries/botframework-connector/botframework/connector/aio/__init__.py
+++ b/libraries/botframework-connector/botframework/connector/aio/__init__.py
@@ -3,10 +3,6 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License. See License.txt in the project root for
 # license information.
-#
-# Code generated by Microsoft (R) AutoRest Code Generator.
-# Changes may cause incorrect behavior and will be lost if the code is
-# regenerated.
 # --------------------------------------------------------------------------
 
 from ._connector_client_async import ConnectorClient
diff --git a/libraries/botframework-connector/botframework/connector/aio/_connector_client_async.py b/libraries/botframework-connector/botframework/connector/aio/_connector_client_async.py
index ff6b9b314..73cebfb07 100644
--- a/libraries/botframework-connector/botframework/connector/aio/_connector_client_async.py
+++ b/libraries/botframework-connector/botframework/connector/aio/_connector_client_async.py
@@ -3,10 +3,6 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License. See License.txt in the project root for
 # license information.
-#
-# Code generated by Microsoft (R) AutoRest Code Generator.
-# Changes may cause incorrect behavior and will be lost if the code is
-# regenerated.
 # --------------------------------------------------------------------------
 
 from msrest.async_client import SDKClientAsync
diff --git a/libraries/botframework-connector/botframework/connector/aio/operations_async/__init__.py b/libraries/botframework-connector/botframework/connector/aio/operations_async/__init__.py
index 6adc13e41..ca019f8e4 100644
--- a/libraries/botframework-connector/botframework/connector/aio/operations_async/__init__.py
+++ b/libraries/botframework-connector/botframework/connector/aio/operations_async/__init__.py
@@ -3,10 +3,6 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License. See License.txt in the project root for
 # license information.
-#
-# Code generated by Microsoft (R) AutoRest Code Generator.
-# Changes may cause incorrect behavior and will be lost if the code is
-# regenerated.
 # --------------------------------------------------------------------------
 
 from ._attachments_operations_async import AttachmentsOperations
diff --git a/libraries/botframework-connector/botframework/connector/aio/operations_async/_attachments_operations_async.py b/libraries/botframework-connector/botframework/connector/aio/operations_async/_attachments_operations_async.py
index a46fa7da5..1bb926cfa 100644
--- a/libraries/botframework-connector/botframework/connector/aio/operations_async/_attachments_operations_async.py
+++ b/libraries/botframework-connector/botframework/connector/aio/operations_async/_attachments_operations_async.py
@@ -3,10 +3,6 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License. See License.txt in the project root for
 # license information.
-#
-# Code generated by Microsoft (R) AutoRest Code Generator.
-# Changes may cause incorrect behavior and will be lost if the code is
-# regenerated.
 # --------------------------------------------------------------------------
 
 from msrest.pipeline import ClientRawResponse
diff --git a/libraries/botframework-connector/botframework/connector/aio/operations_async/_conversations_operations_async.py b/libraries/botframework-connector/botframework/connector/aio/operations_async/_conversations_operations_async.py
index 6afdf82c4..a982ec673 100644
--- a/libraries/botframework-connector/botframework/connector/aio/operations_async/_conversations_operations_async.py
+++ b/libraries/botframework-connector/botframework/connector/aio/operations_async/_conversations_operations_async.py
@@ -3,10 +3,6 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License. See License.txt in the project root for
 # license information.
-#
-# Code generated by Microsoft (R) AutoRest Code Generator.
-# Changes may cause incorrect behavior and will be lost if the code is
-# regenerated.
 # --------------------------------------------------------------------------
 
 from msrest.pipeline import ClientRawResponse
@@ -677,6 +673,77 @@ async def get_conversation_members(
         "url": "/v3/conversations/{conversationId}/members"
     }
 
+    async def get_conversation_member(
+        self,
+        conversation_id,
+        member_id,
+        custom_headers=None,
+        raw=False,
+        **operation_config
+    ):
+        """GetConversationMember.
+
+        Get a member of a conversation.
+        This REST API takes a ConversationId and memberId and returns a
+        ChannelAccount object representing the member of the conversation.
+
+        :param conversation_id: Conversation Id
+        :type conversation_id: str
+        :param member_id: Member Id
+        :type member_id: str
+        :param dict custom_headers: headers that will be added to the request
+        :param bool raw: returns the direct response alongside the
+         deserialized response
+        :param operation_config: :ref:`Operation configuration
+         overrides`.
+        :return: list or ClientRawResponse if raw=true
+        :rtype: list[~botframework.connector.models.ChannelAccount] or
+         ~msrest.pipeline.ClientRawResponse
+        :raises:
+         :class:`ErrorResponseException`
+        """
+        # Construct URL
+        url = self.get_conversation_member.metadata["url"]
+        path_format_arguments = {
+            "conversationId": self._serialize.url(
+                "conversation_id", conversation_id, "str"
+            ),
+            "memberId": self._serialize.url("member_id", member_id, "str"),
+        }
+        url = self._client.format_url(url, **path_format_arguments)
+
+        # Construct parameters
+        query_parameters = {}
+
+        # Construct headers
+        header_parameters = {}
+        header_parameters["Accept"] = "application/json"
+        if custom_headers:
+            header_parameters.update(custom_headers)
+
+        # Construct and send request
+        request = self._client.get(url, query_parameters, header_parameters)
+        response = await self._client.async_send(
+            request, stream=False, **operation_config
+        )
+
+        if response.status_code not in [200]:
+            raise models.ErrorResponseException(self._deserialize, response)
+
+        deserialized = None
+        if response.status_code == 200:
+            deserialized = self._deserialize("ChannelAccount", response)
+
+        if raw:
+            client_raw_response = ClientRawResponse(deserialized, response)
+            return client_raw_response
+
+        return deserialized
+
+    get_conversation_member.metadata = {
+        "url": "/v3/conversations/{conversationId}/members/{memberId}"
+    }
+
     async def get_conversation_paged_members(
         self,
         conversation_id,
@@ -770,6 +837,98 @@ async def get_conversation_paged_members(
         "url": "/v3/conversations/{conversationId}/pagedmembers"
     }
 
+    async def get_teams_conversation_paged_members(
+        self,
+        conversation_id,
+        page_size=None,
+        continuation_token=None,
+        custom_headers=None,
+        raw=False,
+        **operation_config
+    ):
+        """GetTeamsConversationPagedMembers.
+
+        Enumerate the members of a Teams conversation one page at a time.
+        This REST API takes a ConversationId. Optionally a pageSize and/or
+        continuationToken can be provided. It returns a PagedMembersResult,
+        which contains an array
+        of ChannelAccounts representing the members of the conversation and a
+        continuation token that can be used to get more values.
+        One page of ChannelAccounts records are returned with each call. The
+        number of records in a page may vary between channels and calls. The
+        pageSize parameter can be used as
+        a suggestion. If there are no additional results the response will not
+        contain a continuation token. If there are no members in the
+        conversation the Members will be empty or not present in the response.
+        A response to a request that has a continuation token from a prior
+        request may rarely return members from a previous request.
+
+        :param conversation_id: Conversation ID
+        :type conversation_id: str
+        :param page_size: Suggested page size
+        :type page_size: int
+        :param continuation_token: Continuation Token
+        :type continuation_token: str
+        :param dict custom_headers: headers that will be added to the request
+        :param bool raw: returns the direct response alongside the
+         deserialized response
+        :param operation_config: :ref:`Operation configuration
+         overrides`.
+        :return: PagedMembersResult or ClientRawResponse if raw=true
+        :rtype: ~botframework.connector.models.PagedMembersResult or
+         ~msrest.pipeline.ClientRawResponse
+        :raises:
+         :class:`HttpOperationError`
+        """
+        # Construct URL
+        url = self.get_conversation_paged_members.metadata["url"]
+        path_format_arguments = {
+            "conversationId": self._serialize.url(
+                "conversation_id", conversation_id, "str"
+            )
+        }
+        url = self._client.format_url(url, **path_format_arguments)
+
+        # Construct parameters
+        query_parameters = {}
+        if page_size is not None:
+            query_parameters["pageSize"] = self._serialize.query(
+                "page_size", page_size, "int"
+            )
+        if continuation_token is not None:
+            query_parameters["continuationToken"] = self._serialize.query(
+                "continuation_token", continuation_token, "str"
+            )
+
+        # Construct headers
+        header_parameters = {}
+        header_parameters["Accept"] = "application/json"
+        if custom_headers:
+            header_parameters.update(custom_headers)
+
+        # Construct and send request
+        request = self._client.get(url, query_parameters, header_parameters)
+        response = await self._client.async_send(
+            request, stream=False, **operation_config
+        )
+
+        if response.status_code not in [200]:
+            raise HttpOperationError(self._deserialize, response)
+
+        deserialized = None
+        if response.status_code == 200:
+            deserialized = self._deserialize("TeamsPagedMembersResult", response)
+
+        if raw:
+            client_raw_response = ClientRawResponse(deserialized, response)
+            return client_raw_response
+
+        return deserialized
+
+    get_conversation_paged_members.metadata = {
+        "url": "/v3/conversations/{conversationId}/pagedmembers"
+    }
+
     async def delete_conversation_member(
         self,
         conversation_id,
diff --git a/libraries/botframework-connector/botframework/connector/async_mixin/__init__.py b/libraries/botframework-connector/botframework/connector/async_mixin/__init__.py
index 796bf96fe..76ba66e7a 100644
--- a/libraries/botframework-connector/botframework/connector/async_mixin/__init__.py
+++ b/libraries/botframework-connector/botframework/connector/async_mixin/__init__.py
@@ -1 +1,4 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
 from .async_mixin import AsyncServiceClientMixin
diff --git a/libraries/botframework-connector/botframework/connector/auth/__init__.py b/libraries/botframework-connector/botframework/connector/auth/__init__.py
index 3dd269e1b..d5f273e0f 100644
--- a/libraries/botframework-connector/botframework/connector/auth/__init__.py
+++ b/libraries/botframework-connector/botframework/connector/auth/__init__.py
@@ -1,20 +1,22 @@
-# coding=utf-8
-# --------------------------------------------------------------------------
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License. See License.txt in the project root for
-# license information.
-#
-# Code generated by Microsoft (R) AutoRest Code Generator.
-# Changes may cause incorrect behavior and will be lost if the code is
-# regenerated.
-# --------------------------------------------------------------------------
-# pylint: disable=missing-docstring
-
-from .microsoft_app_credentials import *
-from .jwt_token_validation import *
-from .credential_provider import *
-from .channel_validation import *
-from .emulator_validation import *
-from .jwt_token_extractor import *
-from .government_constants import *
-from .authentication_constants import *
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+# --------------------------------------------------------------------------
+# pylint: disable=missing-docstring
+from .authentication_constants import *
+from .government_constants import *
+from .channel_provider import *
+from .simple_channel_provider import *
+from .app_credentials import *
+from .microsoft_app_credentials import *
+from .microsoft_government_app_credentials import *
+from .certificate_app_credentials import *
+from .claims_identity import *
+from .jwt_token_validation import *
+from .credential_provider import *
+from .channel_validation import *
+from .emulator_validation import *
+from .jwt_token_extractor import *
+from .authentication_configuration import *
diff --git a/libraries/botframework-connector/botframework/connector/auth/app_credentials.py b/libraries/botframework-connector/botframework/connector/auth/app_credentials.py
new file mode 100644
index 000000000..db657e25f
--- /dev/null
+++ b/libraries/botframework-connector/botframework/connector/auth/app_credentials.py
@@ -0,0 +1,118 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from datetime import datetime, timedelta
+from urllib.parse import urlparse
+
+import requests
+from msrest.authentication import Authentication
+
+from botframework.connector.auth import AuthenticationConstants
+
+
+class AppCredentials(Authentication):
+    """
+    Base class for token retrieval.  Subclasses MUST override get_access_token in
+    order to supply a valid token for the specific credentials.
+    """
+
+    schema = "Bearer"
+
+    trustedHostNames = {
+        # "state.botframework.com": datetime.max,
+        # "state.botframework.azure.us": datetime.max,
+        "api.botframework.com": datetime.max,
+        "token.botframework.com": datetime.max,
+        "api.botframework.azure.us": datetime.max,
+        "token.botframework.azure.us": datetime.max,
+    }
+    cache = {}
+
+    def __init__(
+        self,
+        app_id: str = None,
+        channel_auth_tenant: str = None,
+        oauth_scope: str = None,
+    ):
+        """
+        Initializes a new instance of MicrosoftAppCredentials class
+        :param channel_auth_tenant: Optional. The oauth token tenant.
+        """
+        tenant = (
+            channel_auth_tenant
+            if channel_auth_tenant
+            else AuthenticationConstants.DEFAULT_CHANNEL_AUTH_TENANT
+        )
+        self.oauth_endpoint = (
+            AuthenticationConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL_PREFIX + tenant
+        )
+        self.oauth_scope = (
+            oauth_scope or AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
+        )
+
+        self.microsoft_app_id = app_id
+
+    @staticmethod
+    def trust_service_url(service_url: str, expiration=None):
+        """
+        Checks if the service url is for a trusted host or not.
+        :param service_url: The service url.
+        :param expiration: The expiration time after which this service url is not trusted anymore.
+        :returns: True if the host of the service url is trusted; False otherwise.
+        """
+        if expiration is None:
+            expiration = datetime.now() + timedelta(days=1)
+        host = urlparse(service_url).hostname
+        if host is not None:
+            AppCredentials.trustedHostNames[host] = expiration
+
+    @staticmethod
+    def is_trusted_service(service_url: str) -> bool:
+        """
+        Checks if the service url is for a trusted host or not.
+        :param service_url: The service url.
+        :returns: True if the host of the service url is trusted; False otherwise.
+        """
+        host = urlparse(service_url).hostname
+        if host is not None:
+            return AppCredentials._is_trusted_url(host)
+        return False
+
+    @staticmethod
+    def _is_trusted_url(host: str) -> bool:
+        expiration = AppCredentials.trustedHostNames.get(host, datetime.min)
+        return expiration > (datetime.now() - timedelta(minutes=5))
+
+    # pylint: disable=arguments-differ
+    def signed_session(self, session: requests.Session = None) -> requests.Session:
+        """
+        Gets the signed session.  This is called by the msrest package
+        :returns: Signed requests.Session object
+        """
+        if not session:
+            session = requests.Session()
+
+        if not self._should_authorize(session):
+            session.headers.pop("Authorization", None)
+        else:
+            auth_token = self.get_access_token()
+            header = "{} {}".format("Bearer", auth_token)
+            session.headers["Authorization"] = header
+
+        return session
+
+    def _should_authorize(
+        self, session: requests.Session  # pylint: disable=unused-argument
+    ) -> bool:
+        # We don't set the token if the AppId is not set, since it means that we are in an un-authenticated scenario.
+        return (
+            self.microsoft_app_id != AuthenticationConstants.ANONYMOUS_SKILL_APP_ID
+            and self.microsoft_app_id is not None
+        )
+
+    def get_access_token(self, force_refresh: bool = False) -> str:
+        """
+        Returns a token for the current AppCredentials.
+        :return: The token
+        """
+        raise NotImplementedError()
diff --git a/libraries/botframework-connector/botframework/connector/auth/authentication_configuration.py b/libraries/botframework-connector/botframework/connector/auth/authentication_configuration.py
new file mode 100644
index 000000000..59642d9ff
--- /dev/null
+++ b/libraries/botframework-connector/botframework/connector/auth/authentication_configuration.py
@@ -0,0 +1,14 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from typing import Awaitable, Callable, Dict, List
+
+
+class AuthenticationConfiguration:
+    def __init__(
+        self,
+        required_endorsements: List[str] = None,
+        claims_validator: Callable[[List[Dict]], Awaitable] = None,
+    ):
+        self.required_endorsements = required_endorsements or []
+        self.claims_validator = claims_validator
diff --git a/libraries/botframework-connector/botframework/connector/auth/authentication_constants.py b/libraries/botframework-connector/botframework/connector/auth/authentication_constants.py
index 429b7ccb6..294223f18 100644
--- a/libraries/botframework-connector/botframework/connector/auth/authentication_constants.py
+++ b/libraries/botframework-connector/botframework/connector/auth/authentication_constants.py
@@ -1,5 +1,6 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License.
+
 from abc import ABC
 
 
@@ -113,3 +114,9 @@ class AuthenticationConstants(ABC):
 
     # Service URL claim name. As used in Microsoft Bot Framework v3.1 auth.
     SERVICE_URL_CLAIM = "serviceurl"
+
+    # AppId used for creating skill claims when there is no appId and password configured.
+    ANONYMOUS_SKILL_APP_ID = "AnonymousSkill"
+
+    # Indicates that ClaimsIdentity.authentication_type is anonymous (no app Id and password were provided).
+    ANONYMOUS_AUTH_TYPE = "anonymous"
diff --git a/libraries/botframework-connector/botframework/connector/auth/certificate_app_credentials.py b/libraries/botframework-connector/botframework/connector/auth/certificate_app_credentials.py
new file mode 100644
index 000000000..a458ce5bb
--- /dev/null
+++ b/libraries/botframework-connector/botframework/connector/auth/certificate_app_credentials.py
@@ -0,0 +1,86 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from abc import ABC
+
+from msal import ConfidentialClientApplication
+
+from .app_credentials import AppCredentials
+
+
+class CertificateAppCredentials(AppCredentials, ABC):
+    """
+    AppCredentials implementation using a certificate.
+
+    See:
+    https://github.com/AzureAD/microsoft-authentication-library-for-python/wiki/Client-Credentials#client-credentials-with-certificate
+    """
+
+    def __init__(
+        self,
+        app_id: str,
+        certificate_thumbprint: str,
+        certificate_private_key: str,
+        channel_auth_tenant: str = None,
+        oauth_scope: str = None,
+        certificate_public: str = None,
+    ):
+        """
+        AppCredentials implementation using a certificate.
+
+        :param app_id:
+        :param certificate_thumbprint:
+        :param certificate_private_key:
+        :param channel_auth_tenant:
+        :param oauth_scope:
+        :param certificate_public: public_certificate (optional) is public key certificate which will be sent
+        through ‘x5c’ JWT header only for subject name and issuer authentication to support cert auto rolls.
+        """
+
+        # super will set proper scope and endpoint.
+        super().__init__(
+            app_id=app_id,
+            channel_auth_tenant=channel_auth_tenant,
+            oauth_scope=oauth_scope,
+        )
+
+        self.scopes = [self.oauth_scope]
+        self.app = None
+        self.certificate_thumbprint = certificate_thumbprint
+        self.certificate_private_key = certificate_private_key
+        self.certificate_public = certificate_public
+
+    def get_access_token(self, force_refresh: bool = False) -> str:
+        """
+        Implementation of AppCredentials.get_token.
+        :return: The access token for the given certificate.
+        """
+
+        # Firstly, looks up a token from cache
+        # Since we are looking for token for the current app, NOT for an end user,
+        # notice we give account parameter as None.
+        auth_token = self.__get_msal_app().acquire_token_silent(
+            self.scopes, account=None
+        )
+        if not auth_token:
+            # No suitable token exists in cache. Let's get a new one from AAD.
+            auth_token = self.__get_msal_app().acquire_token_for_client(
+                scopes=self.scopes
+            )
+        return auth_token["access_token"]
+
+    def __get_msal_app(self):
+        if not self.app:
+            self.app = ConfidentialClientApplication(
+                client_id=self.microsoft_app_id,
+                authority=self.oauth_endpoint,
+                client_credential={
+                    "thumbprint": self.certificate_thumbprint,
+                    "private_key": self.certificate_private_key,
+                    "public_certificate": self.certificate_public
+                    if self.certificate_public
+                    else None,
+                },
+            )
+
+        return self.app
diff --git a/libraries/botframework-connector/botframework/connector/auth/channel_provider.py b/libraries/botframework-connector/botframework/connector/auth/channel_provider.py
new file mode 100644
index 000000000..9c75b10d8
--- /dev/null
+++ b/libraries/botframework-connector/botframework/connector/auth/channel_provider.py
@@ -0,0 +1,24 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from abc import ABC, abstractmethod
+
+
+class ChannelProvider(ABC):
+    """
+    ChannelProvider interface. This interface allows Bots to provide their own
+    implementation for the configuration parameters to connect to a Bot.
+    Framework channel service.
+    """
+
+    @abstractmethod
+    async def get_channel_service(self) -> str:
+        raise NotImplementedError()
+
+    @abstractmethod
+    def is_government(self) -> bool:
+        raise NotImplementedError()
+
+    @abstractmethod
+    def is_public_azure(self) -> bool:
+        raise NotImplementedError()
diff --git a/libraries/botframework-connector/botframework/connector/auth/channel_validation.py b/libraries/botframework-connector/botframework/connector/auth/channel_validation.py
index 5ea008233..0acaeea8f 100644
--- a/libraries/botframework-connector/botframework/connector/auth/channel_validation.py
+++ b/libraries/botframework-connector/botframework/connector/auth/channel_validation.py
@@ -1,134 +1,143 @@
-import asyncio
-
-from .verify_options import VerifyOptions
-from .constants import Constants
-from .jwt_token_extractor import JwtTokenExtractor
-from .claims_identity import ClaimsIdentity
-from .credential_provider import CredentialProvider
-
-
-class ChannelValidation:
-    open_id_metadata_endpoint: str = None
-
-    # This claim is ONLY used in the Channel Validation, and not in the emulator validation
-    SERVICE_URL_CLAIM = "serviceurl"
-
-    #
-    # TO BOT FROM CHANNEL: Token validation parameters when connecting to a bot
-    #
-    TO_BOT_FROM_CHANNEL_TOKEN_VALIDATION_PARAMETERS = VerifyOptions(
-        issuer=[Constants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER],
-        # Audience validation takes place manually in code.
-        audience=None,
-        clock_tolerance=5 * 60,
-        ignore_expiration=False,
-    )
-
-    @staticmethod
-    async def authenticate_channel_token_with_service_url(
-        auth_header: str,
-        credentials: CredentialProvider,
-        service_url: str,
-        channel_id: str,
-    ) -> ClaimsIdentity:
-        """ Validate the incoming Auth Header
-
-        Validate the incoming Auth Header as a token sent from the Bot Framework Service.
-        A token issued by the Bot Framework emulator will FAIL this check.
-
-        :param auth_header: The raw HTTP header in the format: 'Bearer [longString]'
-        :type auth_header: str
-        :param credentials: The user defined set of valid credentials, such as the AppId.
-        :type credentials: CredentialProvider
-        :param service_url: Claim value that must match in the identity.
-        :type service_url: str
-
-        :return: A valid ClaimsIdentity.
-        :raises Exception:
-        """
-        identity = await asyncio.ensure_future(
-            ChannelValidation.authenticate_channel_token(
-                auth_header, credentials, channel_id
-            )
-        )
-
-        service_url_claim = identity.get_claim_value(
-            ChannelValidation.SERVICE_URL_CLAIM
-        )
-        if service_url_claim != service_url:
-            # Claim must match. Not Authorized.
-            raise Exception("Unauthorized. service_url claim do not match.")
-
-        return identity
-
-    @staticmethod
-    async def authenticate_channel_token(
-        auth_header: str, credentials: CredentialProvider, channel_id: str
-    ) -> ClaimsIdentity:
-        """ Validate the incoming Auth Header
-
-        Validate the incoming Auth Header as a token sent from the Bot Framework Service.
-        A token issued by the Bot Framework emulator will FAIL this check.
-
-        :param auth_header: The raw HTTP header in the format: 'Bearer [longString]'
-        :type auth_header: str
-        :param credentials: The user defined set of valid credentials, such as the AppId.
-        :type credentials: CredentialProvider
-
-        :return: A valid ClaimsIdentity.
-        :raises Exception:
-        """
-        metadata_endpoint = (
-            ChannelValidation.open_id_metadata_endpoint
-            if ChannelValidation.open_id_metadata_endpoint
-            else Constants.TO_BOT_FROM_CHANNEL_OPEN_ID_METADATA_URL
-        )
-
-        token_extractor = JwtTokenExtractor(
-            ChannelValidation.TO_BOT_FROM_CHANNEL_TOKEN_VALIDATION_PARAMETERS,
-            metadata_endpoint,
-            Constants.ALLOWED_SIGNING_ALGORITHMS,
-        )
-
-        identity = await asyncio.ensure_future(
-            token_extractor.get_identity_from_auth_header(auth_header, channel_id)
-        )
-
-        return await ChannelValidation.validate_identity(identity, credentials)
-
-    @staticmethod
-    async def validate_identity(
-        identity: ClaimsIdentity, credentials: CredentialProvider
-    ) -> ClaimsIdentity:
-        if not identity:
-            # No valid identity. Not Authorized.
-            raise Exception("Unauthorized. No valid identity.")
-
-        if not identity.is_authenticated:
-            # The token is in some way invalid. Not Authorized.
-            raise Exception("Unauthorized. Is not authenticated")
-
-        # Now check that the AppID in the claimset matches
-        # what we're looking for. Note that in a multi-tenant bot, this value
-        # comes from developer code that may be reaching out to a service, hence the
-        # Async validation.
-
-        # Look for the "aud" claim, but only if issued from the Bot Framework
-        if (
-            identity.get_claim_value(Constants.ISSUER_CLAIM)
-            != Constants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER
-        ):
-            # The relevant Audience Claim MUST be present. Not Authorized.
-            raise Exception("Unauthorized. Audience Claim MUST be present.")
-
-        # The AppId from the claim in the token must match the AppId specified by the developer.
-        # Note that the Bot Framework uses the Audience claim ("aud") to pass the AppID.
-        aud_claim = identity.get_claim_value(Constants.AUDIENCE_CLAIM)
-        is_valid_app_id = await asyncio.ensure_future(
-            credentials.is_valid_appid(aud_claim or "")
-        )
-        if not is_valid_app_id:
-            # The AppId is not valid or not present. Not Authorized.
-            raise Exception("Unauthorized. Invalid AppId passed on token: ", aud_claim)
-
-        return identity
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import asyncio
+
+from .authentication_configuration import AuthenticationConfiguration
+from .verify_options import VerifyOptions
+from .authentication_constants import AuthenticationConstants
+from .jwt_token_extractor import JwtTokenExtractor
+from .claims_identity import ClaimsIdentity
+from .credential_provider import CredentialProvider
+
+
+class ChannelValidation:
+    open_id_metadata_endpoint: str = None
+
+    # This claim is ONLY used in the Channel Validation, and not in the emulator validation
+    SERVICE_URL_CLAIM = "serviceurl"
+
+    #
+    # TO BOT FROM CHANNEL: Token validation parameters when connecting to a bot
+    #
+    TO_BOT_FROM_CHANNEL_TOKEN_VALIDATION_PARAMETERS = VerifyOptions(
+        issuer=[AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER],
+        # Audience validation takes place manually in code.
+        audience=None,
+        clock_tolerance=5 * 60,
+        ignore_expiration=False,
+    )
+
+    @staticmethod
+    async def authenticate_channel_token_with_service_url(
+        auth_header: str,
+        credentials: CredentialProvider,
+        service_url: str,
+        channel_id: str,
+        auth_configuration: AuthenticationConfiguration = None,
+    ) -> ClaimsIdentity:
+        """ Validate the incoming Auth Header
+
+        Validate the incoming Auth Header as a token sent from the Bot Framework Service.
+        A token issued by the Bot Framework emulator will FAIL this check.
+
+        :param auth_header: The raw HTTP header in the format: 'Bearer [longString]'
+        :type auth_header: str
+        :param credentials: The user defined set of valid credentials, such as the AppId.
+        :type credentials: CredentialProvider
+        :param service_url: Claim value that must match in the identity.
+        :type service_url: str
+
+        :return: A valid ClaimsIdentity.
+        :raises Exception:
+        """
+        identity = await ChannelValidation.authenticate_channel_token(
+            auth_header, credentials, channel_id, auth_configuration
+        )
+
+        service_url_claim = identity.get_claim_value(
+            ChannelValidation.SERVICE_URL_CLAIM
+        )
+        if service_url_claim != service_url:
+            # Claim must match. Not Authorized.
+            raise PermissionError("Unauthorized. service_url claim do not match.")
+
+        return identity
+
+    @staticmethod
+    async def authenticate_channel_token(
+        auth_header: str,
+        credentials: CredentialProvider,
+        channel_id: str,
+        auth_configuration: AuthenticationConfiguration = None,
+    ) -> ClaimsIdentity:
+        """ Validate the incoming Auth Header
+
+        Validate the incoming Auth Header as a token sent from the Bot Framework Service.
+        A token issued by the Bot Framework emulator will FAIL this check.
+
+        :param auth_header: The raw HTTP header in the format: 'Bearer [longString]'
+        :type auth_header: str
+        :param credentials: The user defined set of valid credentials, such as the AppId.
+        :type credentials: CredentialProvider
+
+        :return: A valid ClaimsIdentity.
+        :raises Exception:
+        """
+        auth_configuration = auth_configuration or AuthenticationConfiguration()
+        metadata_endpoint = (
+            ChannelValidation.open_id_metadata_endpoint
+            if ChannelValidation.open_id_metadata_endpoint
+            else AuthenticationConstants.TO_BOT_FROM_CHANNEL_OPEN_ID_METADATA_URL
+        )
+
+        token_extractor = JwtTokenExtractor(
+            ChannelValidation.TO_BOT_FROM_CHANNEL_TOKEN_VALIDATION_PARAMETERS,
+            metadata_endpoint,
+            AuthenticationConstants.ALLOWED_SIGNING_ALGORITHMS,
+        )
+
+        identity = await token_extractor.get_identity_from_auth_header(
+            auth_header, channel_id, auth_configuration.required_endorsements
+        )
+
+        return await ChannelValidation.validate_identity(identity, credentials)
+
+    @staticmethod
+    async def validate_identity(
+        identity: ClaimsIdentity, credentials: CredentialProvider
+    ) -> ClaimsIdentity:
+        if not identity:
+            # No valid identity. Not Authorized.
+            raise PermissionError("Unauthorized. No valid identity.")
+
+        if not identity.is_authenticated:
+            # The token is in some way invalid. Not Authorized.
+            raise PermissionError("Unauthorized. Is not authenticated")
+
+        # Now check that the AppID in the claimset matches
+        # what we're looking for. Note that in a multi-tenant bot, this value
+        # comes from developer code that may be reaching out to a service, hence the
+        # Async validation.
+
+        # Look for the "aud" claim, but only if issued from the Bot Framework
+        if (
+            identity.get_claim_value(AuthenticationConstants.ISSUER_CLAIM)
+            != AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER
+        ):
+            # The relevant Audience Claim MUST be present. Not Authorized.
+            raise PermissionError("Unauthorized. Audience Claim MUST be present.")
+
+        # The AppId from the claim in the token must match the AppId specified by the developer.
+        # Note that the Bot Framework uses the Audience claim ("aud") to pass the AppID.
+        aud_claim = identity.get_claim_value(AuthenticationConstants.AUDIENCE_CLAIM)
+        is_valid_app_id = await asyncio.ensure_future(
+            credentials.is_valid_appid(aud_claim or "")
+        )
+        if not is_valid_app_id:
+            # The AppId is not valid or not present. Not Authorized.
+            raise PermissionError(
+                "Unauthorized. Invalid AppId passed on token: ", aud_claim
+            )
+
+        return identity
diff --git a/libraries/botframework-connector/botframework/connector/auth/claims_identity.py b/libraries/botframework-connector/botframework/connector/auth/claims_identity.py
index 9abdb6cb0..211f7b241 100644
--- a/libraries/botframework-connector/botframework/connector/auth/claims_identity.py
+++ b/libraries/botframework-connector/botframework/connector/auth/claims_identity.py
@@ -1,3 +1,7 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+
 class Claim:
     def __init__(self, claim_type: str, value):
         self.type = claim_type
@@ -5,9 +9,12 @@ def __init__(self, claim_type: str, value):
 
 
 class ClaimsIdentity:
-    def __init__(self, claims: dict, is_authenticated: bool):
+    def __init__(
+        self, claims: dict, is_authenticated: bool, authentication_type: str = None
+    ):
         self.claims = claims
         self.is_authenticated = is_authenticated
+        self.authentication_type = authentication_type
 
     def get_claim_value(self, claim_type: str):
         return self.claims.get(claim_type)
diff --git a/libraries/botframework-connector/botframework/connector/auth/constants.py b/libraries/botframework-connector/botframework/connector/auth/constants.py
deleted file mode 100644
index 03a95a908..000000000
--- a/libraries/botframework-connector/botframework/connector/auth/constants.py
+++ /dev/null
@@ -1,31 +0,0 @@
-class Constants:  # pylint: disable=too-few-public-methods
-    """
-    TO CHANNEL FROM BOT: Login URL prefix
-    """
-
-    TO_CHANNEL_FROM_BOT_LOGIN_URL_PREFIX = "https://login.microsoftonline.com/"
-
-    """
-    TO CHANNEL FROM BOT: Login URL token endpoint path
-    """
-    TO_CHANNEL_FROM_BOT_TOKEN_ENDPOINT_PATH = "/oauth2/v2.0/token"
-
-    """
-    TO CHANNEL FROM BOT: Default tenant from which to obtain a token for bot to channel communication
-    """
-    DEFAULT_CHANNEL_AUTH_TENANT = "botframework.com"
-
-    TO_BOT_FROM_CHANNEL_TOKEN_ISSUER = "https://api.botframework.com"
-
-    TO_BOT_FROM_EMULATOR_OPEN_ID_METADATA_URL = (
-        "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration"
-    )
-    TO_BOT_FROM_CHANNEL_OPEN_ID_METADATA_URL = (
-        "https://login.botframework.com/v1/.well-known/openidconfiguration"
-    )
-
-    ALLOWED_SIGNING_ALGORITHMS = ["RS256", "RS384", "RS512"]
-
-    AUTHORIZED_PARTY = "azp"
-    AUDIENCE_CLAIM = "aud"
-    ISSUER_CLAIM = "iss"
diff --git a/libraries/botframework-connector/botframework/connector/auth/credential_provider.py b/libraries/botframework-connector/botframework/connector/auth/credential_provider.py
index b95cff120..7d41c2464 100644
--- a/libraries/botframework-connector/botframework/connector/auth/credential_provider.py
+++ b/libraries/botframework-connector/botframework/connector/auth/credential_provider.py
@@ -1,3 +1,7 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+
 class CredentialProvider:
     """CredentialProvider.
     This class allows Bots to provide their own implemention
diff --git a/libraries/botframework-connector/botframework/connector/auth/emulator_validation.py b/libraries/botframework-connector/botframework/connector/auth/emulator_validation.py
index 37e376cd7..b00b8e1cc 100644
--- a/libraries/botframework-connector/botframework/connector/auth/emulator_validation.py
+++ b/libraries/botframework-connector/botframework/connector/auth/emulator_validation.py
@@ -1,187 +1,192 @@
-import asyncio
-import jwt
-
-from .jwt_token_extractor import JwtTokenExtractor
-from .verify_options import VerifyOptions
-from .constants import Constants
-from .credential_provider import CredentialProvider
-from .claims_identity import ClaimsIdentity
-from .government_constants import GovernmentConstants
-
-
-class EmulatorValidation:
-    APP_ID_CLAIM = "appid"
-    VERSION_CLAIM = "ver"
-
-    TO_BOT_FROM_EMULATOR_TOKEN_VALIDATION_PARAMETERS = VerifyOptions(
-        issuer=[
-            # Auth v3.1, 1.0 token
-            "https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/",
-            # Auth v3.1, 2.0 token
-            "https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0",
-            # Auth v3.2, 1.0 token
-            "https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/",
-            # Auth v3.2, 2.0 token
-            "https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0",
-            # ???
-            "https://sts.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47/",
-            # Auth for US Gov, 1.0 token
-            "https://sts.windows.net/cab8a31a-1906-4287-a0d8-4eef66b95f6e/",
-            # Auth for US Gov, 2.0 token
-            "https://login.microsoftonline.us/cab8a31a-1906-4287-a0d8-4eef66b95f6e/v2.0",
-        ],
-        audience=None,
-        clock_tolerance=5 * 60,
-        ignore_expiration=False,
-    )
-
-    @staticmethod
-    def is_token_from_emulator(auth_header: str) -> bool:
-        """ Determines if a given Auth header is from the Bot Framework Emulator
-
-        :param auth_header: Bearer Token, in the 'Bearer [Long String]' Format.
-        :type auth_header: str
-
-        :return: True, if the token was issued by the Emulator. Otherwise, false.
-        """
-        # The Auth Header generally looks like this:
-        # "Bearer eyJ0e[...Big Long String...]XAiO"
-        if not auth_header:
-            # No token. Can't be an emulator token.
-            return False
-
-        parts = auth_header.split(" ")
-        if len(parts) != 2:
-            # Emulator tokens MUST have exactly 2 parts.
-            # If we don't have 2 parts, it's not an emulator token
-            return False
-
-        auth_scheme = parts[0]
-        bearer_token = parts[1]
-
-        # We now have an array that should be:
-        # [0] = "Bearer"
-        # [1] = "[Big Long String]"
-        if auth_scheme != "Bearer":
-            # The scheme from the emulator MUST be "Bearer"
-            return False
-
-        # Parse the Big Long String into an actual token.
-        token = jwt.decode(bearer_token, verify=False)
-        if not token:
-            return False
-
-        # Is there an Issuer?
-        issuer = token["iss"]
-        if not issuer:
-            # No Issuer, means it's not from the Emulator.
-            return False
-
-        # Is the token issues by a source we consider to be the emulator?
-        issuer_list = (
-            EmulatorValidation.TO_BOT_FROM_EMULATOR_TOKEN_VALIDATION_PARAMETERS.issuer
-        )
-        if issuer_list and not issuer in issuer_list:
-            # Not a Valid Issuer. This is NOT a Bot Framework Emulator Token.
-            return False
-
-        # The Token is from the Bot Framework Emulator. Success!
-        return True
-
-    @staticmethod
-    async def authenticate_emulator_token(
-        auth_header: str,
-        credentials: CredentialProvider,
-        channel_service: str,
-        channel_id: str,
-    ) -> ClaimsIdentity:
-        """ Validate the incoming Auth Header
-
-        Validate the incoming Auth Header as a token sent from the Bot Framework Service.
-        A token issued by the Bot Framework emulator will FAIL this check.
-
-        :param auth_header: The raw HTTP header in the format: 'Bearer [longString]'
-        :type auth_header: str
-        :param credentials: The user defined set of valid credentials, such as the AppId.
-        :type credentials: CredentialProvider
-
-        :return: A valid ClaimsIdentity.
-        :raises Exception:
-        """
-        # pylint: disable=import-outside-toplevel
-        from .jwt_token_validation import JwtTokenValidation
-
-        open_id_metadata = (
-            GovernmentConstants.TO_BOT_FROM_EMULATOR_OPEN_ID_METADATA_URL
-            if (
-                channel_service is not None
-                and JwtTokenValidation.is_government(channel_service)
-            )
-            else Constants.TO_BOT_FROM_EMULATOR_OPEN_ID_METADATA_URL
-        )
-
-        token_extractor = JwtTokenExtractor(
-            EmulatorValidation.TO_BOT_FROM_EMULATOR_TOKEN_VALIDATION_PARAMETERS,
-            open_id_metadata,
-            Constants.ALLOWED_SIGNING_ALGORITHMS,
-        )
-
-        identity = await asyncio.ensure_future(
-            token_extractor.get_identity_from_auth_header(auth_header, channel_id)
-        )
-        if not identity:
-            # No valid identity. Not Authorized.
-            raise Exception("Unauthorized. No valid identity.")
-
-        if not identity.is_authenticated:
-            # The token is in some way invalid. Not Authorized.
-            raise Exception("Unauthorized. Is not authenticated")
-
-        # Now check that the AppID in the claimset matches
-        # what we're looking for. Note that in a multi-tenant bot, this value
-        # comes from developer code that may be reaching out to a service, hence the
-        # Async validation.
-        version_claim = identity.get_claim_value(EmulatorValidation.VERSION_CLAIM)
-        if version_claim is None:
-            raise Exception('Unauthorized. "ver" claim is required on Emulator Tokens.')
-
-        app_id = ""
-
-        # The Emulator, depending on Version, sends the AppId via either the
-        # appid claim (Version 1) or the Authorized Party claim (Version 2).
-        if not version_claim or version_claim == "1.0":
-            # either no Version or a version of "1.0" means we should look for
-            # the claim in the "appid" claim.
-            app_id_claim = identity.get_claim_value(EmulatorValidation.APP_ID_CLAIM)
-            if not app_id_claim:
-                # No claim around AppID. Not Authorized.
-                raise Exception(
-                    "Unauthorized. "
-                    '"appid" claim is required on Emulator Token version "1.0".'
-                )
-
-            app_id = app_id_claim
-        elif version_claim == "2.0":
-            # Emulator, "2.0" puts the AppId in the "azp" claim.
-            app_authz_claim = identity.get_claim_value(Constants.AUTHORIZED_PARTY)
-            if not app_authz_claim:
-                # No claim around AppID. Not Authorized.
-                raise Exception(
-                    "Unauthorized. "
-                    '"azp" claim is required on Emulator Token version "2.0".'
-                )
-
-            app_id = app_authz_claim
-        else:
-            # Unknown Version. Not Authorized.
-            raise Exception(
-                "Unauthorized. Unknown Emulator Token version ", version_claim, "."
-            )
-
-        is_valid_app_id = await asyncio.ensure_future(
-            credentials.is_valid_appid(app_id)
-        )
-        if not is_valid_app_id:
-            raise Exception("Unauthorized. Invalid AppId passed on token: ", app_id)
-
-        return identity
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import asyncio
+from typing import Union
+
+import jwt
+
+from .jwt_token_extractor import JwtTokenExtractor
+from .verify_options import VerifyOptions
+from .authentication_constants import AuthenticationConstants
+from .credential_provider import CredentialProvider
+from .claims_identity import ClaimsIdentity
+from .government_constants import GovernmentConstants
+from .channel_provider import ChannelProvider
+
+
+class EmulatorValidation:
+    APP_ID_CLAIM = "appid"
+    VERSION_CLAIM = "ver"
+
+    TO_BOT_FROM_EMULATOR_TOKEN_VALIDATION_PARAMETERS = VerifyOptions(
+        issuer=[
+            # Auth v3.1, 1.0 token
+            "https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/",
+            # Auth v3.1, 2.0 token
+            "https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0",
+            # Auth v3.2, 1.0 token
+            "https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/",
+            # Auth v3.2, 2.0 token
+            "https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0",
+            # ???
+            "https://sts.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47/",
+            # Auth for US Gov, 1.0 token
+            "https://sts.windows.net/cab8a31a-1906-4287-a0d8-4eef66b95f6e/",
+            # Auth for US Gov, 2.0 token
+            "https://login.microsoftonline.us/cab8a31a-1906-4287-a0d8-4eef66b95f6e/v2.0",
+            # Auth for US Gov, 1.0 token
+            "https://login.microsoftonline.us/f8cdef31-a31e-4b4a-93e4-5f571e91255a/",
+            # Auth for US Gov, 2.0 token
+            "https://login.microsoftonline.us/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0",
+        ],
+        audience=None,
+        clock_tolerance=5 * 60,
+        ignore_expiration=False,
+    )
+
+    @staticmethod
+    def is_token_from_emulator(auth_header: str) -> bool:
+        """ Determines if a given Auth header is from the Bot Framework Emulator
+
+        :param auth_header: Bearer Token, in the 'Bearer [Long String]' Format.
+        :type auth_header: str
+
+        :return: True, if the token was issued by the Emulator. Otherwise, false.
+        """
+        from .jwt_token_validation import (  # pylint: disable=import-outside-toplevel
+            JwtTokenValidation,
+        )
+
+        if not JwtTokenValidation.is_valid_token_format(auth_header):
+            return False
+
+        bearer_token = auth_header.split(" ")[1]
+
+        # Parse the Big Long String into an actual token.
+        token = jwt.decode(bearer_token, verify=False)
+        if not token:
+            return False
+
+        # Is there an Issuer?
+        issuer = token["iss"]
+        if not issuer:
+            # No Issuer, means it's not from the Emulator.
+            return False
+
+        # Is the token issues by a source we consider to be the emulator?
+        issuer_list = (
+            EmulatorValidation.TO_BOT_FROM_EMULATOR_TOKEN_VALIDATION_PARAMETERS.issuer
+        )
+        if issuer_list and not issuer in issuer_list:
+            # Not a Valid Issuer. This is NOT a Bot Framework Emulator Token.
+            return False
+
+        # The Token is from the Bot Framework Emulator. Success!
+        return True
+
+    @staticmethod
+    async def authenticate_emulator_token(
+        auth_header: str,
+        credentials: CredentialProvider,
+        channel_service_or_provider: Union[str, ChannelProvider],
+        channel_id: str,
+    ) -> ClaimsIdentity:
+        """ Validate the incoming Auth Header
+
+        Validate the incoming Auth Header as a token sent from the Bot Framework Service.
+        A token issued by the Bot Framework emulator will FAIL this check.
+
+        :param auth_header: The raw HTTP header in the format: 'Bearer [longString]'
+        :type auth_header: str
+        :param credentials: The user defined set of valid credentials, such as the AppId.
+        :type credentials: CredentialProvider
+
+        :return: A valid ClaimsIdentity.
+        :raises Exception:
+        """
+        # pylint: disable=import-outside-toplevel
+        from .jwt_token_validation import JwtTokenValidation
+
+        if isinstance(channel_service_or_provider, ChannelProvider):
+            is_gov = channel_service_or_provider.is_government()
+        else:
+            is_gov = JwtTokenValidation.is_government(channel_service_or_provider)
+
+        open_id_metadata = (
+            GovernmentConstants.TO_BOT_FROM_EMULATOR_OPEN_ID_METADATA_URL
+            if is_gov
+            else AuthenticationConstants.TO_BOT_FROM_EMULATOR_OPEN_ID_METADATA_URL
+        )
+
+        token_extractor = JwtTokenExtractor(
+            EmulatorValidation.TO_BOT_FROM_EMULATOR_TOKEN_VALIDATION_PARAMETERS,
+            open_id_metadata,
+            AuthenticationConstants.ALLOWED_SIGNING_ALGORITHMS,
+        )
+
+        identity = await token_extractor.get_identity_from_auth_header(
+            auth_header, channel_id
+        )
+        if not identity:
+            # No valid identity. Not Authorized.
+            raise PermissionError("Unauthorized. No valid identity.")
+
+        if not identity.is_authenticated:
+            # The token is in some way invalid. Not Authorized.
+            raise PermissionError("Unauthorized. Is not authenticated")
+
+        # Now check that the AppID in the claimset matches
+        # what we're looking for. Note that in a multi-tenant bot, this value
+        # comes from developer code that may be reaching out to a service, hence the
+        # Async validation.
+        version_claim = identity.get_claim_value(EmulatorValidation.VERSION_CLAIM)
+        if version_claim is None:
+            raise PermissionError(
+                'Unauthorized. "ver" claim is required on Emulator Tokens.'
+            )
+
+        app_id = ""
+
+        # The Emulator, depending on Version, sends the AppId via either the
+        # appid claim (Version 1) or the Authorized Party claim (Version 2).
+        if not version_claim or version_claim == "1.0":
+            # either no Version or a version of "1.0" means we should look for
+            # the claim in the "appid" claim.
+            app_id_claim = identity.get_claim_value(EmulatorValidation.APP_ID_CLAIM)
+            if not app_id_claim:
+                # No claim around AppID. Not Authorized.
+                raise PermissionError(
+                    "Unauthorized. "
+                    '"appid" claim is required on Emulator Token version "1.0".'
+                )
+
+            app_id = app_id_claim
+        elif version_claim == "2.0":
+            # Emulator, "2.0" puts the AppId in the "azp" claim.
+            app_authz_claim = identity.get_claim_value(
+                AuthenticationConstants.AUTHORIZED_PARTY
+            )
+            if not app_authz_claim:
+                # No claim around AppID. Not Authorized.
+                raise PermissionError(
+                    "Unauthorized. "
+                    '"azp" claim is required on Emulator Token version "2.0".'
+                )
+
+            app_id = app_authz_claim
+        else:
+            # Unknown Version. Not Authorized.
+            raise PermissionError(
+                "Unauthorized. Unknown Emulator Token version ", version_claim, "."
+            )
+
+        is_valid_app_id = await asyncio.ensure_future(
+            credentials.is_valid_appid(app_id)
+        )
+        if not is_valid_app_id:
+            raise PermissionError(
+                "Unauthorized. Invalid AppId passed on token: ", app_id
+            )
+
+        return identity
diff --git a/libraries/botframework-connector/botframework/connector/auth/endorsements_validator.py b/libraries/botframework-connector/botframework/connector/auth/endorsements_validator.py
index a9c234972..46e93234a 100644
--- a/libraries/botframework-connector/botframework/connector/auth/endorsements_validator.py
+++ b/libraries/botframework-connector/botframework/connector/auth/endorsements_validator.py
@@ -6,10 +6,10 @@
 
 class EndorsementsValidator:
     @staticmethod
-    def validate(channel_id: str, endorsements: List[str]):
+    def validate(expected_endorsement: str, endorsements: List[str]):
         # If the Activity came in and doesn't have a Channel ID then it's making no
         # assertions as to who endorses it. This means it should pass.
-        if not channel_id:
+        if not expected_endorsement:
             return True
 
         if endorsements is None:
@@ -31,5 +31,5 @@ def validate(channel_id: str, endorsements: List[str]):
         # of scope, tokens from WebChat have about 10 endorsements, and
         # tokens coming from Teams have about 20.
 
-        endorsement_present = channel_id in endorsements
+        endorsement_present = expected_endorsement in endorsements
         return endorsement_present
diff --git a/libraries/botframework-connector/botframework/connector/auth/enterprise_channel_validation.py b/libraries/botframework-connector/botframework/connector/auth/enterprise_channel_validation.py
index 5124b65ed..48a93ba5d 100644
--- a/libraries/botframework-connector/botframework/connector/auth/enterprise_channel_validation.py
+++ b/libraries/botframework-connector/botframework/connector/auth/enterprise_channel_validation.py
@@ -1,106 +1,119 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from abc import ABC
-
-from .authentication_constants import AuthenticationConstants
-from .channel_validation import ChannelValidation
-from .claims_identity import ClaimsIdentity
-from .credential_provider import CredentialProvider
-from .jwt_token_extractor import JwtTokenExtractor
-from .verify_options import VerifyOptions
-
-
-class EnterpriseChannelValidation(ABC):
-
-    TO_BOT_FROM_ENTERPRISE_CHANNEL_TOKEN_VALIDATION_PARAMETERS = VerifyOptions(
-        issuer=[AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER],
-        audience=None,
-        clock_tolerance=5 * 60,
-        ignore_expiration=False,
-    )
-
-    @staticmethod
-    async def authenticate_channel_token(
-        auth_header: str,
-        credentials: CredentialProvider,
-        channel_id: str,
-        channel_service: str,
-    ) -> ClaimsIdentity:
-        endpoint = (
-            ChannelValidation.open_id_metadata_endpoint
-            if ChannelValidation.open_id_metadata_endpoint
-            else AuthenticationConstants.TO_BOT_FROM_ENTERPRISE_CHANNEL_OPEN_ID_METADATA_URL_FORMAT.replace(
-                "{channelService}", channel_service
-            )
-        )
-        token_extractor = JwtTokenExtractor(
-            EnterpriseChannelValidation.TO_BOT_FROM_ENTERPRISE_CHANNEL_TOKEN_VALIDATION_PARAMETERS,
-            endpoint,
-            AuthenticationConstants.ALLOWED_SIGNING_ALGORITHMS,
-        )
-
-        identity: ClaimsIdentity = await token_extractor.get_identity_from_auth_header(
-            auth_header, channel_id
-        )
-        return await EnterpriseChannelValidation.validate_identity(
-            identity, credentials
-        )
-
-    @staticmethod
-    async def authenticate_channel_token_with_service_url(
-        auth_header: str,
-        credentials: CredentialProvider,
-        service_url: str,
-        channel_id: str,
-        channel_service: str,
-    ) -> ClaimsIdentity:
-        identity: ClaimsIdentity = await EnterpriseChannelValidation.authenticate_channel_token(
-            auth_header, credentials, channel_id, channel_service
-        )
-
-        service_url_claim: str = identity.get_claim_value(
-            AuthenticationConstants.SERVICE_URL_CLAIM
-        )
-        if service_url_claim != service_url:
-            raise Exception("Unauthorized. service_url claim do not match.")
-
-        return identity
-
-    @staticmethod
-    async def validate_identity(
-        identity: ClaimsIdentity, credentials: CredentialProvider
-    ) -> ClaimsIdentity:
-        if identity is None:
-            # No valid identity. Not Authorized.
-            raise Exception("Unauthorized. No valid identity.")
-
-        if not identity.is_authenticated:
-            # The token is in some way invalid. Not Authorized.
-            raise Exception("Unauthorized. Is not authenticated.")
-
-        # Now check that the AppID in the claim set matches
-        # what we're looking for. Note that in a multi-tenant bot, this value
-        # comes from developer code that may be reaching out to a service, hence the
-        # Async validation.
-
-        # Look for the "aud" claim, but only if issued from the Bot Framework
-        if (
-            identity.get_claim_value(AuthenticationConstants.ISSUER_CLAIM)
-            != AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER
-        ):
-            # The relevant Audience Claim MUST be present. Not Authorized.
-            raise Exception("Unauthorized. Issuer claim MUST be present.")
-
-        # The AppId from the claim in the token must match the AppId specified by the developer.
-        # In this case, the token is destined for the app, so we find the app ID in the audience claim.
-        aud_claim: str = identity.get_claim_value(
-            AuthenticationConstants.AUDIENCE_CLAIM
-        )
-        if not await credentials.is_valid_appid(aud_claim or ""):
-            # The AppId is not valid or not present. Not Authorized.
-            raise Exception(
-                f"Unauthorized. Invalid AppId passed on token: { aud_claim }"
-            )
-
-        return identity
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from abc import ABC
+from typing import Union
+
+from .authentication_configuration import AuthenticationConfiguration
+from .authentication_constants import AuthenticationConstants
+from .channel_validation import ChannelValidation
+from .channel_provider import ChannelProvider
+from .claims_identity import ClaimsIdentity
+from .credential_provider import CredentialProvider
+from .jwt_token_extractor import JwtTokenExtractor
+from .verify_options import VerifyOptions
+
+
+class EnterpriseChannelValidation(ABC):
+
+    TO_BOT_FROM_ENTERPRISE_CHANNEL_TOKEN_VALIDATION_PARAMETERS = VerifyOptions(
+        issuer=[AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER],
+        audience=None,
+        clock_tolerance=5 * 60,
+        ignore_expiration=False,
+    )
+
+    @staticmethod
+    async def authenticate_channel_token(
+        auth_header: str,
+        credentials: CredentialProvider,
+        channel_id: str,
+        channel_service_or_provider: Union[str, ChannelProvider],
+        auth_configuration: AuthenticationConfiguration = None,
+    ) -> ClaimsIdentity:
+        channel_service = channel_service_or_provider
+        if isinstance(channel_service_or_provider, ChannelProvider):
+            channel_service = await channel_service_or_provider.get_channel_service()
+
+        endpoint = (
+            ChannelValidation.open_id_metadata_endpoint
+            if ChannelValidation.open_id_metadata_endpoint
+            else AuthenticationConstants.TO_BOT_FROM_ENTERPRISE_CHANNEL_OPEN_ID_METADATA_URL_FORMAT.replace(
+                "{channelService}", channel_service
+            )
+        )
+        token_extractor = JwtTokenExtractor(
+            EnterpriseChannelValidation.TO_BOT_FROM_ENTERPRISE_CHANNEL_TOKEN_VALIDATION_PARAMETERS,
+            endpoint,
+            AuthenticationConstants.ALLOWED_SIGNING_ALGORITHMS,
+        )
+
+        identity: ClaimsIdentity = await token_extractor.get_identity_from_auth_header(
+            auth_header, channel_id, auth_configuration.required_endorsements
+        )
+        return await EnterpriseChannelValidation.validate_identity(
+            identity, credentials
+        )
+
+    @staticmethod
+    async def authenticate_channel_token_with_service_url(
+        auth_header: str,
+        credentials: CredentialProvider,
+        service_url: str,
+        channel_id: str,
+        channel_service_or_provider: Union[str, ChannelProvider],
+        auth_configuration: AuthenticationConfiguration = None,
+    ) -> ClaimsIdentity:
+        identity: ClaimsIdentity = await EnterpriseChannelValidation.authenticate_channel_token(
+            auth_header,
+            credentials,
+            channel_id,
+            channel_service_or_provider,
+            auth_configuration,
+        )
+
+        service_url_claim: str = identity.get_claim_value(
+            AuthenticationConstants.SERVICE_URL_CLAIM
+        )
+        if service_url_claim != service_url:
+            raise PermissionError("Unauthorized. service_url claim do not match.")
+
+        return identity
+
+    @staticmethod
+    async def validate_identity(
+        identity: ClaimsIdentity, credentials: CredentialProvider
+    ) -> ClaimsIdentity:
+        if identity is None:
+            # No valid identity. Not Authorized.
+            raise PermissionError("Unauthorized. No valid identity.")
+
+        if not identity.is_authenticated:
+            # The token is in some way invalid. Not Authorized.
+            raise PermissionError("Unauthorized. Is not authenticated.")
+
+        # Now check that the AppID in the claim set matches
+        # what we're looking for. Note that in a multi-tenant bot, this value
+        # comes from developer code that may be reaching out to a service, hence the
+        # Async validation.
+
+        # Look for the "aud" claim, but only if issued from the Bot Framework
+        if (
+            identity.get_claim_value(AuthenticationConstants.ISSUER_CLAIM)
+            != AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER
+        ):
+            # The relevant Audience Claim MUST be present. Not Authorized.
+            raise PermissionError("Unauthorized. Issuer claim MUST be present.")
+
+        # The AppId from the claim in the token must match the AppId specified by the developer.
+        # In this case, the token is destined for the app, so we find the app ID in the audience claim.
+        aud_claim: str = identity.get_claim_value(
+            AuthenticationConstants.AUDIENCE_CLAIM
+        )
+        if not await credentials.is_valid_appid(aud_claim or ""):
+            # The AppId is not valid or not present. Not Authorized.
+            raise PermissionError(
+                f"Unauthorized. Invalid AppId passed on token: { aud_claim }"
+            )
+
+        return identity
diff --git a/libraries/botframework-connector/botframework/connector/auth/government_channel_validation.py b/libraries/botframework-connector/botframework/connector/auth/government_channel_validation.py
index f4226be79..3c2285393 100644
--- a/libraries/botframework-connector/botframework/connector/auth/government_channel_validation.py
+++ b/libraries/botframework-connector/botframework/connector/auth/government_channel_validation.py
@@ -1,102 +1,108 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from abc import ABC
-
-from .authentication_constants import AuthenticationConstants
-from .claims_identity import ClaimsIdentity
-from .credential_provider import CredentialProvider
-from .government_constants import GovernmentConstants
-from .jwt_token_extractor import JwtTokenExtractor
-from .verify_options import VerifyOptions
-
-
-class GovernmentChannelValidation(ABC):
-
-    OPEN_ID_METADATA_ENDPOINT = ""
-
-    TO_BOT_FROM_GOVERNMENT_CHANNEL_TOKEN_VALIDATION_PARAMETERS = VerifyOptions(
-        issuer=[GovernmentConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER],
-        audience=None,
-        clock_tolerance=5 * 60,
-        ignore_expiration=False,
-    )
-
-    @staticmethod
-    async def authenticate_channel_token(
-        auth_header: str, credentials: CredentialProvider, channel_id: str
-    ) -> ClaimsIdentity:
-        endpoint = (
-            GovernmentChannelValidation.OPEN_ID_METADATA_ENDPOINT
-            if GovernmentChannelValidation.OPEN_ID_METADATA_ENDPOINT
-            else GovernmentConstants.TO_BOT_FROM_CHANNEL_OPEN_ID_METADATA_URL
-        )
-        token_extractor = JwtTokenExtractor(
-            GovernmentChannelValidation.TO_BOT_FROM_GOVERNMENT_CHANNEL_TOKEN_VALIDATION_PARAMETERS,
-            endpoint,
-            AuthenticationConstants.ALLOWED_SIGNING_ALGORITHMS,
-        )
-
-        identity: ClaimsIdentity = await token_extractor.get_identity_from_auth_header(
-            auth_header, channel_id
-        )
-        return await GovernmentChannelValidation.validate_identity(
-            identity, credentials
-        )
-
-    @staticmethod
-    async def authenticate_channel_token_with_service_url(
-        auth_header: str,
-        credentials: CredentialProvider,
-        service_url: str,
-        channel_id: str,
-    ) -> ClaimsIdentity:
-        identity: ClaimsIdentity = await GovernmentChannelValidation.authenticate_channel_token(
-            auth_header, credentials, channel_id
-        )
-
-        service_url_claim: str = identity.get_claim_value(
-            AuthenticationConstants.SERVICE_URL_CLAIM
-        )
-        if service_url_claim != service_url:
-            raise Exception("Unauthorized. service_url claim do not match.")
-
-        return identity
-
-    @staticmethod
-    async def validate_identity(
-        identity: ClaimsIdentity, credentials: CredentialProvider
-    ) -> ClaimsIdentity:
-        if identity is None:
-            # No valid identity. Not Authorized.
-            raise Exception("Unauthorized. No valid identity.")
-
-        if not identity.is_authenticated:
-            # The token is in some way invalid. Not Authorized.
-            raise Exception("Unauthorized. Is not authenticated.")
-
-        # Now check that the AppID in the claim set matches
-        # what we're looking for. Note that in a multi-tenant bot, this value
-        # comes from developer code that may be reaching out to a service, hence the
-        # Async validation.
-
-        # Look for the "aud" claim, but only if issued from the Bot Framework
-        if (
-            identity.get_claim_value(AuthenticationConstants.ISSUER_CLAIM)
-            != GovernmentConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER
-        ):
-            # The relevant Audience Claim MUST be present. Not Authorized.
-            raise Exception("Unauthorized. Issuer claim MUST be present.")
-
-        # The AppId from the claim in the token must match the AppId specified by the developer.
-        # In this case, the token is destined for the app, so we find the app ID in the audience claim.
-        aud_claim: str = identity.get_claim_value(
-            AuthenticationConstants.AUDIENCE_CLAIM
-        )
-        if not await credentials.is_valid_appid(aud_claim or ""):
-            # The AppId is not valid or not present. Not Authorized.
-            raise Exception(
-                f"Unauthorized. Invalid AppId passed on token: { aud_claim }"
-            )
-
-        return identity
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from abc import ABC
+
+from .authentication_configuration import AuthenticationConfiguration
+from .authentication_constants import AuthenticationConstants
+from .claims_identity import ClaimsIdentity
+from .credential_provider import CredentialProvider
+from .government_constants import GovernmentConstants
+from .jwt_token_extractor import JwtTokenExtractor
+from .verify_options import VerifyOptions
+
+
+class GovernmentChannelValidation(ABC):
+
+    OPEN_ID_METADATA_ENDPOINT = ""
+
+    TO_BOT_FROM_GOVERNMENT_CHANNEL_TOKEN_VALIDATION_PARAMETERS = VerifyOptions(
+        issuer=[GovernmentConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER],
+        audience=None,
+        clock_tolerance=5 * 60,
+        ignore_expiration=False,
+    )
+
+    @staticmethod
+    async def authenticate_channel_token(
+        auth_header: str,
+        credentials: CredentialProvider,
+        channel_id: str,
+        auth_configuration: AuthenticationConfiguration = None,
+    ) -> ClaimsIdentity:
+        auth_configuration = auth_configuration or AuthenticationConfiguration()
+        endpoint = (
+            GovernmentChannelValidation.OPEN_ID_METADATA_ENDPOINT
+            if GovernmentChannelValidation.OPEN_ID_METADATA_ENDPOINT
+            else GovernmentConstants.TO_BOT_FROM_CHANNEL_OPEN_ID_METADATA_URL
+        )
+        token_extractor = JwtTokenExtractor(
+            GovernmentChannelValidation.TO_BOT_FROM_GOVERNMENT_CHANNEL_TOKEN_VALIDATION_PARAMETERS,
+            endpoint,
+            AuthenticationConstants.ALLOWED_SIGNING_ALGORITHMS,
+        )
+
+        identity: ClaimsIdentity = await token_extractor.get_identity_from_auth_header(
+            auth_header, channel_id, auth_configuration.required_endorsements
+        )
+        return await GovernmentChannelValidation.validate_identity(
+            identity, credentials
+        )
+
+    @staticmethod
+    async def authenticate_channel_token_with_service_url(
+        auth_header: str,
+        credentials: CredentialProvider,
+        service_url: str,
+        channel_id: str,
+        auth_configuration: AuthenticationConfiguration = None,
+    ) -> ClaimsIdentity:
+        identity: ClaimsIdentity = await GovernmentChannelValidation.authenticate_channel_token(
+            auth_header, credentials, channel_id, auth_configuration
+        )
+
+        service_url_claim: str = identity.get_claim_value(
+            AuthenticationConstants.SERVICE_URL_CLAIM
+        )
+        if service_url_claim != service_url:
+            raise PermissionError("Unauthorized. service_url claim do not match.")
+
+        return identity
+
+    @staticmethod
+    async def validate_identity(
+        identity: ClaimsIdentity, credentials: CredentialProvider
+    ) -> ClaimsIdentity:
+        if identity is None:
+            # No valid identity. Not Authorized.
+            raise PermissionError("Unauthorized. No valid identity.")
+
+        if not identity.is_authenticated:
+            # The token is in some way invalid. Not Authorized.
+            raise PermissionError("Unauthorized. Is not authenticated.")
+
+        # Now check that the AppID in the claim set matches
+        # what we're looking for. Note that in a multi-tenant bot, this value
+        # comes from developer code that may be reaching out to a service, hence the
+        # Async validation.
+
+        # Look for the "aud" claim, but only if issued from the Bot Framework
+        if (
+            identity.get_claim_value(AuthenticationConstants.ISSUER_CLAIM)
+            != GovernmentConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER
+        ):
+            # The relevant Audience Claim MUST be present. Not Authorized.
+            raise PermissionError("Unauthorized. Issuer claim MUST be present.")
+
+        # The AppId from the claim in the token must match the AppId specified by the developer.
+        # In this case, the token is destined for the app, so we find the app ID in the audience claim.
+        aud_claim: str = identity.get_claim_value(
+            AuthenticationConstants.AUDIENCE_CLAIM
+        )
+        if not await credentials.is_valid_appid(aud_claim or ""):
+            # The AppId is not valid or not present. Not Authorized.
+            raise PermissionError(
+                f"Unauthorized. Invalid AppId passed on token: { aud_claim }"
+            )
+
+        return identity
diff --git a/libraries/botframework-connector/botframework/connector/auth/government_constants.py b/libraries/botframework-connector/botframework/connector/auth/government_constants.py
index 8dcb19b34..550eb3e3f 100644
--- a/libraries/botframework-connector/botframework/connector/auth/government_constants.py
+++ b/libraries/botframework-connector/botframework/connector/auth/government_constants.py
@@ -1,5 +1,6 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License.
+
 from abc import ABC
 
 
@@ -15,9 +16,7 @@ class GovernmentConstants(ABC):
     TO CHANNEL FROM BOT: Login URL
     """
     TO_CHANNEL_FROM_BOT_LOGIN_URL = (
-        "https://login.microsoftonline.us/"
-        "cab8a31a-1906-4287-a0d8-4eef66b95f6e/"
-        "oauth2/v2.0/token"
+        "https://login.microsoftonline.us/MicrosoftServices.onmicrosoft.us"
     )
 
     """
diff --git a/libraries/botframework-connector/botframework/connector/auth/jwt_token_extractor.py b/libraries/botframework-connector/botframework/connector/auth/jwt_token_extractor.py
index 043c0eccb..529ad00cb 100644
--- a/libraries/botframework-connector/botframework/connector/auth/jwt_token_extractor.py
+++ b/libraries/botframework-connector/botframework/connector/auth/jwt_token_extractor.py
@@ -3,6 +3,7 @@
 
 import json
 from datetime import datetime, timedelta
+from typing import List
 import requests
 from jwt.algorithms import RSAAlgorithm
 import jwt
@@ -33,17 +34,23 @@ def get_open_id_metadata(metadata_url: str):
         return metadata
 
     async def get_identity_from_auth_header(
-        self, auth_header: str, channel_id: str
+        self, auth_header: str, channel_id: str, required_endorsements: List[str] = None
     ) -> ClaimsIdentity:
         if not auth_header:
             return None
         parts = auth_header.split(" ")
         if len(parts) == 2:
-            return await self.get_identity(parts[0], parts[1], channel_id)
+            return await self.get_identity(
+                parts[0], parts[1], channel_id, required_endorsements
+            )
         return None
 
     async def get_identity(
-        self, schema: str, parameter: str, channel_id
+        self,
+        schema: str,
+        parameter: str,
+        channel_id: str,
+        required_endorsements: List[str] = None,
     ) -> ClaimsIdentity:
         # No header in correct scheme or no token
         if schema != "Bearer" or not parameter:
@@ -54,7 +61,9 @@ async def get_identity(
             return None
 
         try:
-            return await self._validate_token(parameter, channel_id)
+            return await self._validate_token(
+                parameter, channel_id, required_endorsements
+            )
         except Exception as error:
             raise error
 
@@ -64,9 +73,12 @@ def _has_allowed_issuer(self, jwt_token: str) -> bool:
         if issuer in self.validation_parameters.issuer:
             return True
 
-        return issuer is self.validation_parameters.issuer
+        return issuer == self.validation_parameters.issuer
 
-    async def _validate_token(self, jwt_token: str, channel_id: str) -> ClaimsIdentity:
+    async def _validate_token(
+        self, jwt_token: str, channel_id: str, required_endorsements: List[str] = None
+    ) -> ClaimsIdentity:
+        required_endorsements = required_endorsements or []
         headers = jwt.get_unverified_header(jwt_token)
 
         # Update the signing tokens from the last refresh
@@ -74,9 +86,18 @@ async def _validate_token(self, jwt_token: str, channel_id: str) -> ClaimsIdenti
         metadata = await self.open_id_metadata.get(key_id)
 
         if key_id and metadata.endorsements:
+            # Verify that channelId is included in endorsements
             if not EndorsementsValidator.validate(channel_id, metadata.endorsements):
                 raise Exception("Could not validate endorsement key")
 
+            # Verify that additional endorsements are satisfied.
+            # If no additional endorsements are expected, the requirement is satisfied as well
+            for endorsement in required_endorsements:
+                if not EndorsementsValidator.validate(
+                    endorsement, metadata.endorsements
+                ):
+                    raise Exception("Could not validate endorsement key")
+
         if headers.get("alg", None) not in self.validation_parameters.algorithms:
             raise Exception("Token signing algorithm not in allowed list")
 
@@ -84,7 +105,14 @@ async def _validate_token(self, jwt_token: str, channel_id: str) -> ClaimsIdenti
             "verify_aud": False,
             "verify_exp": not self.validation_parameters.ignore_expiration,
         }
-        decoded_payload = jwt.decode(jwt_token, metadata.public_key, options=options)
+
+        decoded_payload = jwt.decode(
+            jwt_token,
+            metadata.public_key,
+            leeway=self.validation_parameters.clock_tolerance,
+            options=options,
+        )
+
         claims = ClaimsIdentity(decoded_payload, True)
 
         return claims
@@ -98,7 +126,7 @@ def __init__(self, url):
 
     async def get(self, key_id: str):
         # If keys are more than 5 days old, refresh them
-        if self.last_updated < (datetime.now() + timedelta(days=5)):
+        if self.last_updated < (datetime.now() - timedelta(days=5)):
             await self._refresh()
         return self._find(key_id)
 
@@ -114,7 +142,7 @@ async def _refresh(self):
     def _find(self, key_id: str):
         if not self.keys:
             return None
-        key = next(x for x in self.keys if x["kid"] == key_id)
+        key = [x for x in self.keys if x["kid"] == key_id][0]
         public_key = RSAAlgorithm.from_jwk(json.dumps(key))
         endorsements = key.get("endorsements", [])
         return _OpenIdConfig(public_key, endorsements)
diff --git a/libraries/botframework-connector/botframework/connector/auth/jwt_token_validation.py b/libraries/botframework-connector/botframework/connector/auth/jwt_token_validation.py
index b67789a36..e83d6ccf6 100644
--- a/libraries/botframework-connector/botframework/connector/auth/jwt_token_validation.py
+++ b/libraries/botframework-connector/botframework/connector/auth/jwt_token_validation.py
@@ -1,111 +1,230 @@
-from botbuilder.schema import Activity
-
-from .emulator_validation import EmulatorValidation
-from .enterprise_channel_validation import EnterpriseChannelValidation
-from .channel_validation import ChannelValidation
-from .microsoft_app_credentials import MicrosoftAppCredentials
-from .credential_provider import CredentialProvider
-from .claims_identity import ClaimsIdentity
-from .government_constants import GovernmentConstants
-from .government_channel_validation import GovernmentChannelValidation
-
-
-class JwtTokenValidation:
-
-    # TODO remove the default value on channel_service
-    @staticmethod
-    async def authenticate_request(
-        activity: Activity,
-        auth_header: str,
-        credentials: CredentialProvider,
-        channel_service: str = "",
-    ) -> ClaimsIdentity:
-        """Authenticates the request and sets the service url in the set of trusted urls.
-        :param activity: The incoming Activity from the Bot Framework or the Emulator
-        :type activity: ~botframework.connector.models.Activity
-        :param auth_header: The Bearer token included as part of the request
-        :type auth_header: str
-        :param credentials: The set of valid credentials, such as the Bot Application ID
-        :param channel_service: String for the channel service
-        :type credentials: CredentialProvider
-
-        :raises Exception:
-        """
-        if not auth_header:
-            # No auth header was sent. We might be on the anonymous code path.
-            is_auth_disabled = await credentials.is_authentication_disabled()
-            if is_auth_disabled:
-                # We are on the anonymous code path.
-                return ClaimsIdentity({}, True)
-
-            # No Auth Header. Auth is required. Request is not authorized.
-            raise Exception("Unauthorized Access. Request is not authorized")
-
-        claims_identity = await JwtTokenValidation.validate_auth_header(
-            auth_header,
-            credentials,
-            channel_service,
-            activity.channel_id,
-            activity.service_url,
-        )
-
-        # On the standard Auth path, we need to trust the URL that was incoming.
-        MicrosoftAppCredentials.trust_service_url(activity.service_url)
-
-        return claims_identity
-
-    @staticmethod
-    async def validate_auth_header(
-        auth_header: str,
-        credentials: CredentialProvider,
-        channel_service: str,
-        channel_id: str,
-        service_url: str = None,
-    ) -> ClaimsIdentity:
-        if not auth_header:
-            raise ValueError("argument auth_header is null")
-
-        using_emulator = EmulatorValidation.is_token_from_emulator(auth_header)
-
-        if using_emulator:
-            return await EmulatorValidation.authenticate_emulator_token(
-                auth_header, credentials, channel_service, channel_id
-            )
-
-        # If the channel is Public Azure
-        if not channel_service:
-            if service_url:
-                return await ChannelValidation.authenticate_channel_token_with_service_url(
-                    auth_header, credentials, service_url, channel_id
-                )
-
-            return await ChannelValidation.authenticate_channel_token(
-                auth_header, credentials, channel_id
-            )
-
-        if JwtTokenValidation.is_government(channel_service):
-            if service_url:
-                return await GovernmentChannelValidation.authenticate_channel_token_with_service_url(
-                    auth_header, credentials, service_url, channel_id
-                )
-
-            return await GovernmentChannelValidation.authenticate_channel_token(
-                auth_header, credentials, channel_id
-            )
-
-        # Otherwise use Enterprise Channel Validation
-        if service_url:
-            return await EnterpriseChannelValidation.authenticate_channel_token_with_service_url(
-                auth_header, credentials, service_url, channel_id, channel_service
-            )
-
-        return await EnterpriseChannelValidation.authenticate_channel_token(
-            auth_header, credentials, channel_id, channel_service
-        )
-
-    @staticmethod
-    def is_government(channel_service: str) -> bool:
-        return (
-            channel_service
-            and channel_service.lower() == GovernmentConstants.CHANNEL_SERVICE
-        )
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from typing import Dict, List, Union
+
+from botbuilder.schema import Activity, RoleTypes
+
+from ..channels import Channels
+from .authentication_configuration import AuthenticationConfiguration
+from .authentication_constants import AuthenticationConstants
+from .emulator_validation import EmulatorValidation
+from .enterprise_channel_validation import EnterpriseChannelValidation
+from .channel_validation import ChannelValidation
+from .microsoft_app_credentials import MicrosoftAppCredentials
+from .credential_provider import CredentialProvider
+from .claims_identity import ClaimsIdentity
+from .government_constants import GovernmentConstants
+from .government_channel_validation import GovernmentChannelValidation
+from .skill_validation import SkillValidation
+from .channel_provider import ChannelProvider
+
+
+class JwtTokenValidation:
+
+    # TODO remove the default value on channel_service
+    @staticmethod
+    async def authenticate_request(
+        activity: Activity,
+        auth_header: str,
+        credentials: CredentialProvider,
+        channel_service_or_provider: Union[str, ChannelProvider] = "",
+        auth_configuration: AuthenticationConfiguration = None,
+    ) -> ClaimsIdentity:
+        """Authenticates the request and sets the service url in the set of trusted urls.
+        :param activity: The incoming Activity from the Bot Framework or the Emulator
+        :type activity: ~botframework.connector.models.Activity
+        :param auth_header: The Bearer token included as part of the request
+        :type auth_header: str
+        :param credentials: The set of valid credentials, such as the Bot Application ID
+        :param channel_service_or_provider: String for the channel service
+        :param auth_configuration: Authentication configuration
+        :type credentials: CredentialProvider
+
+        :raises Exception:
+        """
+        if not auth_header:
+            # No auth header was sent. We might be on the anonymous code path.
+            auth_is_disabled = await credentials.is_authentication_disabled()
+            if not auth_is_disabled:
+                # No Auth Header. Auth is required. Request is not authorized.
+                raise PermissionError("Unauthorized Access. Request is not authorized")
+
+            # Check if the activity is for a skill call and is coming from the Emulator.
+            try:
+                if (
+                    activity.channel_id == Channels.emulator
+                    and activity.recipient.role == RoleTypes.skill
+                    and activity.relates_to is not None
+                ):
+                    # Return an anonymous claim with an anonymous skill AppId
+                    return SkillValidation.create_anonymous_skill_claim()
+            except AttributeError:
+                pass
+
+            # In the scenario where Auth is disabled, we still want to have the
+            # IsAuthenticated flag set in the ClaimsIdentity. To do this requires
+            # adding in an empty claim.
+            return ClaimsIdentity({}, True, AuthenticationConstants.ANONYMOUS_AUTH_TYPE)
+
+        # Validate the header and extract claims.
+        claims_identity = await JwtTokenValidation.validate_auth_header(
+            auth_header,
+            credentials,
+            channel_service_or_provider,
+            activity.channel_id,
+            activity.service_url,
+            auth_configuration,
+        )
+
+        # On the standard Auth path, we need to trust the URL that was incoming.
+        MicrosoftAppCredentials.trust_service_url(activity.service_url)
+
+        return claims_identity
+
+    @staticmethod
+    async def validate_auth_header(
+        auth_header: str,
+        credentials: CredentialProvider,
+        channel_service_or_provider: Union[str, ChannelProvider],
+        channel_id: str,
+        service_url: str = None,
+        auth_configuration: AuthenticationConfiguration = None,
+    ) -> ClaimsIdentity:
+        if not auth_header:
+            raise ValueError("argument auth_header is null")
+
+        async def get_claims() -> ClaimsIdentity:
+            if SkillValidation.is_skill_token(auth_header):
+                return await SkillValidation.authenticate_channel_token(
+                    auth_header,
+                    credentials,
+                    channel_service_or_provider,
+                    channel_id,
+                    auth_configuration,
+                )
+
+            if EmulatorValidation.is_token_from_emulator(auth_header):
+                return await EmulatorValidation.authenticate_emulator_token(
+                    auth_header, credentials, channel_service_or_provider, channel_id
+                )
+
+            is_public = (
+                not channel_service_or_provider
+                or isinstance(channel_service_or_provider, ChannelProvider)
+                and channel_service_or_provider.is_public_azure()
+            )
+            is_gov = (
+                isinstance(channel_service_or_provider, ChannelProvider)
+                and channel_service_or_provider.is_public_azure()
+                or isinstance(channel_service_or_provider, str)
+                and JwtTokenValidation.is_government(channel_service_or_provider)
+            )
+
+            # If the channel is Public Azure
+            if is_public:
+                if service_url:
+                    return await ChannelValidation.authenticate_channel_token_with_service_url(
+                        auth_header,
+                        credentials,
+                        service_url,
+                        channel_id,
+                        auth_configuration,
+                    )
+
+                return await ChannelValidation.authenticate_channel_token(
+                    auth_header, credentials, channel_id, auth_configuration
+                )
+
+            if is_gov:
+                if service_url:
+                    return await GovernmentChannelValidation.authenticate_channel_token_with_service_url(
+                        auth_header,
+                        credentials,
+                        service_url,
+                        channel_id,
+                        auth_configuration,
+                    )
+
+                return await GovernmentChannelValidation.authenticate_channel_token(
+                    auth_header, credentials, channel_id, auth_configuration
+                )
+
+            # Otherwise use Enterprise Channel Validation
+            if service_url:
+                return await EnterpriseChannelValidation.authenticate_channel_token_with_service_url(
+                    auth_header,
+                    credentials,
+                    service_url,
+                    channel_id,
+                    channel_service_or_provider,
+                    auth_configuration,
+                )
+
+            return await EnterpriseChannelValidation.authenticate_channel_token(
+                auth_header,
+                credentials,
+                channel_id,
+                channel_service_or_provider,
+                auth_configuration,
+            )
+
+        claims = await get_claims()
+
+        if claims:
+            await JwtTokenValidation.validate_claims(auth_configuration, claims.claims)
+
+        return claims
+
+    @staticmethod
+    async def validate_claims(
+        auth_config: AuthenticationConfiguration, claims: List[Dict]
+    ):
+        if auth_config and auth_config.claims_validator:
+            await auth_config.claims_validator(claims)
+        elif SkillValidation.is_skill_claim(claims):
+            # Skill claims must be validated using AuthenticationConfiguration claims_validator
+            raise PermissionError(
+                "Unauthorized Access. Request is not authorized. Skill Claims require validation."
+            )
+
+    @staticmethod
+    def is_government(channel_service: str) -> bool:
+        return (
+            channel_service
+            and channel_service.lower() == GovernmentConstants.CHANNEL_SERVICE
+        )
+
+    @staticmethod
+    def get_app_id_from_claims(claims: Dict[str, object]) -> str:
+        app_id = None
+
+        # Depending on Version, the is either in the
+        # appid claim (Version 1) or the Authorized Party claim (Version 2).
+        token_version = claims.get(AuthenticationConstants.VERSION_CLAIM)
+
+        if not token_version or token_version == "1.0":
+            # either no Version or a version of "1.0" means we should look for
+            # the claim in the "appid" claim.
+            app_id = claims.get(AuthenticationConstants.APP_ID_CLAIM)
+        elif token_version == "2.0":
+            app_id = claims.get(AuthenticationConstants.AUTHORIZED_PARTY)
+
+        return app_id
+
+    @staticmethod
+    def is_valid_token_format(auth_header: str) -> bool:
+        if not auth_header:
+            # No token. Can't be an emulator token.
+            return False
+
+        parts = auth_header.split(" ")
+        if len(parts) != 2:
+            # Emulator tokens MUST have exactly 2 parts.
+            # If we don't have 2 parts, it's not an emulator token
+            return False
+
+        auth_scheme = parts[0]
+
+        # The scheme MUST be "Bearer"
+        return auth_scheme == "Bearer"
diff --git a/libraries/botframework-connector/botframework/connector/auth/microsoft_app_credentials.py b/libraries/botframework-connector/botframework/connector/auth/microsoft_app_credentials.py
index 05a4f1cd4..d625d6ede 100644
--- a/libraries/botframework-connector/botframework/connector/auth/microsoft_app_credentials.py
+++ b/libraries/botframework-connector/botframework/connector/auth/microsoft_app_credentials.py
@@ -1,177 +1,83 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License.
 
-from datetime import datetime, timedelta
-from urllib.parse import urlparse
-from msrest.authentication import BasicTokenAuthentication, Authentication
-import requests
-from .constants import Constants
-
-# TODO: Decide to move this to Constants or viceversa (when porting OAuth)
-AUTH_SETTINGS = {
-    "refreshEndpoint": "https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token",
-    "refreshScope": "https://api.botframework.com/.default",
-    "botConnectorOpenIdMetadata": "https://login.botframework.com/v1/.well-known/openidconfiguration",
-    "botConnectorIssuer": "https://api.botframework.com",
-    "emulatorOpenIdMetadata": "https://login.microsoftonline.com/botframework.com/v2.0/"
-    ".well-known/openid-configuration",
-    "emulatorAuthV31IssuerV1": "https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/",
-    "emulatorAuthV31IssuerV2": "https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0",
-    "emulatorAuthV32IssuerV1": "https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/",
-    "emulatorAuthV32IssuerV2": "https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0",
-}
+from abc import ABC
 
+import requests
+from msal import ConfidentialClientApplication
 
-class _OAuthResponse:
-    def __init__(self):
-        self.token_type = None
-        self.expires_in = None
-        self.access_token = None
-        self.expiration_time = None
+from .app_credentials import AppCredentials
 
-    @staticmethod
-    def from_json(json_values):
-        result = _OAuthResponse()
-        try:
-            result.token_type = json_values["token_type"]
-            result.access_token = json_values["access_token"]
-            result.expires_in = json_values["expires_in"]
-        except KeyError:
-            pass
-        return result
 
-
-class MicrosoftAppCredentials(Authentication):
+class MicrosoftAppCredentials(AppCredentials, ABC):
     """
-    MicrosoftAppCredentials auth implementation and cache.
+    AppCredentials implementation using application ID and password.
     """
 
-    schema = "Bearer"
-
-    trustedHostNames = {
-        "state.botframework.com": datetime.max,
-        "api.botframework.com": datetime.max,
-        "token.botframework.com": datetime.max,
-        "state.botframework.azure.us": datetime.max,
-        "api.botframework.azure.us": datetime.max,
-        "token.botframework.azure.us": datetime.max,
-    }
-    cache = {}
-
-    def __init__(self, app_id: str, password: str, channel_auth_tenant: str = None):
-        """
-        Initializes a new instance of MicrosoftAppCredentials class
-        :param app_id: The Microsoft app ID.
-        :param app_password: The Microsoft app password.
-        :param channel_auth_tenant: Optional. The oauth token tenant.
-        """
-        # The configuration property for the Microsoft app ID.
-        self.microsoft_app_id = app_id
-        # The configuration property for the Microsoft app Password.
-        self.microsoft_app_password = password
-        tenant = (
-            channel_auth_tenant
-            if channel_auth_tenant
-            else Constants.DEFAULT_CHANNEL_AUTH_TENANT
-        )
-        self.oauth_endpoint = (
-            Constants.TO_CHANNEL_FROM_BOT_LOGIN_URL_PREFIX
-            + tenant
-            + Constants.TO_CHANNEL_FROM_BOT_TOKEN_ENDPOINT_PATH
+    MICROSOFT_APP_ID = "MicrosoftAppId"
+    MICROSOFT_PASSWORD = "MicrosoftPassword"
+
+    def __init__(
+        self,
+        app_id: str,
+        password: str,
+        channel_auth_tenant: str = None,
+        oauth_scope: str = None,
+    ):
+        # super will set proper scope and endpoint.
+        super().__init__(
+            app_id=app_id,
+            channel_auth_tenant=channel_auth_tenant,
+            oauth_scope=oauth_scope,
         )
-        self.oauth_scope = AUTH_SETTINGS["refreshScope"]
-        self.token_cache_key = app_id + "-cache"
 
-    def signed_session(self) -> requests.Session:  # pylint: disable=arguments-differ
-        """
-        Gets the signed session.
-        :returns: Signed requests.Session object
-        """
-        auth_token = self.get_access_token()
+        self.microsoft_app_password = password
+        self.app = None
 
-        basic_authentication = BasicTokenAuthentication({"access_token": auth_token})
-        session = basic_authentication.signed_session()
+        # This check likely needs to be more nuanced than this.  Assuming
+        # "/.default" precludes other valid suffixes
+        scope = self.oauth_scope
+        if oauth_scope and not scope.endswith("/.default"):
+            scope += "/.default"
+        self.scopes = [scope]
 
-        # If there is no microsoft_app_id and no self.microsoft_app_password, then there shouldn't
-        # be an "Authorization" header on the outgoing activity.
-        if not self.microsoft_app_id and not self.microsoft_app_password:
-            del session.headers["Authorization"]
-        return session
+    @staticmethod
+    def empty():
+        return MicrosoftAppCredentials("", "")
 
     def get_access_token(self, force_refresh: bool = False) -> str:
         """
-        Gets an OAuth access token.
-        :param force_refresh: True to force a refresh of the token; or false to get
-                              a cached token if it exists.
-        :returns: Access token string
-        """
-        if self.microsoft_app_id and self.microsoft_app_password:
-            if not force_refresh:
-                # check the global cache for the token. If we have it, and it's valid, we're done.
-                oauth_token = MicrosoftAppCredentials.cache.get(
-                    self.token_cache_key, None
-                )
-                if oauth_token is not None:
-                    # we have the token. Is it valid?
-                    if oauth_token.expiration_time > datetime.now():
-                        return oauth_token.access_token
-            # We need to refresh the token, because:
-            #   1. The user requested it via the force_refresh parameter
-            #   2. We have it, but it's expired
-            #   3. We don't have it in the cache.
-            oauth_token = self.refresh_token()
-            MicrosoftAppCredentials.cache.setdefault(self.token_cache_key, oauth_token)
-            return oauth_token.access_token
-        return ""
-
-    def refresh_token(self) -> _OAuthResponse:
-        """
-        returns: _OAuthResponse
+        Implementation of AppCredentials.get_token.
+        :return: The access token for the given app id and password.
         """
-        options = {
-            "grant_type": "client_credentials",
-            "client_id": self.microsoft_app_id,
-            "client_secret": self.microsoft_app_password,
-            "scope": self.oauth_scope,
-        }
 
-        response = requests.post(self.oauth_endpoint, data=options)
-        response.raise_for_status()
-
-        oauth_response = _OAuthResponse.from_json(response.json())
-        oauth_response.expiration_time = datetime.now() + timedelta(
-            seconds=(oauth_response.expires_in - 300)
+        # Firstly, looks up a token from cache
+        # Since we are looking for token for the current app, NOT for an end user,
+        # notice we give account parameter as None.
+        auth_token = self.__get_msal_app().acquire_token_silent(
+            self.scopes, account=None
         )
-
-        return oauth_response
-
-    @staticmethod
-    def trust_service_url(service_url: str, expiration=None):
-        """
-        Checks if the service url is for a trusted host or not.
-        :param service_url: The service url.
-        :param expiration: The expiration time after which this service url is not trusted anymore.
-        :returns: True if the host of the service url is trusted; False otherwise.
+        if not auth_token:
+            # No suitable token exists in cache. Let's get a new one from AAD.
+            auth_token = self.__get_msal_app().acquire_token_for_client(
+                scopes=self.scopes
+            )
+        return auth_token["access_token"]
+
+    def __get_msal_app(self):
+        if not self.app:
+            self.app = ConfidentialClientApplication(
+                client_id=self.microsoft_app_id,
+                client_credential=self.microsoft_app_password,
+                authority=self.oauth_endpoint,
+            )
+
+        return self.app
+
+    def _should_authorize(self, session: requests.Session) -> bool:
         """
-        if expiration is None:
-            expiration = datetime.now() + timedelta(days=1)
-        host = urlparse(service_url).hostname
-        if host is not None:
-            MicrosoftAppCredentials.trustedHostNames[host] = expiration
-
-    @staticmethod
-    def is_trusted_service(service_url: str) -> bool:
+        Override of AppCredentials._should_authorize
+        :param session:
+        :return:
         """
-        Checks if the service url is for a trusted host or not.
-        :param service_url: The service url.
-        :returns: True if the host of the service url is trusted; False otherwise.
-        """
-        host = urlparse(service_url).hostname
-        if host is not None:
-            return MicrosoftAppCredentials._is_trusted_url(host)
-        return False
-
-    @staticmethod
-    def _is_trusted_url(host: str) -> bool:
-        expiration = MicrosoftAppCredentials.trustedHostNames.get(host, datetime.min)
-        return expiration > (datetime.now() - timedelta(minutes=5))
+        return self.microsoft_app_id and self.microsoft_app_password
diff --git a/libraries/botframework-connector/botframework/connector/auth/microsoft_government_app_credentials.py b/libraries/botframework-connector/botframework/connector/auth/microsoft_government_app_credentials.py
new file mode 100644
index 000000000..eb59fe941
--- /dev/null
+++ b/libraries/botframework-connector/botframework/connector/auth/microsoft_government_app_credentials.py
@@ -0,0 +1,27 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botframework.connector.auth import MicrosoftAppCredentials, GovernmentConstants
+
+
+class MicrosoftGovernmentAppCredentials(MicrosoftAppCredentials):
+    """
+    MicrosoftGovernmentAppCredentials auth implementation.
+    """
+
+    def __init__(
+        self,
+        app_id: str,
+        app_password: str,
+        channel_auth_tenant: str = None,
+        scope: str = None,
+    ):
+        super().__init__(app_id, app_password, channel_auth_tenant, scope)
+        self.oauth_endpoint = GovernmentConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL
+        self.oauth_scope = (
+            scope if scope else GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
+        )
+
+    @staticmethod
+    def empty():
+        return MicrosoftGovernmentAppCredentials("", "")
diff --git a/libraries/botframework-connector/botframework/connector/auth/simple_channel_provider.py b/libraries/botframework-connector/botframework/connector/auth/simple_channel_provider.py
new file mode 100644
index 000000000..a64998833
--- /dev/null
+++ b/libraries/botframework-connector/botframework/connector/auth/simple_channel_provider.py
@@ -0,0 +1,25 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from .channel_provider import ChannelProvider
+from .government_constants import GovernmentConstants
+
+
+class SimpleChannelProvider(ChannelProvider):
+    """
+    ChannelProvider interface. This interface allows Bots to provide their own
+    implementation for the configuration parameters to connect to a Bot.
+    Framework channel service.
+    """
+
+    def __init__(self, channel_service: str = None):
+        self.channel_service = channel_service
+
+    async def get_channel_service(self) -> str:
+        return self.channel_service
+
+    def is_government(self) -> bool:
+        return self.channel_service == GovernmentConstants.CHANNEL_SERVICE
+
+    def is_public_azure(self) -> bool:
+        return not self.channel_service
diff --git a/libraries/botframework-connector/botframework/connector/auth/skill_validation.py b/libraries/botframework-connector/botframework/connector/auth/skill_validation.py
new file mode 100644
index 000000000..c868d6f62
--- /dev/null
+++ b/libraries/botframework-connector/botframework/connector/auth/skill_validation.py
@@ -0,0 +1,185 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from datetime import timedelta
+from typing import Dict, Union
+
+import jwt
+
+from .authentication_configuration import AuthenticationConfiguration
+from .authentication_constants import AuthenticationConstants
+from .claims_identity import ClaimsIdentity
+from .credential_provider import CredentialProvider
+from .government_constants import GovernmentConstants
+from .verify_options import VerifyOptions
+from .jwt_token_extractor import JwtTokenExtractor
+from .channel_provider import ChannelProvider
+
+
+class SkillValidation:
+    # TODO: Remove circular dependcies after C# refactor
+    # pylint: disable=import-outside-toplevel
+
+    """
+    Validates JWT tokens sent to and from a Skill.
+    """
+
+    _token_validation_parameters = VerifyOptions(
+        issuer=[
+            "https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/",  # Auth v3.1, 1.0 token
+            "https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0",  # Auth v3.1, 2.0 token
+            "https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/",  # Auth v3.2, 1.0 token
+            "https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0",  # Auth v3.2, 2.0 token
+            "https://sts.windows.net/cab8a31a-1906-4287-a0d8-4eef66b95f6e/",  # Auth for US Gov, 1.0 token
+            "https://login.microsoftonline.us/cab8a31a-1906-4287-a0d8-4eef66b95f6e/v2.0",  # Auth for US Gov, 2.0 token
+            "https://login.microsoftonline.us/f8cdef31-a31e-4b4a-93e4-5f571e91255a/",  # Auth for US Gov, 1.0 token
+            "https://login.microsoftonline.us/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0",  # Auth for US Gov, 2.0 token
+        ],
+        audience=None,
+        clock_tolerance=timedelta(minutes=5),
+        ignore_expiration=False,
+    )
+
+    @staticmethod
+    def is_skill_token(auth_header: str) -> bool:
+        """
+        Determines if a given Auth header is from from a skill to bot or bot to skill request.
+        :param auth_header: Bearer Token, in the "Bearer [Long String]" Format.
+        :return bool:
+        """
+        from .jwt_token_validation import JwtTokenValidation
+
+        if not JwtTokenValidation.is_valid_token_format(auth_header):
+            return False
+
+        bearer_token = auth_header.split(" ")[1]
+
+        # Parse the Big Long String into an actual token.
+        token = jwt.decode(bearer_token, verify=False)
+        return SkillValidation.is_skill_claim(token)
+
+    @staticmethod
+    def is_skill_claim(claims: Dict[str, object]) -> bool:
+        """
+        Checks if the given list of claims represents a skill.
+        :param claims: A dict of claims.
+        :return bool:
+        """
+        if AuthenticationConstants.VERSION_CLAIM not in claims:
+            return False
+
+        if (
+            claims.get(AuthenticationConstants.APP_ID_CLAIM, None)
+            == AuthenticationConstants.ANONYMOUS_SKILL_APP_ID
+        ):
+            return True
+
+        audience = claims.get(AuthenticationConstants.AUDIENCE_CLAIM)
+
+        # The audience is https://api.botframework.com and not an appId.
+        if (
+            not audience
+            or audience == AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER
+        ):
+            return False
+
+        from .jwt_token_validation import JwtTokenValidation
+
+        app_id = JwtTokenValidation.get_app_id_from_claims(claims)
+
+        if not app_id:
+            return False
+
+        # Skill claims must contain and app ID and the AppID must be different than the audience.
+        return app_id != audience
+
+    @staticmethod
+    async def authenticate_channel_token(
+        auth_header: str,
+        credentials: CredentialProvider,
+        channel_service_or_provider: Union[str, ChannelProvider],
+        channel_id: str,
+        auth_configuration: AuthenticationConfiguration,
+    ) -> ClaimsIdentity:
+        if auth_configuration is None:
+            raise Exception(
+                "auth_configuration cannot be None in SkillValidation.authenticate_channel_token"
+            )
+
+        from .jwt_token_validation import JwtTokenValidation
+
+        if isinstance(channel_service_or_provider, ChannelProvider):
+            is_gov = channel_service_or_provider.is_government()
+        else:
+            is_gov = JwtTokenValidation.is_government(channel_service_or_provider)
+
+        open_id_metadata_url = (
+            GovernmentConstants.TO_BOT_FROM_EMULATOR_OPEN_ID_METADATA_URL
+            if is_gov
+            else AuthenticationConstants.TO_BOT_FROM_EMULATOR_OPEN_ID_METADATA_URL
+        )
+
+        token_extractor = JwtTokenExtractor(
+            SkillValidation._token_validation_parameters,
+            open_id_metadata_url,
+            AuthenticationConstants.ALLOWED_SIGNING_ALGORITHMS,
+        )
+
+        identity = await token_extractor.get_identity_from_auth_header(
+            auth_header, channel_id, auth_configuration.required_endorsements
+        )
+        await SkillValidation._validate_identity(identity, credentials)
+
+        return identity
+
+    @staticmethod
+    def create_anonymous_skill_claim():
+        """
+        Creates a ClaimsIdentity for an anonymous (unauthenticated) skill.
+        :return ClaimsIdentity:
+        """
+        return ClaimsIdentity(
+            {
+                AuthenticationConstants.APP_ID_CLAIM: AuthenticationConstants.ANONYMOUS_SKILL_APP_ID
+            },
+            True,
+            AuthenticationConstants.ANONYMOUS_AUTH_TYPE,
+        )
+
+    @staticmethod
+    async def _validate_identity(
+        identity: ClaimsIdentity, credentials: CredentialProvider
+    ):
+        if not identity:
+            # No valid identity. Not Authorized.
+            raise PermissionError("Invalid Identity")
+
+        if not identity.is_authenticated:
+            # The token is in some way invalid. Not Authorized.
+            raise PermissionError("Token Not Authenticated")
+
+        version_claim = identity.claims.get(AuthenticationConstants.VERSION_CLAIM)
+        if not version_claim:
+            # No version claim
+            raise PermissionError(
+                f"'{AuthenticationConstants.VERSION_CLAIM}' claim is required on skill Tokens."
+            )
+
+        # Look for the "aud" claim, but only if issued from the Bot Framework
+        audience_claim = identity.claims.get(AuthenticationConstants.AUDIENCE_CLAIM)
+        if not audience_claim:
+            # Claim is not present or doesn't have a value. Not Authorized.
+            raise PermissionError(
+                f"'{AuthenticationConstants.AUDIENCE_CLAIM}' claim is required on skill Tokens."
+            )
+
+        if not await credentials.is_valid_appid(audience_claim):
+            # The AppId is not valid. Not Authorized.
+            raise PermissionError("Invalid audience.")
+
+        from .jwt_token_validation import JwtTokenValidation
+
+        app_id = JwtTokenValidation.get_app_id_from_claims(identity.claims)
+        if not app_id:
+            # Invalid AppId
+            raise PermissionError("Invalid app_id.")
diff --git a/libraries/botframework-connector/botframework/connector/auth/verify_options.py b/libraries/botframework-connector/botframework/connector/auth/verify_options.py
index 9bec402f7..5a49e5a04 100644
--- a/libraries/botframework-connector/botframework/connector/auth/verify_options.py
+++ b/libraries/botframework-connector/botframework/connector/auth/verify_options.py
@@ -1,6 +1,13 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from datetime import timedelta
+from typing import List, Union
+
+
 class VerifyOptions:
     def __init__(self, issuer, audience, clock_tolerance, ignore_expiration):
-        self.issuer = issuer
-        self.audience = audience
-        self.clock_tolerance = clock_tolerance
-        self.ignore_expiration = ignore_expiration
+        self.issuer: Union[List[str], str] = issuer or []
+        self.audience: str = audience
+        self.clock_tolerance: Union[int, timedelta] = clock_tolerance or 0
+        self.ignore_expiration: bool = ignore_expiration or False
diff --git a/libraries/botframework-connector/botframework/connector/connector_client.py b/libraries/botframework-connector/botframework/connector/connector_client.py
index ab88ac9ae..db503016d 100644
--- a/libraries/botframework-connector/botframework/connector/connector_client.py
+++ b/libraries/botframework-connector/botframework/connector/connector_client.py
@@ -3,10 +3,6 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License. See License.txt in the project root for
 # license information.
-#
-# Code generated by Microsoft (R) AutoRest Code Generator.
-# Changes may cause incorrect behavior and will be lost if the code is
-# regenerated.
 # --------------------------------------------------------------------------
 
 from msrest.service_client import SDKClient
diff --git a/libraries/botframework-connector/botframework/connector/emulator_api_client.py b/libraries/botframework-connector/botframework/connector/emulator_api_client.py
index 6012456ca..ad83f96f7 100644
--- a/libraries/botframework-connector/botframework/connector/emulator_api_client.py
+++ b/libraries/botframework-connector/botframework/connector/emulator_api_client.py
@@ -2,13 +2,13 @@
 # Licensed under the MIT License.
 
 import requests
-from .auth import MicrosoftAppCredentials
+from .auth import AppCredentials
 
 
 class EmulatorApiClient:
     @staticmethod
     async def emulate_oauth_cards(
-        credentials: MicrosoftAppCredentials, emulator_url: str, emulate: bool
+        credentials: AppCredentials, emulator_url: str, emulate: bool
     ) -> bool:
         token = await credentials.get_token()
         request_url = (
diff --git a/libraries/botframework-connector/botframework/connector/models/__init__.py b/libraries/botframework-connector/botframework/connector/models/__init__.py
index 084330d3b..54eea3e77 100644
--- a/libraries/botframework-connector/botframework/connector/models/__init__.py
+++ b/libraries/botframework-connector/botframework/connector/models/__init__.py
@@ -3,10 +3,7 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License. See License.txt in the project root for
 # license information.
-#
-# Code generated by Microsoft (R) AutoRest Code Generator.
-# Changes may cause incorrect behavior and will be lost if the code is
-# regenerated.
 # --------------------------------------------------------------------------
 
 from botbuilder.schema import *
+from botbuilder.schema.teams import *
diff --git a/libraries/botframework-connector/botframework/connector/operations/__init__.py b/libraries/botframework-connector/botframework/connector/operations/__init__.py
index b2bc000ca..2476fcd20 100644
--- a/libraries/botframework-connector/botframework/connector/operations/__init__.py
+++ b/libraries/botframework-connector/botframework/connector/operations/__init__.py
@@ -3,10 +3,6 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License. See License.txt in the project root for
 # license information.
-#
-# Code generated by Microsoft (R) AutoRest Code Generator.
-# Changes may cause incorrect behavior and will be lost if the code is
-# regenerated.
 # --------------------------------------------------------------------------
 
 from ._attachments_operations import AttachmentsOperations
diff --git a/libraries/botframework-connector/botframework/connector/operations/_attachments_operations.py b/libraries/botframework-connector/botframework/connector/operations/_attachments_operations.py
index 03cce075d..d7d6287eb 100644
--- a/libraries/botframework-connector/botframework/connector/operations/_attachments_operations.py
+++ b/libraries/botframework-connector/botframework/connector/operations/_attachments_operations.py
@@ -3,10 +3,6 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License. See License.txt in the project root for
 # license information.
-#
-# Code generated by Microsoft (R) AutoRest Code Generator.
-# Changes may cause incorrect behavior and will be lost if the code is
-# regenerated.
 # --------------------------------------------------------------------------
 
 from msrest.pipeline import ClientRawResponse
diff --git a/libraries/botframework-connector/botframework/connector/operations/_conversations_operations.py b/libraries/botframework-connector/botframework/connector/operations/_conversations_operations.py
index c5a8bb68c..a4c37f6f4 100644
--- a/libraries/botframework-connector/botframework/connector/operations/_conversations_operations.py
+++ b/libraries/botframework-connector/botframework/connector/operations/_conversations_operations.py
@@ -3,10 +3,6 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License. See License.txt in the project root for
 # license information.
-#
-# Code generated by Microsoft (R) AutoRest Code Generator.
-# Changes may cause incorrect behavior and will be lost if the code is
-# regenerated.
 # --------------------------------------------------------------------------
 
 from msrest.pipeline import ClientRawResponse
@@ -655,6 +651,75 @@ def get_conversation_members(
         "url": "/v3/conversations/{conversationId}/members"
     }
 
+    def get_conversation_member(
+        self,
+        conversation_id,
+        member_id,
+        custom_headers=None,
+        raw=False,
+        **operation_config
+    ):
+        """GetConversationMember.
+
+        Get a member of a conversation.
+        This REST API takes a ConversationId and memberId and returns a
+        ChannelAccount object representing the member of the conversation.
+
+        :param conversation_id: Conversation Id
+        :type conversation_id: str
+        :param member_id: Member Id
+        :type member_id: str
+        :param dict custom_headers: headers that will be added to the request
+        :param bool raw: returns the direct response alongside the
+         deserialized response
+        :param operation_config: :ref:`Operation configuration
+         overrides`.
+        :return: list or ClientRawResponse if raw=true
+        :rtype: list[~botframework.connector.models.ChannelAccount] or
+         ~msrest.pipeline.ClientRawResponse
+        :raises:
+         :class:`ErrorResponseException`
+        """
+        # Construct URL
+        url = self.get_conversation_member.metadata["url"]
+        path_format_arguments = {
+            "conversationId": self._serialize.url(
+                "conversation_id", conversation_id, "str"
+            ),
+            "memberId": self._serialize.url("member_id", member_id, "str"),
+        }
+        url = self._client.format_url(url, **path_format_arguments)
+
+        # Construct parameters
+        query_parameters = {}
+
+        # Construct headers
+        header_parameters = {}
+        header_parameters["Accept"] = "application/json"
+        if custom_headers:
+            header_parameters.update(custom_headers)
+
+        # Construct and send request
+        request = self._client.get(url, query_parameters, header_parameters)
+        response = self._client.send(request, stream=False, **operation_config)
+
+        if response.status_code not in [200]:
+            raise models.ErrorResponseException(self._deserialize, response)
+
+        deserialized = None
+        if response.status_code == 200:
+            deserialized = self._deserialize("ChannelAccount", response)
+
+        if raw:
+            client_raw_response = ClientRawResponse(deserialized, response)
+            return client_raw_response
+
+        return deserialized
+
+    get_conversation_member.metadata = {
+        "url": "/v3/conversations/{conversationId}/members/{memberId}"
+    }
+
     def get_conversation_paged_members(
         self,
         conversation_id,
@@ -745,6 +810,96 @@ def get_conversation_paged_members(
         "url": "/v3/conversations/{conversationId}/pagedmembers"
     }
 
+    def get_teams_conversation_paged_members(
+        self,
+        conversation_id,
+        page_size=None,
+        continuation_token=None,
+        custom_headers=None,
+        raw=False,
+        **operation_config
+    ):
+        """GetTeamsConversationPagedMembers.
+
+        Enumerate the members of a Teams conversation one page at a time.
+        This REST API takes a ConversationId. Optionally a pageSize and/or
+        continuationToken can be provided. It returns a PagedMembersResult,
+        which contains an array
+        of ChannelAccounts representing the members of the conversation and a
+        continuation token that can be used to get more values.
+        One page of ChannelAccounts records are returned with each call. The
+        number of records in a page may vary between channels and calls. The
+        pageSize parameter can be used as
+        a suggestion. If there are no additional results the response will not
+        contain a continuation token. If there are no members in the
+        conversation the Members will be empty or not present in the response.
+        A response to a request that has a continuation token from a prior
+        request may rarely return members from a previous request.
+
+        :param conversation_id: Conversation ID
+        :type conversation_id: str
+        :param page_size: Suggested page size
+        :type page_size: int
+        :param continuation_token: Continuation Token
+        :type continuation_token: str
+        :param dict custom_headers: headers that will be added to the request
+        :param bool raw: returns the direct response alongside the
+         deserialized response
+        :param operation_config: :ref:`Operation configuration
+         overrides`.
+        :return: PagedMembersResult or ClientRawResponse if raw=true
+        :rtype: ~botframework.connector.models.PagedMembersResult or
+         ~msrest.pipeline.ClientRawResponse
+        :raises:
+         :class:`HttpOperationError`
+        """
+        # Construct URL
+        url = self.get_conversation_paged_members.metadata["url"]
+        path_format_arguments = {
+            "conversationId": self._serialize.url(
+                "conversation_id", conversation_id, "str"
+            )
+        }
+        url = self._client.format_url(url, **path_format_arguments)
+
+        # Construct parameters
+        query_parameters = {}
+        if page_size is not None:
+            query_parameters["pageSize"] = self._serialize.query(
+                "page_size", page_size, "int"
+            )
+        if continuation_token is not None:
+            query_parameters["continuationToken"] = self._serialize.query(
+                "continuation_token", continuation_token, "str"
+            )
+
+        # Construct headers
+        header_parameters = {}
+        header_parameters["Accept"] = "application/json"
+        if custom_headers:
+            header_parameters.update(custom_headers)
+
+        # Construct and send request
+        request = self._client.get(url, query_parameters, header_parameters)
+        response = self._client.send(request, stream=False, **operation_config)
+
+        if response.status_code not in [200]:
+            raise HttpOperationError(self._deserialize, response)
+
+        deserialized = None
+        if response.status_code == 200:
+            deserialized = self._deserialize("TeamsPagedMembersResult", response)
+
+        if raw:
+            client_raw_response = ClientRawResponse(deserialized, response)
+            return client_raw_response
+
+        return deserialized
+
+    get_conversation_paged_members.metadata = {
+        "url": "/v3/conversations/{conversationId}/pagedmembers"
+    }
+
     def delete_conversation_member(  # pylint: disable=inconsistent-return-statements
         self,
         conversation_id,
diff --git a/libraries/botframework-connector/botframework/connector/teams/__init__.py b/libraries/botframework-connector/botframework/connector/teams/__init__.py
new file mode 100644
index 000000000..48125ad74
--- /dev/null
+++ b/libraries/botframework-connector/botframework/connector/teams/__init__.py
@@ -0,0 +1,13 @@
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+# --------------------------------------------------------------------------
+
+from .teams_connector_client import TeamsConnectorClient
+from .version import VERSION
+
+__all__ = ["TeamsConnectorClient"]
+
+__version__ = VERSION
diff --git a/libraries/botframework-connector/botframework/connector/teams/operations/__init__.py b/libraries/botframework-connector/botframework/connector/teams/operations/__init__.py
new file mode 100644
index 000000000..326ddcf8d
--- /dev/null
+++ b/libraries/botframework-connector/botframework/connector/teams/operations/__init__.py
@@ -0,0 +1,12 @@
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+# --------------------------------------------------------------------------
+
+from .teams_operations import TeamsOperations
+
+__all__ = [
+    "TeamsOperations",
+]
diff --git a/libraries/botframework-connector/botframework/connector/teams/operations/teams_operations.py b/libraries/botframework-connector/botframework/connector/teams/operations/teams_operations.py
new file mode 100644
index 000000000..c53e2045f
--- /dev/null
+++ b/libraries/botframework-connector/botframework/connector/teams/operations/teams_operations.py
@@ -0,0 +1,214 @@
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+# --------------------------------------------------------------------------
+
+from msrest.pipeline import ClientRawResponse
+from msrest.exceptions import HttpOperationError
+
+from ... import models
+
+
+class TeamsOperations(object):
+    """TeamsOperations operations.
+
+    :param client: Client for service requests.
+    :param config: Configuration of service client.
+    :param serializer: An object model serializer.
+    :param deserializer: An object model deserializer.
+    """
+
+    models = models
+
+    def __init__(self, client, config, serializer, deserializer):
+
+        self._client = client
+        self._serialize = serializer
+        self._deserialize = deserializer
+
+        self.config = config
+
+    def get_teams_channels(
+        self, team_id, custom_headers=None, raw=False, **operation_config
+    ):
+        """Fetches channel list for a given team.
+
+        Fetch the channel list.
+
+        :param team_id: Team Id
+        :type team_id: str
+        :param dict custom_headers: headers that will be added to the request
+        :param bool raw: returns the direct response alongside the
+         deserialized response
+        :param operation_config: :ref:`Operation configuration
+         overrides`.
+        :return: ConversationList or ClientRawResponse if raw=true
+        :rtype: ~botframework.connector.teams.models.ConversationList or
+         ~msrest.pipeline.ClientRawResponse
+        :raises:
+         :class:`HttpOperationError`
+        """
+        # Construct URL
+        url = self.get_teams_channels.metadata["url"]
+        path_format_arguments = {
+            "teamId": self._serialize.url("team_id", team_id, "str")
+        }
+        url = self._client.format_url(url, **path_format_arguments)
+
+        # Construct parameters
+        query_parameters = {}
+
+        # Construct headers
+        header_parameters = {}
+        header_parameters["Accept"] = "application/json"
+        if custom_headers:
+            header_parameters.update(custom_headers)
+
+        # Construct and send request
+        request = self._client.get(url, query_parameters, header_parameters)
+        response = self._client.send(request, stream=False, **operation_config)
+
+        if response.status_code not in [200]:
+            raise HttpOperationError(self._deserialize, response)
+
+        deserialized = None
+
+        if response.status_code == 200:
+            deserialized = self._deserialize("ConversationList", response)
+
+        if raw:
+            client_raw_response = ClientRawResponse(deserialized, response)
+            return client_raw_response
+
+        return deserialized
+
+    get_teams_channels.metadata = {"url": "/v3/teams/{teamId}/conversations"}
+
+    def get_team_details(
+        self, team_id, custom_headers=None, raw=False, **operation_config
+    ):
+        """Fetches details related to a team.
+
+        Fetch details for a team.
+
+        :param team_id: Team Id
+        :type team_id: str
+        :param dict custom_headers: headers that will be added to the request
+        :param bool raw: returns the direct response alongside the
+         deserialized response
+        :param operation_config: :ref:`Operation configuration
+         overrides`.
+        :return: TeamDetails or ClientRawResponse if raw=true
+        :rtype: ~botframework.connector.teams.models.TeamDetails or
+         ~msrest.pipeline.ClientRawResponse
+        :raises:
+         :class:`HttpOperationError`
+        """
+        # Construct URL
+        url = self.get_team_details.metadata["url"]
+        path_format_arguments = {
+            "teamId": self._serialize.url("team_id", team_id, "str")
+        }
+        url = self._client.format_url(url, **path_format_arguments)
+
+        # Construct parameters
+        query_parameters = {}
+
+        # Construct headers
+        header_parameters = {}
+        header_parameters["Accept"] = "application/json"
+        if custom_headers:
+            header_parameters.update(custom_headers)
+
+        # Construct and send request
+        request = self._client.get(url, query_parameters, header_parameters)
+        response = self._client.send(request, stream=False, **operation_config)
+
+        if response.status_code not in [200]:
+            raise HttpOperationError(self._deserialize, response)
+
+        deserialized = None
+
+        if response.status_code == 200:
+            deserialized = self._deserialize("TeamDetails", response)
+
+        if raw:
+            client_raw_response = ClientRawResponse(deserialized, response)
+            return client_raw_response
+
+        return deserialized
+
+    get_team_details.metadata = {"url": "/v3/teams/{teamId}"}
+
+    def fetch_participant(
+        self,
+        meeting_id: str,
+        participant_id: str,
+        tenant_id: str,
+        custom_headers=None,
+        raw=False,
+        **operation_config
+    ):
+        """Fetches Teams meeting participant details.
+
+        :param meeting_id: Teams meeting id
+        :type meeting_id: str
+        :param participant_id: Teams meeting participant id
+        :type participant_id: str
+        :param tenant_id: Teams meeting tenant id
+        :type tenant_id: str
+        :param dict custom_headers: headers that will be added to the request
+        :param bool raw: returns the direct response alongside the
+         deserialized response
+        :param operation_config: :ref:`Operation configuration
+         overrides`.
+        :return: TeamsMeetingParticipant or ClientRawResponse if raw=true
+        :rtype: ~botframework.connector.teams.models.TeamsParticipantChannelAccount or
+         ~msrest.pipeline.ClientRawResponse
+        :raises:
+         :class:`HttpOperationError`
+        """
+
+        # Construct URL
+        url = self.fetch_participant.metadata["url"]
+        path_format_arguments = {
+            "meetingId": self._serialize.url("meeting_id", meeting_id, "str"),
+            "participantId": self._serialize.url(
+                "participant_id", participant_id, "str"
+            ),
+            "tenantId": self._serialize.url("tenant_id", tenant_id, "str"),
+        }
+        url = self._client.format_url(url, **path_format_arguments)
+
+        # Construct parameters
+        query_parameters = {}
+
+        # Construct headers
+        header_parameters = {}
+        header_parameters["Accept"] = "application/json"
+        if custom_headers:
+            header_parameters.update(custom_headers)
+
+        # Construct and send request
+        request = self._client.get(url, query_parameters, header_parameters)
+        response = self._client.send(request, stream=False, **operation_config)
+
+        if response.status_code not in [200]:
+            raise HttpOperationError(self._deserialize, response)
+
+        deserialized = None
+
+        if response.status_code == 200:
+            deserialized = self._deserialize("TeamsMeetingParticipant", response)
+
+        if raw:
+            client_raw_response = ClientRawResponse(deserialized, response)
+            return client_raw_response
+
+        return deserialized
+
+    fetch_participant.metadata = {
+        "url": "/v1/meetings/{meetingId}/participants/{participantId}?tenantId={tenantId}"
+    }
diff --git a/libraries/botframework-connector/botframework/connector/teams/teams_connector_client.py b/libraries/botframework-connector/botframework/connector/teams/teams_connector_client.py
new file mode 100644
index 000000000..73c3fec66
--- /dev/null
+++ b/libraries/botframework-connector/botframework/connector/teams/teams_connector_client.py
@@ -0,0 +1,79 @@
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+# --------------------------------------------------------------------------
+
+from msrest.service_client import SDKClient
+from msrest import Configuration, Serializer, Deserializer
+from .. import models
+from .version import VERSION
+from .operations.teams_operations import TeamsOperations
+
+
+class TeamsConnectorClientConfiguration(Configuration):
+    """Configuration for TeamsConnectorClient
+    Note that all parameters used to create this instance are saved as instance
+    attributes.
+
+    :param credentials: Subscription credentials which uniquely identify
+     client subscription.
+    :type credentials: None
+    :param str base_url: Service URL
+    """
+
+    def __init__(self, credentials, base_url=None):
+
+        if credentials is None:
+            raise ValueError("Parameter 'credentials' must not be None.")
+        if not base_url:
+            base_url = "https://api.botframework.com"
+
+        super(TeamsConnectorClientConfiguration, self).__init__(base_url)
+
+        self.add_user_agent("botframework-connector/{}".format(VERSION))
+
+        self.credentials = credentials
+
+
+class TeamsConnectorClient(SDKClient):
+    """The Bot Connector REST API extension for Microsoft Teams allows your bot to perform extended
+    operations on to Microsoft Teams channel configured in the
+    [Bot Framework Developer Portal](https://dev.botframework.com). The Connector service uses
+    industry-standard REST and JSON over HTTPS. Client libraries for this REST API are available. See below for a list.
+    Authentication for both the Bot Connector and Bot State REST APIs is accomplished with JWT Bearer tokens, and is
+    described in detail in the [Connector Authentication](https://docs.botframework.com/en-us/restapi/authentication)
+     document.
+    # Client Libraries for the Bot Connector REST API
+    * [Bot Builder for C#](https://docs.botframework.com/en-us/csharp/builder/sdkreference/)
+    * [Bot Builder for Node.js](https://docs.botframework.com/en-us/node/builder/overview/)
+    © 2016 Microsoft
+
+    :ivar config: Configuration for client.
+    :vartype config: TeamsConnectorClientConfiguration
+
+    :ivar teams: Teams operations
+    :vartype teams: botframework.connector.teams.operations.TeamsOperations
+
+    :param credentials: Subscription credentials which uniquely identify
+     client subscription.
+    :type credentials: None
+    :param str base_url: Service URL
+    """
+
+    def __init__(self, credentials, base_url=None):
+
+        self.config = TeamsConnectorClientConfiguration(credentials, base_url)
+        super(TeamsConnectorClient, self).__init__(self.config.credentials, self.config)
+
+        client_models = {
+            k: v for k, v in models.__dict__.items() if isinstance(v, type)
+        }
+        self.api_version = "v3"
+        self._serialize = Serializer(client_models)
+        self._deserialize = Deserializer(client_models)
+
+        self.teams = TeamsOperations(
+            self._client, self.config, self._serialize, self._deserialize
+        )
diff --git a/libraries/botframework-connector/botframework/connector/teams/version.py b/libraries/botframework-connector/botframework/connector/teams/version.py
new file mode 100644
index 000000000..059dc8b92
--- /dev/null
+++ b/libraries/botframework-connector/botframework/connector/teams/version.py
@@ -0,0 +1,8 @@
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+# --------------------------------------------------------------------------
+
+VERSION = "v3"
diff --git a/libraries/botframework-connector/botframework/connector/token_api/__init__.py b/libraries/botframework-connector/botframework/connector/token_api/__init__.py
index e15b7c0d4..284737f97 100644
--- a/libraries/botframework-connector/botframework/connector/token_api/__init__.py
+++ b/libraries/botframework-connector/botframework/connector/token_api/__init__.py
@@ -3,10 +3,6 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License. See License.txt in the project root for
 # license information.
-#
-# Code generated by Microsoft (R) AutoRest Code Generator.
-# Changes may cause incorrect behavior and will be lost if the code is
-# regenerated.
 # --------------------------------------------------------------------------
 
 from ._configuration import TokenApiClientConfiguration
diff --git a/libraries/botframework-connector/botframework/connector/token_api/_configuration.py b/libraries/botframework-connector/botframework/connector/token_api/_configuration.py
index ff26db8d8..dd94bf968 100644
--- a/libraries/botframework-connector/botframework/connector/token_api/_configuration.py
+++ b/libraries/botframework-connector/botframework/connector/token_api/_configuration.py
@@ -3,10 +3,6 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License. See License.txt in the project root for
 # license information.
-#
-# Code generated by Microsoft (R) AutoRest Code Generator.
-# Changes may cause incorrect behavior and will be lost if the code is
-# regenerated.
 # --------------------------------------------------------------------------
 
 from msrest import Configuration
diff --git a/libraries/botframework-connector/botframework/connector/token_api/_token_api_client.py b/libraries/botframework-connector/botframework/connector/token_api/_token_api_client.py
index 863dcb2e5..f4d34c744 100644
--- a/libraries/botframework-connector/botframework/connector/token_api/_token_api_client.py
+++ b/libraries/botframework-connector/botframework/connector/token_api/_token_api_client.py
@@ -3,10 +3,6 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License. See License.txt in the project root for
 # license information.
-#
-# Code generated by Microsoft (R) AutoRest Code Generator.
-# Changes may cause incorrect behavior and will be lost if the code is
-# regenerated.
 # --------------------------------------------------------------------------
 
 from msrest.service_client import SDKClient
diff --git a/libraries/botframework-connector/botframework/connector/token_api/aio/__init__.py b/libraries/botframework-connector/botframework/connector/token_api/aio/__init__.py
index 967abe5f8..eb69ef863 100644
--- a/libraries/botframework-connector/botframework/connector/token_api/aio/__init__.py
+++ b/libraries/botframework-connector/botframework/connector/token_api/aio/__init__.py
@@ -3,10 +3,6 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License. See License.txt in the project root for
 # license information.
-#
-# Code generated by Microsoft (R) AutoRest Code Generator.
-# Changes may cause incorrect behavior and will be lost if the code is
-# regenerated.
 # --------------------------------------------------------------------------
 
 from ._token_api_client_async import TokenApiClient
diff --git a/libraries/botframework-connector/botframework/connector/token_api/aio/_token_api_client_async.py b/libraries/botframework-connector/botframework/connector/token_api/aio/_token_api_client_async.py
index a72fed429..80eba06be 100644
--- a/libraries/botframework-connector/botframework/connector/token_api/aio/_token_api_client_async.py
+++ b/libraries/botframework-connector/botframework/connector/token_api/aio/_token_api_client_async.py
@@ -3,10 +3,6 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License. See License.txt in the project root for
 # license information.
-#
-# Code generated by Microsoft (R) AutoRest Code Generator.
-# Changes may cause incorrect behavior and will be lost if the code is
-# regenerated.
 # --------------------------------------------------------------------------
 
 from msrest.async_client import SDKClientAsync
diff --git a/libraries/botframework-connector/botframework/connector/token_api/aio/operations_async/__init__.py b/libraries/botframework-connector/botframework/connector/token_api/aio/operations_async/__init__.py
index 0c30a7ed3..8194c77fd 100644
--- a/libraries/botframework-connector/botframework/connector/token_api/aio/operations_async/__init__.py
+++ b/libraries/botframework-connector/botframework/connector/token_api/aio/operations_async/__init__.py
@@ -3,10 +3,6 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License. See License.txt in the project root for
 # license information.
-#
-# Code generated by Microsoft (R) AutoRest Code Generator.
-# Changes may cause incorrect behavior and will be lost if the code is
-# regenerated.
 # --------------------------------------------------------------------------
 
 from ._bot_sign_in_operations_async import BotSignInOperations
diff --git a/libraries/botframework-connector/botframework/connector/token_api/aio/operations_async/_bot_sign_in_operations_async.py b/libraries/botframework-connector/botframework/connector/token_api/aio/operations_async/_bot_sign_in_operations_async.py
index 1ec07e04c..385f14466 100644
--- a/libraries/botframework-connector/botframework/connector/token_api/aio/operations_async/_bot_sign_in_operations_async.py
+++ b/libraries/botframework-connector/botframework/connector/token_api/aio/operations_async/_bot_sign_in_operations_async.py
@@ -3,10 +3,6 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License. See License.txt in the project root for
 # license information.
-#
-# Code generated by Microsoft (R) AutoRest Code Generator.
-# Changes may cause incorrect behavior and will be lost if the code is
-# regenerated.
 # --------------------------------------------------------------------------
 
 from msrest.pipeline import ClientRawResponse
@@ -18,8 +14,8 @@
 class BotSignInOperations:
     """BotSignInOperations async operations.
 
-    You should not instantiate directly this class, but create a Client instance that will create it for you and attach
-     it as attribute.
+    You should not instantiate directly this class, but create a Client instance that will create it for you and
+    attach it as attribute.
 
     :param client: Client for service requests.
     :param config: Configuration of service client.
@@ -118,3 +114,81 @@ async def get_sign_in_url(
         return deserialized
 
     get_sign_in_url.metadata = {"url": "/api/botsignin/GetSignInUrl"}
+
+    async def get_sign_in_resource(
+        self,
+        state,
+        code_challenge=None,
+        emulator_url=None,
+        final_redirect=None,
+        *,
+        custom_headers=None,
+        raw=False,
+        **operation_config
+    ):
+        """
+
+        :param state:
+        :type state: str
+        :param code_challenge:
+        :type code_challenge: str
+        :param emulator_url:
+        :type emulator_url: str
+        :param final_redirect:
+        :type final_redirect: str
+        :param dict custom_headers: headers that will be added to the request
+        :param bool raw: returns the direct response alongside the
+         deserialized response
+        :param operation_config: :ref:`Operation configuration
+         overrides`.
+        :return: SignInUrlResponse or ClientRawResponse if raw=true
+        :rtype: ~botframework.tokenapi.models.SignInUrlResponse or
+         ~msrest.pipeline.ClientRawResponse
+        :raises:
+         :class:`HttpOperationError`
+        """
+        # Construct URL
+        url = self.get_sign_in_resource.metadata["url"]
+
+        # Construct parameters
+        query_parameters = {}
+        query_parameters["state"] = self._serialize.query("state", state, "str")
+        if code_challenge is not None:
+            query_parameters["code_challenge"] = self._serialize.query(
+                "code_challenge", code_challenge, "str"
+            )
+        if emulator_url is not None:
+            query_parameters["emulatorUrl"] = self._serialize.query(
+                "emulator_url", emulator_url, "str"
+            )
+        if final_redirect is not None:
+            query_parameters["finalRedirect"] = self._serialize.query(
+                "final_redirect", final_redirect, "str"
+            )
+
+        # Construct headers
+        header_parameters = {}
+        header_parameters["Accept"] = "application/json"
+        if custom_headers:
+            header_parameters.update(custom_headers)
+
+        # Construct and send request
+        request = self._client.get(url, query_parameters, header_parameters)
+        response = await self._client.async_send(
+            request, stream=False, **operation_config
+        )
+
+        if response.status_code not in [200]:
+            raise HttpOperationError(self._deserialize, response)
+
+        deserialized = None
+        if response.status_code == 200:
+            deserialized = self._deserialize("SignInUrlResponse", response)
+
+        if raw:
+            client_raw_response = ClientRawResponse(deserialized, response)
+            return client_raw_response
+
+        return deserialized
+
+    get_sign_in_resource.metadata = {"url": "/api/botsignin/GetSignInResource"}
diff --git a/libraries/botframework-connector/botframework/connector/token_api/aio/operations_async/_user_token_operations_async.py b/libraries/botframework-connector/botframework/connector/token_api/aio/operations_async/_user_token_operations_async.py
index 53fc2947a..5ac397d66 100644
--- a/libraries/botframework-connector/botframework/connector/token_api/aio/operations_async/_user_token_operations_async.py
+++ b/libraries/botframework-connector/botframework/connector/token_api/aio/operations_async/_user_token_operations_async.py
@@ -3,10 +3,6 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License. See License.txt in the project root for
 # license information.
-#
-# Code generated by Microsoft (R) AutoRest Code Generator.
-# Changes may cause incorrect behavior and will be lost if the code is
-# regenerated.
 # --------------------------------------------------------------------------
 
 from msrest.pipeline import ClientRawResponse
@@ -17,8 +13,8 @@
 class UserTokenOperations:
     """UserTokenOperations async operations.
 
-    You should not instantiate directly this class, but create a Client instance that will create it for you and attach
-     it as attribute.
+    You should not instantiate directly this class, but create a Client instance that will create it for you and
+    attach it as attribute.
 
     :param client: Client for service requests.
     :param config: Configuration of service client.
@@ -348,3 +344,89 @@ async def get_token_status(
         return deserialized
 
     get_token_status.metadata = {"url": "/api/usertoken/GetTokenStatus"}
+
+    async def exchange_async(
+        self,
+        user_id,
+        connection_name,
+        channel_id,
+        uri=None,
+        token=None,
+        *,
+        custom_headers=None,
+        raw=False,
+        **operation_config
+    ):
+        """
+
+        :param user_id:
+        :type user_id: str
+        :param connection_name:
+        :type connection_name: str
+        :param channel_id:
+        :type channel_id: str
+        :param uri:
+        :type uri: str
+        :param token:
+        :type token: str
+        :param dict custom_headers: headers that will be added to the request
+        :param bool raw: returns the direct response alongside the
+         deserialized response
+        :param operation_config: :ref:`Operation configuration
+         overrides`.
+        :return: object or ClientRawResponse if raw=true
+        :rtype: object or ~msrest.pipeline.ClientRawResponse
+        :raises:
+         :class:`ErrorResponseException`
+        """
+        exchange_request = models.TokenExchangeRequest(uri=uri, token=token)
+
+        # Construct URL
+        url = self.exchange_async.metadata["url"]
+
+        # Construct parameters
+        query_parameters = {}
+        query_parameters["userId"] = self._serialize.query("user_id", user_id, "str")
+        query_parameters["connectionName"] = self._serialize.query(
+            "connection_name", connection_name, "str"
+        )
+        query_parameters["channelId"] = self._serialize.query(
+            "channel_id", channel_id, "str"
+        )
+
+        # Construct headers
+        header_parameters = {}
+        header_parameters["Accept"] = "application/json"
+        header_parameters["Content-Type"] = "application/json; charset=utf-8"
+        if custom_headers:
+            header_parameters.update(custom_headers)
+
+        # Construct body
+        body_content = self._serialize.body(exchange_request, "TokenExchangeRequest")
+
+        # Construct and send request
+        request = self._client.post(
+            url, query_parameters, header_parameters, body_content
+        )
+        response = await self._client.async_send(
+            request, stream=False, **operation_config
+        )
+
+        if response.status_code not in [200, 400, 404]:
+            raise models.ErrorResponseException(self._deserialize, response)
+
+        deserialized = None
+        if response.status_code == 200:
+            deserialized = self._deserialize("TokenResponse", response)
+        if response.status_code == 400:
+            deserialized = self._deserialize("ErrorResponse", response)
+        if response.status_code == 404:
+            deserialized = self._deserialize("TokenResponse", response)
+
+        if raw:
+            client_raw_response = ClientRawResponse(deserialized, response)
+            return client_raw_response
+
+        return deserialized
+
+    exchange_async.metadata = {"url": "/api/usertoken/exchange"}
diff --git a/libraries/botframework-connector/botframework/connector/token_api/models/__init__.py b/libraries/botframework-connector/botframework/connector/token_api/models/__init__.py
index a4896757f..f4593e21a 100644
--- a/libraries/botframework-connector/botframework/connector/token_api/models/__init__.py
+++ b/libraries/botframework-connector/botframework/connector/token_api/models/__init__.py
@@ -3,10 +3,6 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License. See License.txt in the project root for
 # license information.
-#
-# Code generated by Microsoft (R) AutoRest Code Generator.
-# Changes may cause incorrect behavior and will be lost if the code is
-# regenerated.
 # --------------------------------------------------------------------------
 
 try:
@@ -14,6 +10,9 @@
     from ._models_py3 import Error
     from ._models_py3 import ErrorResponse, ErrorResponseException
     from ._models_py3 import InnerHttpError
+    from ._models_py3 import SignInUrlResponse
+    from ._models_py3 import TokenExchangeRequest
+    from ._models_py3 import TokenExchangeResource
     from ._models_py3 import TokenResponse
     from ._models_py3 import TokenStatus
 except (SyntaxError, ImportError):
@@ -21,6 +20,9 @@
     from ._models import Error
     from ._models import ErrorResponse, ErrorResponseException
     from ._models import InnerHttpError
+    from ._models import SignInUrlResponse
+    from ._models import TokenExchangeRequest
+    from ._models import TokenExchangeResource
     from ._models import TokenResponse
     from ._models import TokenStatus
 
@@ -30,6 +32,9 @@
     "ErrorResponse",
     "ErrorResponseException",
     "InnerHttpError",
+    "SignInUrlResponse",
+    "TokenExchangeRequest",
+    "TokenExchangeResource",
     "TokenResponse",
     "TokenStatus",
 ]
diff --git a/libraries/botframework-connector/botframework/connector/token_api/models/_models.py b/libraries/botframework-connector/botframework/connector/token_api/models/_models.py
index bf92ee596..63c1eedae 100644
--- a/libraries/botframework-connector/botframework/connector/token_api/models/_models.py
+++ b/libraries/botframework-connector/botframework/connector/token_api/models/_models.py
@@ -3,15 +3,13 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License. See License.txt in the project root for
 # license information.
-#
-# Code generated by Microsoft (R) AutoRest Code Generator.
-# Changes may cause incorrect behavior and will be lost if the code is
-# regenerated.
 # --------------------------------------------------------------------------
 
 from msrest.serialization import Model
 from msrest.exceptions import HttpOperationError
 
+# pylint: disable=invalid-name
+
 
 class AadResourceUrls(Model):
     """AadResourceUrls.
@@ -99,6 +97,74 @@ def __init__(self, **kwargs):
         self.body = kwargs.get("body", None)
 
 
+class SignInUrlResponse(Model):
+    """SignInUrlResponse.
+
+    :param sign_in_link:
+    :type sign_in_link: str
+    :param token_exchange_resource:
+    :type token_exchange_resource:
+     ~botframework.tokenapi.models.TokenExchangeResource
+    """
+
+    _attribute_map = {
+        "sign_in_link": {"key": "signInLink", "type": "str"},
+        "token_exchange_resource": {
+            "key": "tokenExchangeResource",
+            "type": "TokenExchangeResource",
+        },
+    }
+
+    def __init__(self, **kwargs):
+        super(SignInUrlResponse, self).__init__(**kwargs)
+        self.sign_in_link = kwargs.get("sign_in_link", None)
+        self.token_exchange_resource = kwargs.get("token_exchange_resource", None)
+
+
+class TokenExchangeRequest(Model):
+    """TokenExchangeRequest.
+
+    :param uri:
+    :type uri: str
+    :param token:
+    :type token: str
+    """
+
+    _attribute_map = {
+        "uri": {"key": "uri", "type": "str"},
+        "token": {"key": "token", "type": "str"},
+    }
+
+    def __init__(self, **kwargs):
+        super(TokenExchangeRequest, self).__init__(**kwargs)
+        self.uri = kwargs.get("uri", None)
+        self.token = kwargs.get("token", None)
+
+
+class TokenExchangeResource(Model):
+    """TokenExchangeResource.
+
+    :param id:
+    :type id: str
+    :param uri:
+    :type uri: str
+    :param provider_id:
+    :type provider_id: str
+    """
+
+    _attribute_map = {
+        "id": {"key": "id", "type": "str"},
+        "uri": {"key": "uri", "type": "str"},
+        "provider_id": {"key": "providerId", "type": "str"},
+    }
+
+    def __init__(self, **kwargs):
+        super(TokenExchangeResource, self).__init__(**kwargs)
+        self.id = kwargs.get("id", None)
+        self.uri = kwargs.get("uri", None)
+        self.provider_id = kwargs.get("provider_id", None)
+
+
 class TokenResponse(Model):
     """TokenResponse.
 
diff --git a/libraries/botframework-connector/botframework/connector/token_api/models/_models_py3.py b/libraries/botframework-connector/botframework/connector/token_api/models/_models_py3.py
index d5aee86de..271c532dc 100644
--- a/libraries/botframework-connector/botframework/connector/token_api/models/_models_py3.py
+++ b/libraries/botframework-connector/botframework/connector/token_api/models/_models_py3.py
@@ -3,15 +3,13 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License. See License.txt in the project root for
 # license information.
-#
-# Code generated by Microsoft (R) AutoRest Code Generator.
-# Changes may cause incorrect behavior and will be lost if the code is
-# regenerated.
 # --------------------------------------------------------------------------
 
 from msrest.serialization import Model
 from msrest.exceptions import HttpOperationError
 
+# pylint: disable=invalid-name
+
 
 class AadResourceUrls(Model):
     """AadResourceUrls.
@@ -101,6 +99,78 @@ def __init__(self, *, status_code: int = None, body=None, **kwargs) -> None:
         self.body = body
 
 
+class SignInUrlResponse(Model):
+    """SignInUrlResponse.
+
+    :param sign_in_link:
+    :type sign_in_link: str
+    :param token_exchange_resource:
+    :type token_exchange_resource:
+     ~botframework.tokenapi.models.TokenExchangeResource
+    """
+
+    _attribute_map = {
+        "sign_in_link": {"key": "signInLink", "type": "str"},
+        "token_exchange_resource": {
+            "key": "tokenExchangeResource",
+            "type": "TokenExchangeResource",
+        },
+    }
+
+    def __init__(
+        self, *, sign_in_link: str = None, token_exchange_resource=None, **kwargs
+    ) -> None:
+        super(SignInUrlResponse, self).__init__(**kwargs)
+        self.sign_in_link = sign_in_link
+        self.token_exchange_resource = token_exchange_resource
+
+
+class TokenExchangeRequest(Model):
+    """TokenExchangeRequest.
+
+    :param uri:
+    :type uri: str
+    :param token:
+    :type token: str
+    """
+
+    _attribute_map = {
+        "uri": {"key": "uri", "type": "str"},
+        "token": {"key": "token", "type": "str"},
+    }
+
+    def __init__(self, *, uri: str = None, token: str = None, **kwargs) -> None:
+        super(TokenExchangeRequest, self).__init__(**kwargs)
+        self.uri = uri
+        self.token = token
+
+
+class TokenExchangeResource(Model):
+    """TokenExchangeResource.
+
+    :param id:
+    :type id: str
+    :param uri:
+    :type uri: str
+    :param provider_id:
+    :type provider_id: str
+    """
+
+    _attribute_map = {
+        "id": {"key": "id", "type": "str"},
+        "uri": {"key": "uri", "type": "str"},
+        "provider_id": {"key": "providerId", "type": "str"},
+    }
+
+    def __init__(
+        self, *, id: str = None, uri: str = None, provider_id: str = None, **kwargs
+    ) -> None:
+        super(TokenExchangeResource, self).__init__(**kwargs)
+        self.id = id
+        self.uri = uri
+        self.provider_id = provider_id
+
+
 class TokenResponse(Model):
     """TokenResponse.
 
diff --git a/libraries/botframework-connector/botframework/connector/token_api/operations/__init__.py b/libraries/botframework-connector/botframework/connector/token_api/operations/__init__.py
index d860b4524..76df7af4e 100644
--- a/libraries/botframework-connector/botframework/connector/token_api/operations/__init__.py
+++ b/libraries/botframework-connector/botframework/connector/token_api/operations/__init__.py
@@ -3,10 +3,6 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License. See License.txt in the project root for
 # license information.
-#
-# Code generated by Microsoft (R) AutoRest Code Generator.
-# Changes may cause incorrect behavior and will be lost if the code is
-# regenerated.
 # --------------------------------------------------------------------------
 
 from ._bot_sign_in_operations import BotSignInOperations
diff --git a/libraries/botframework-connector/botframework/connector/token_api/operations/_bot_sign_in_operations.py b/libraries/botframework-connector/botframework/connector/token_api/operations/_bot_sign_in_operations.py
index f4c45037d..83f128b15 100644
--- a/libraries/botframework-connector/botframework/connector/token_api/operations/_bot_sign_in_operations.py
+++ b/libraries/botframework-connector/botframework/connector/token_api/operations/_bot_sign_in_operations.py
@@ -3,10 +3,6 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License. See License.txt in the project root for
 # license information.
-#
-# Code generated by Microsoft (R) AutoRest Code Generator.
-# Changes may cause incorrect behavior and will be lost if the code is
-# regenerated.
 # --------------------------------------------------------------------------
 
 from msrest.pipeline import ClientRawResponse
@@ -18,8 +14,8 @@
 class BotSignInOperations:
     """BotSignInOperations operations.
 
-    You should not instantiate directly this class, but create a Client instance that will create it for you and attach
-     it as attribute.
+    You should not instantiate directly this class, but create a Client instance that will create it for you and
+    attach it as attribute.
 
     :param client: Client for service requests.
     :param config: Configuration of service client.
@@ -115,3 +111,78 @@ def get_sign_in_url(
         return deserialized
 
     get_sign_in_url.metadata = {"url": "/api/botsignin/GetSignInUrl"}
+
+    def get_sign_in_resource(
+        self,
+        state,
+        code_challenge=None,
+        emulator_url=None,
+        final_redirect=None,
+        custom_headers=None,
+        raw=False,
+        **operation_config
+    ):
+        """
+
+        :param state:
+        :type state: str
+        :param code_challenge:
+        :type code_challenge: str
+        :param emulator_url:
+        :type emulator_url: str
+        :param final_redirect:
+        :type final_redirect: str
+        :param dict custom_headers: headers that will be added to the request
+        :param bool raw: returns the direct response alongside the
+         deserialized response
+        :param operation_config: :ref:`Operation configuration
+         overrides`.
+        :return: SignInUrlResponse or ClientRawResponse if raw=true
+        :rtype: ~botframework.tokenapi.models.SignInUrlResponse or
+         ~msrest.pipeline.ClientRawResponse
+        :raises:
+         :class:`HttpOperationError`
+        """
+        # Construct URL
+        url = self.get_sign_in_resource.metadata["url"]
+
+        # Construct parameters
+        query_parameters = {}
+        query_parameters["state"] = self._serialize.query("state", state, "str")
+        if code_challenge is not None:
+            query_parameters["code_challenge"] = self._serialize.query(
+                "code_challenge", code_challenge, "str"
+            )
+        if emulator_url is not None:
+            query_parameters["emulatorUrl"] = self._serialize.query(
+                "emulator_url", emulator_url, "str"
+            )
+        if final_redirect is not None:
+            query_parameters["finalRedirect"] = self._serialize.query(
+                "final_redirect", final_redirect, "str"
+            )
+
+        # Construct headers
+        header_parameters = {}
+        header_parameters["Accept"] = "application/json"
+        if custom_headers:
+            header_parameters.update(custom_headers)
+
+        # Construct and send request
+        request = self._client.get(url, query_parameters, header_parameters)
+        response = self._client.send(request, stream=False, **operation_config)
+
+        if response.status_code not in [200]:
+            raise HttpOperationError(self._deserialize, response)
+
+        deserialized = None
+        if response.status_code == 200:
+            deserialized = self._deserialize("SignInUrlResponse", response)
+
+        if raw:
+            client_raw_response = ClientRawResponse(deserialized, response)
+            return client_raw_response
+
+        return deserialized
+
+    get_sign_in_resource.metadata = {"url": "/api/botsignin/GetSignInResource"}
diff --git a/libraries/botframework-connector/botframework/connector/token_api/operations/_user_token_operations.py b/libraries/botframework-connector/botframework/connector/token_api/operations/_user_token_operations.py
index f154c7cd2..f63952571 100644
--- a/libraries/botframework-connector/botframework/connector/token_api/operations/_user_token_operations.py
+++ b/libraries/botframework-connector/botframework/connector/token_api/operations/_user_token_operations.py
@@ -3,10 +3,6 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License. See License.txt in the project root for
 # license information.
-#
-# Code generated by Microsoft (R) AutoRest Code Generator.
-# Changes may cause incorrect behavior and will be lost if the code is
-# regenerated.
 # --------------------------------------------------------------------------
 
 from msrest.pipeline import ClientRawResponse
@@ -18,7 +14,7 @@ class UserTokenOperations:
     """UserTokenOperations operations.
 
     You should not instantiate directly this class, but create a Client instance that will create it for you and attach
-     it as attribute.
+    it as attribute.
 
     :param client: Client for service requests.
     :param config: Configuration of service client.
@@ -336,3 +332,86 @@ def get_token_status(
         return deserialized
 
     get_token_status.metadata = {"url": "/api/usertoken/GetTokenStatus"}
+
+    def exchange_async(
+        self,
+        user_id,
+        connection_name,
+        channel_id,
+        uri=None,
+        token=None,
+        custom_headers=None,
+        raw=False,
+        **operation_config
+    ):
+        """
+
+        :param user_id:
+        :type user_id: str
+        :param connection_name:
+        :type connection_name: str
+        :param channel_id:
+        :type channel_id: str
+        :param uri:
+        :type uri: str
+        :param token:
+        :type token: str
+        :param dict custom_headers: headers that will be added to the request
+        :param bool raw: returns the direct response alongside the
+         deserialized response
+        :param operation_config: :ref:`Operation configuration
+         overrides`.
+        :return: object or ClientRawResponse if raw=true
+        :rtype: object or ~msrest.pipeline.ClientRawResponse
+        :raises:
+         :class:`ErrorResponseException`
+        """
+        exchange_request = models.TokenExchangeRequest(uri=uri, token=token)
+
+        # Construct URL
+        url = self.exchange_async.metadata["url"]
+
+        # Construct parameters
+        query_parameters = {}
+        query_parameters["userId"] = self._serialize.query("user_id", user_id, "str")
+        query_parameters["connectionName"] = self._serialize.query(
+            "connection_name", connection_name, "str"
+        )
+        query_parameters["channelId"] = self._serialize.query(
+            "channel_id", channel_id, "str"
+        )
+
+        # Construct headers
+        header_parameters = {}
+        header_parameters["Accept"] = "application/json"
+        header_parameters["Content-Type"] = "application/json; charset=utf-8"
+        if custom_headers:
+            header_parameters.update(custom_headers)
+
+        # Construct body
+        body_content = self._serialize.body(exchange_request, "TokenExchangeRequest")
+
+        # Construct and send request
+        request = self._client.post(
+            url, query_parameters, header_parameters, body_content
+        )
+        response = self._client.send(request, stream=False, **operation_config)
+
+        if response.status_code not in [200, 400, 404]:
+            raise models.ErrorResponseException(self._deserialize, response)
+
+        deserialized = None
+        if response.status_code == 200:
+            deserialized = self._deserialize("TokenResponse", response)
+        if response.status_code == 400:
+            deserialized = self._deserialize("ErrorResponse", response)
+        if response.status_code == 404:
+            deserialized = self._deserialize("TokenResponse", response)
+
+        if raw:
+            client_raw_response = ClientRawResponse(deserialized, response)
+            return client_raw_response
+
+        return deserialized
+
+    exchange_async.metadata = {"url": "/api/usertoken/exchange"}
diff --git a/libraries/botframework-connector/botframework/connector/token_api/version.py b/libraries/botframework-connector/botframework/connector/token_api/version.py
index c184fa4a9..1ca57ef7f 100644
--- a/libraries/botframework-connector/botframework/connector/token_api/version.py
+++ b/libraries/botframework-connector/botframework/connector/token_api/version.py
@@ -3,10 +3,6 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License. See License.txt in the project root for
 # license information.
-#
-# Code generated by Microsoft (R) AutoRest Code Generator.
-# Changes may cause incorrect behavior and will be lost if the code is
-# regenerated.
 # --------------------------------------------------------------------------
 
 VERSION = "token"
diff --git a/libraries/botframework-connector/botframework/connector/version.py b/libraries/botframework-connector/botframework/connector/version.py
index e36069e74..059dc8b92 100644
--- a/libraries/botframework-connector/botframework/connector/version.py
+++ b/libraries/botframework-connector/botframework/connector/version.py
@@ -3,10 +3,6 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License. See License.txt in the project root for
 # license information.
-#
-# Code generated by Microsoft (R) AutoRest Code Generator.
-# Changes may cause incorrect behavior and will be lost if the code is
-# regenerated.
 # --------------------------------------------------------------------------
 
 VERSION = "v3"
diff --git a/libraries/botframework-connector/requirements.txt b/libraries/botframework-connector/requirements.txt
index 123fc5791..d6fa1a0d1 100644
--- a/libraries/botframework-connector/requirements.txt
+++ b/libraries/botframework-connector/requirements.txt
@@ -1,5 +1,6 @@
-msrest>=0.6.6
-botbuilder-schema>=4.4.0b1
-requests>=2.18.1
+msrest==0.6.10
+botbuilder-schema==4.12.0
+requests==2.23.0
 PyJWT==1.5.3
-cryptography>=2.3.0
\ No newline at end of file
+cryptography==3.2
+msal==1.2.0
diff --git a/libraries/botframework-connector/setup.py b/libraries/botframework-connector/setup.py
index 87e25d465..59631e4cd 100644
--- a/libraries/botframework-connector/setup.py
+++ b/libraries/botframework-connector/setup.py
@@ -1,16 +1,19 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License.
+
 import os
 from setuptools import setup
 
 NAME = "botframework-connector"
-VERSION = os.environ["packageVersion"] if "packageVersion" in os.environ else "4.4.0b1"
+VERSION = os.environ["packageVersion"] if "packageVersion" in os.environ else "4.12.0"
 REQUIRES = [
-    "msrest>=0.6.6",
-    "requests>=2.8.1",
-    "cryptography>=2.3.0",
-    "PyJWT>=1.5.3",
-    "botbuilder-schema>=4.4.0b1",
+    "msrest==0.6.10",
+    "requests==2.23.0",
+    "cryptography==3.2",
+    "PyJWT==1.5.3",
+    "botbuilder-schema==4.12.0",
+    "adal==1.2.1",
+    "msal==1.6.0",
 ]
 
 root = os.path.abspath(os.path.dirname(__file__))
@@ -34,6 +37,8 @@
         "botframework.connector.models",
         "botframework.connector.aio",
         "botframework.connector.aio.operations_async",
+        "botframework.connector.teams",
+        "botframework.connector.teams.operations",
         "botframework.connector.token_api",
         "botframework.connector.token_api.aio",
         "botframework.connector.token_api.models",
@@ -48,7 +53,7 @@
         "Intended Audience :: Developers",
         "License :: OSI Approved :: MIT License",
         "Operating System :: OS Independent",
-        "Development Status :: 3 - Alpha",
+        "Development Status :: 5 - Production/Stable",
         "Topic :: Scientific/Engineering :: Artificial Intelligence",
     ],
 )
diff --git a/libraries/botframework-connector/tests/requirements.txt b/libraries/botframework-connector/tests/requirements.txt
index ca88e209f..0c8169787 100644
--- a/libraries/botframework-connector/tests/requirements.txt
+++ b/libraries/botframework-connector/tests/requirements.txt
@@ -1,4 +1,5 @@
 pytest-cov>=2.6.0
-pytest>=4.3.0
+pytest==5.2.2
 azure-devtools>=0.4.1
-pytest-asyncio
\ No newline at end of file
+pytest-asyncio==0.10.0
+ddt==1.2.1
\ No newline at end of file
diff --git a/libraries/botframework-connector/tests/test_app_credentials.py b/libraries/botframework-connector/tests/test_app_credentials.py
new file mode 100644
index 000000000..d56981e92
--- /dev/null
+++ b/libraries/botframework-connector/tests/test_app_credentials.py
@@ -0,0 +1,30 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import aiounittest
+from botframework.connector.auth import AppCredentials, AuthenticationConstants
+
+
+class AppCredentialsTests(aiounittest.AsyncTestCase):
+    @staticmethod
+    def test_should_not_send_token_for_anonymous():
+        # AppID is None
+        app_creds_none = AppCredentials(app_id=None)
+        assert app_creds_none.signed_session().headers.get("Authorization") is None
+
+        # AppID is anonymous skill
+        app_creds_anon = AppCredentials(
+            app_id=AuthenticationConstants.ANONYMOUS_SKILL_APP_ID
+        )
+        assert app_creds_anon.signed_session().headers.get("Authorization") is None
+
+
+def test_constructor():
+    should_default_to_channel_scope = AppCredentials()
+    assert (
+        should_default_to_channel_scope.oauth_scope
+        == AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
+    )
+
+    should_default_to_custom_scope = AppCredentials(oauth_scope="customScope")
+    assert should_default_to_custom_scope.oauth_scope == "customScope"
diff --git a/libraries/botframework-connector/tests/test_auth.py b/libraries/botframework-connector/tests/test_auth.py
index b3d904b4d..4e5c94745 100644
--- a/libraries/botframework-connector/tests/test_auth.py
+++ b/libraries/botframework-connector/tests/test_auth.py
@@ -1,34 +1,54 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License.
 
+import uuid
+from typing import Dict, List, Union
+from unittest.mock import Mock
+
 import pytest
 
-from botbuilder.schema import Activity
-from botframework.connector.auth import JwtTokenValidation
-from botframework.connector.auth import SimpleCredentialProvider
-from botframework.connector.auth import EmulatorValidation
-from botframework.connector.auth import EnterpriseChannelValidation
-from botframework.connector.auth import ChannelValidation
-from botframework.connector.auth import ClaimsIdentity
-from botframework.connector.auth import MicrosoftAppCredentials
-from botframework.connector.auth import GovernmentConstants
-from botframework.connector.auth import GovernmentChannelValidation
+from botbuilder.schema import Activity, ConversationReference, ChannelAccount, RoleTypes
+from botframework.connector import Channels
+from botframework.connector.auth import (
+    AuthenticationConfiguration,
+    AuthenticationConstants,
+    JwtTokenValidation,
+    SimpleCredentialProvider,
+    EmulatorValidation,
+    EnterpriseChannelValidation,
+    ChannelValidation,
+    ClaimsIdentity,
+    MicrosoftAppCredentials,
+    GovernmentConstants,
+    GovernmentChannelValidation,
+    SimpleChannelProvider,
+    ChannelProvider,
+    AppCredentials,
+)
 
 
 async def jwt_token_validation_validate_auth_header_with_channel_service_succeeds(
-    app_id: str, pwd: str, channel_service: str, header: str = None
+    app_id: str,
+    pwd: str,
+    channel_service_or_provider: Union[str, ChannelProvider],
+    header: str = None,
 ):
     if header is None:
         header = f"Bearer {MicrosoftAppCredentials(app_id, pwd).get_access_token()}"
 
     credentials = SimpleCredentialProvider(app_id, pwd)
     result = await JwtTokenValidation.validate_auth_header(
-        header, credentials, channel_service, "", "https://webchat.botframework.com/"
+        header,
+        credentials,
+        channel_service_or_provider,
+        "",
+        "https://webchat.botframework.com/",
     )
 
     assert result.is_authenticated
 
 
+# TODO: Consider changing to unittest to use ddt for Credentials tests
 class TestAuth:
     EmulatorValidation.TO_BOT_FROM_EMULATOR_TOKEN_VALIDATION_PARAMETERS.ignore_expiration = (
         True
@@ -37,9 +57,57 @@ class TestAuth:
         True
     )
 
+    @pytest.mark.asyncio
+    async def test_claims_validation(self):
+        claims: List[Dict] = []
+        default_auth_config = AuthenticationConfiguration()
+
+        # No validator should pass.
+        await JwtTokenValidation.validate_claims(default_auth_config, claims)
+
+        mock_validator = Mock()
+        auth_with_validator = AuthenticationConfiguration(
+            claims_validator=mock_validator
+        )
+
+        # Configure IClaimsValidator to fail
+        mock_validator.side_effect = PermissionError("Invalid claims.")
+        with pytest.raises(PermissionError) as excinfo:
+            await JwtTokenValidation.validate_claims(auth_with_validator, claims)
+
+        assert "Invalid claims." in str(excinfo.value)
+
+        # No validator with not skill cliams should pass.
+        default_auth_config.claims_validator = None
+        claims: List[Dict] = {
+            AuthenticationConstants.VERSION_CLAIM: "1.0",
+            AuthenticationConstants.AUDIENCE_CLAIM: "this_bot_id",
+            AuthenticationConstants.APP_ID_CLAIM: "this_bot_id",  # Skill claims aud!=azp
+        }
+
+        await JwtTokenValidation.validate_claims(default_auth_config, claims)
+
+        # No validator with skill cliams should fail.
+        claims: List[Dict] = {
+            AuthenticationConstants.VERSION_CLAIM: "1.0",
+            AuthenticationConstants.AUDIENCE_CLAIM: "this_bot_id",
+            AuthenticationConstants.APP_ID_CLAIM: "not_this_bot_id",  # Skill claims aud!=azp
+        }
+
+        mock_validator.side_effect = PermissionError(
+            "Unauthorized Access. Request is not authorized. Skill Claims require validation."
+        )
+        with pytest.raises(PermissionError) as excinfo_skill:
+            await JwtTokenValidation.validate_claims(auth_with_validator, claims)
+
+        assert (
+            "Unauthorized Access. Request is not authorized. Skill Claims require validation."
+            in str(excinfo_skill.value)
+        )
+
     @pytest.mark.asyncio
     async def test_connector_auth_header_correct_app_id_and_service_url_should_validate(
-        self
+        self,
     ):
         header = (
             "Bearer "
@@ -54,11 +122,19 @@ async def test_connector_auth_header_correct_app_id_and_service_url_should_valid
             header, credentials, "", "https://webchat.botframework.com/"
         )
 
+        result_with_provider = await JwtTokenValidation.validate_auth_header(
+            header,
+            credentials,
+            SimpleChannelProvider(),
+            "https://webchat.botframework.com/",
+        )
+
         assert result
+        assert result_with_provider
 
     @pytest.mark.asyncio
     async def test_connector_auth_header_with_different_bot_app_id_should_not_validate(
-        self
+        self,
     ):
         header = (
             "Bearer "
@@ -75,6 +151,15 @@ async def test_connector_auth_header_with_different_bot_app_id_should_not_valida
             )
         assert "Unauthorized" in str(excinfo.value)
 
+        with pytest.raises(Exception) as excinfo2:
+            await JwtTokenValidation.validate_auth_header(
+                header,
+                credentials,
+                SimpleChannelProvider(),
+                "https://webchat.botframework.com/",
+            )
+        assert "Unauthorized" in str(excinfo2.value)
+
     @pytest.mark.asyncio
     async def test_connector_auth_header_and_no_credential_should_not_validate(self):
         header = (
@@ -90,6 +175,15 @@ async def test_connector_auth_header_and_no_credential_should_not_validate(self)
             )
         assert "Unauthorized" in str(excinfo.value)
 
+        with pytest.raises(Exception) as excinfo2:
+            await JwtTokenValidation.validate_auth_header(
+                header,
+                credentials,
+                SimpleChannelProvider(),
+                "https://webchat.botframework.com/",
+            )
+        assert "Unauthorized" in str(excinfo2.value)
+
     @pytest.mark.asyncio
     async def test_empty_header_and_no_credential_should_throw(self):
         header = ""
@@ -98,9 +192,15 @@ async def test_empty_header_and_no_credential_should_throw(self):
             await JwtTokenValidation.validate_auth_header(header, credentials, "", None)
         assert "auth_header" in str(excinfo.value)
 
+        with pytest.raises(Exception) as excinfo2:
+            await JwtTokenValidation.validate_auth_header(
+                header, credentials, SimpleChannelProvider(), None
+            )
+        assert "auth_header" in str(excinfo2.value)
+
     @pytest.mark.asyncio
     async def test_emulator_msa_header_correct_app_id_and_service_url_should_validate(
-        self
+        self,
     ):
         header = (
             "Bearer "
@@ -115,10 +215,19 @@ async def test_emulator_msa_header_correct_app_id_and_service_url_should_validat
             header, credentials, "", "https://webchat.botframework.com/"
         )
 
+        result_with_provider = await JwtTokenValidation.validate_auth_header(
+            header,
+            credentials,
+            SimpleChannelProvider(),
+            "https://webchat.botframework.com/",
+        )
+
         assert result
+        assert result_with_provider
 
     @pytest.mark.asyncio
     async def test_emulator_msa_header_and_no_credential_should_not_validate(self):
+        # pylint: disable=protected-access
         header = (
             "Bearer "
             + MicrosoftAppCredentials(
@@ -130,7 +239,13 @@ async def test_emulator_msa_header_and_no_credential_should_not_validate(self):
         )
         with pytest.raises(Exception) as excinfo:
             await JwtTokenValidation.validate_auth_header(header, credentials, "", None)
-            assert "Unauthorized" in excinfo
+        assert "Unauthorized" in str(excinfo._excinfo)
+
+        with pytest.raises(Exception) as excinfo2:
+            await JwtTokenValidation.validate_auth_header(
+                header, credentials, SimpleChannelProvider(), None
+            )
+        assert "Unauthorized" in str(excinfo2._excinfo)
 
     # Tests with a valid Token and service url; and ensures that Service url is added to Trusted service url list.
     @pytest.mark.asyncio
@@ -150,7 +265,7 @@ async def test_channel_msa_header_valid_service_url_should_be_trusted(self):
 
         await JwtTokenValidation.authenticate_request(activity, header, credentials)
 
-        assert MicrosoftAppCredentials.is_trusted_service(
+        assert AppCredentials.is_trusted_service(
             "https://smba.trafficmanager.net/amer-client-ss.msg/"
         )
 
@@ -177,6 +292,32 @@ async def test_channel_msa_header_invalid_service_url_should_not_be_trusted(self
             "https://webchat.botframework.com/"
         )
 
+    @pytest.mark.asyncio
+    # Tests with a valid Token and invalid service url and ensures that Service url is NOT added to
+    # Trusted service url list.
+    async def test_channel_authentication_disabled_and_skill_should_be_anonymous(self):
+        activity = Activity(
+            channel_id=Channels.emulator,
+            service_url="https://webchat.botframework.com/",
+            relates_to=ConversationReference(),
+            recipient=ChannelAccount(role=RoleTypes.skill),
+        )
+        header = ""
+        credentials = SimpleCredentialProvider("", "")
+
+        claims_principal = await JwtTokenValidation.authenticate_request(
+            activity, header, credentials
+        )
+
+        assert (
+            claims_principal.authentication_type
+            == AuthenticationConstants.ANONYMOUS_AUTH_TYPE
+        )
+        assert (
+            JwtTokenValidation.get_app_id_from_claims(claims_principal.claims)
+            == AuthenticationConstants.ANONYMOUS_SKILL_APP_ID
+        )
+
     @pytest.mark.asyncio
     async def test_channel_msa_header_from_user_specified_tenant(self):
         activity = Activity(
@@ -206,13 +347,15 @@ async def test_channel_authentication_disabled_should_be_anonymous(self):
             activity, header, credentials
         )
 
-        assert claims_principal.is_authenticated
-        assert not claims_principal.claims
+        assert (
+            claims_principal.authentication_type
+            == AuthenticationConstants.ANONYMOUS_AUTH_TYPE
+        )
 
     @pytest.mark.asyncio
     # Tests with no authentication header and makes sure the service URL is not added to the trusted list.
     async def test_channel_authentication_disabled_service_url_should_not_be_trusted(
-        self
+        self,
     ):
         activity = Activity(service_url="https://webchat.botframework.com/")
         header = ""
@@ -226,7 +369,7 @@ async def test_channel_authentication_disabled_service_url_should_not_be_trusted
 
     @pytest.mark.asyncio
     async def test_emulator_auth_header_correct_app_id_and_service_url_with_gov_channel_service_should_validate(
-        self
+        self,
     ):
         await jwt_token_validation_validate_auth_header_with_channel_service_succeeds(
             "2cd87869-38a0-4182-9251-d056e8f0ac24",  # emulator creds
@@ -234,9 +377,15 @@ async def test_emulator_auth_header_correct_app_id_and_service_url_with_gov_chan
             GovernmentConstants.CHANNEL_SERVICE,
         )
 
+        await jwt_token_validation_validate_auth_header_with_channel_service_succeeds(
+            "2cd87869-38a0-4182-9251-d056e8f0ac24",  # emulator creds
+            "2.30Vs3VQLKt974F",
+            SimpleChannelProvider(GovernmentConstants.CHANNEL_SERVICE),
+        )
+
     @pytest.mark.asyncio
     async def test_emulator_auth_header_correct_app_id_and_service_url_with_private_channel_service_should_validate(
-        self
+        self,
     ):
         await jwt_token_validation_validate_auth_header_with_channel_service_succeeds(
             "2cd87869-38a0-4182-9251-d056e8f0ac24",  # emulator creds
@@ -244,6 +393,12 @@ async def test_emulator_auth_header_correct_app_id_and_service_url_with_private_
             "TheChannel",
         )
 
+        await jwt_token_validation_validate_auth_header_with_channel_service_succeeds(
+            "2cd87869-38a0-4182-9251-d056e8f0ac24",  # emulator creds
+            "2.30Vs3VQLKt974F",
+            SimpleChannelProvider("TheChannel"),
+        )
+
     @pytest.mark.asyncio
     async def test_government_channel_validation_succeeds(self):
         credentials = SimpleCredentialProvider(
@@ -381,3 +536,28 @@ async def test_enterprise_channel_validation_wrong_audience_fails(self):
                 credentials,
             )
         assert "Unauthorized" in str(excinfo.value)
+
+    def test_get_app_id_from_claims(self):
+        v1_claims = {}
+        v2_claims = {}
+
+        app_id = str(uuid.uuid4())
+
+        # Empty list
+        assert not JwtTokenValidation.get_app_id_from_claims(v1_claims)
+
+        # AppId there but no version (assumes v1)
+        v1_claims[AuthenticationConstants.APP_ID_CLAIM] = app_id
+        assert JwtTokenValidation.get_app_id_from_claims(v1_claims) == app_id
+
+        # AppId there with v1 version
+        v1_claims[AuthenticationConstants.VERSION_CLAIM] = "1.0"
+        assert JwtTokenValidation.get_app_id_from_claims(v1_claims) == app_id
+
+        # v2 version but no azp
+        v2_claims[AuthenticationConstants.VERSION_CLAIM] = "2.0"
+        assert not JwtTokenValidation.get_app_id_from_claims(v2_claims)
+
+        # v2 version but no azp
+        v2_claims[AuthenticationConstants.AUTHORIZED_PARTY] = app_id
+        assert JwtTokenValidation.get_app_id_from_claims(v2_claims) == app_id
diff --git a/libraries/botframework-connector/tests/test_conversations.py b/libraries/botframework-connector/tests/test_conversations.py
index b75960c00..badd636d7 100644
--- a/libraries/botframework-connector/tests/test_conversations.py
+++ b/libraries/botframework-connector/tests/test_conversations.py
@@ -190,7 +190,7 @@ def test_conversations_send_to_conversation_with_attachment(self):
         assert response is not None
 
     def test_conversations_send_to_conversation_with_invalid_conversation_id_fails(
-        self
+        self,
     ):
         activity = Activity(
             type=ActivityTypes.message,
diff --git a/libraries/botframework-connector/tests/test_conversations_async.py b/libraries/botframework-connector/tests/test_conversations_async.py
index 8445391bc..a6ad2242b 100644
--- a/libraries/botframework-connector/tests/test_conversations_async.py
+++ b/libraries/botframework-connector/tests/test_conversations_async.py
@@ -203,7 +203,7 @@ def test_conversations_send_to_conversation_with_attachment(self):
         assert response is not None
 
     def test_conversations_send_to_conversation_with_invalid_conversation_id_fails(
-        self
+        self,
     ):
         activity = Activity(
             type=ActivityTypes.message,
diff --git a/libraries/botframework-connector/tests/test_endorsements_validator.py b/libraries/botframework-connector/tests/test_endorsements_validator.py
index 18dee4c31..9d4fad0fa 100644
--- a/libraries/botframework-connector/tests/test_endorsements_validator.py
+++ b/libraries/botframework-connector/tests/test_endorsements_validator.py
@@ -1,3 +1,6 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
 import pytest
 
 from botframework.connector.auth import EndorsementsValidator
diff --git a/libraries/botframework-connector/tests/test_microsoft_app_credentials.py b/libraries/botframework-connector/tests/test_microsoft_app_credentials.py
new file mode 100644
index 000000000..e1beff8bf
--- /dev/null
+++ b/libraries/botframework-connector/tests/test_microsoft_app_credentials.py
@@ -0,0 +1,36 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import aiounittest
+
+from botframework.connector.auth import AuthenticationConstants, MicrosoftAppCredentials
+
+
+class TestMicrosoftAppCredentials(aiounittest.AsyncTestCase):
+    async def test_app_credentials(self):
+        default_scope_case_1 = MicrosoftAppCredentials("some_app", "some_password")
+        assert (
+            AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
+            == default_scope_case_1.oauth_scope
+        )
+
+        # Use with default scope
+        default_scope_case_2 = MicrosoftAppCredentials(
+            "some_app", "some_password", "some_tenant"
+        )
+        assert (
+            AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
+            == default_scope_case_2.oauth_scope
+        )
+
+        custom_scope = "some_scope"
+        custom_scope_case_1 = MicrosoftAppCredentials(
+            "some_app", "some_password", oauth_scope=custom_scope
+        )
+        assert custom_scope_case_1.oauth_scope == custom_scope
+
+        # Use with default scope
+        custom_scope_case_2 = MicrosoftAppCredentials(
+            "some_app", "some_password", "some_tenant", custom_scope
+        )
+        assert custom_scope_case_2.oauth_scope == custom_scope
diff --git a/libraries/botframework-connector/tests/test_skill_validation.py b/libraries/botframework-connector/tests/test_skill_validation.py
new file mode 100644
index 000000000..a7667c3d7
--- /dev/null
+++ b/libraries/botframework-connector/tests/test_skill_validation.py
@@ -0,0 +1,179 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import uuid
+from asyncio import Future
+from unittest.mock import Mock, DEFAULT
+import aiounittest
+from ddt import data, ddt, unpack
+
+from botframework.connector.auth import (
+    AuthenticationConstants,
+    ClaimsIdentity,
+    CredentialProvider,
+    SkillValidation,
+    JwtTokenValidation,
+)
+
+
+def future_builder(return_val: object) -> Future:
+    result = Future()
+    result.set_result(return_val)
+    return result
+
+
+@ddt
+class TestSkillValidation(aiounittest.AsyncTestCase):
+    def test_is_skill_claim_test(self):
+        claims = {}
+        audience = str(uuid.uuid4())
+        app_id = str(uuid.uuid4())
+
+        # Empty list of claims
+        assert not SkillValidation.is_skill_claim(claims)
+
+        # No Audience claim
+        claims[AuthenticationConstants.VERSION_CLAIM] = "1.0"
+        assert not SkillValidation.is_skill_claim(claims)
+
+        # Emulator Audience claim
+        claims[
+            AuthenticationConstants.AUDIENCE_CLAIM
+        ] = AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER
+        assert not SkillValidation.is_skill_claim(claims)
+
+        # No AppId claim
+        del claims[AuthenticationConstants.AUDIENCE_CLAIM]
+        claims[AuthenticationConstants.AUDIENCE_CLAIM] = audience
+        assert not SkillValidation.is_skill_claim(claims)
+
+        # AppId != Audience
+        claims[AuthenticationConstants.APP_ID_CLAIM] = audience
+        assert not SkillValidation.is_skill_claim(claims)
+
+        # Anonymous skill app id
+        del claims[AuthenticationConstants.APP_ID_CLAIM]
+        claims[
+            AuthenticationConstants.APP_ID_CLAIM
+        ] = AuthenticationConstants.ANONYMOUS_SKILL_APP_ID
+        assert SkillValidation.is_skill_claim(claims)
+
+        # All checks pass, should be good now
+        del claims[AuthenticationConstants.AUDIENCE_CLAIM]
+        claims[AuthenticationConstants.AUDIENCE_CLAIM] = app_id
+        assert SkillValidation.is_skill_claim(claims)
+
+    # pylint: disable=line-too-long
+    @data(
+        (False, "Failed on: Null string", None),
+        (False, "Failed on: Empty string", ""),
+        (False, "Failed on: No token part", "Bearer"),
+        (
+            False,
+            "Failed on: No bearer part",
+            "ew0KICAiYWxnIjogIlJTMjU2IiwNCiAgImtpZCI6ICJKVzNFWGRudy13WTJFcUxyV1RxUTJyVWtCLWciLA0KICAieDV0IjogIkpXM0VYZG53LXdZMkVxTHJXVHFRMnJVa0ItZyIsDQogICJ0eXAiOiAiSldUIg0KfQ.ew0KICAic2VydmljZXVybCI6ICJodHRwczovL2RpcmVjdGxpbmUuYm90ZnJhbWV3b3JrLmNvbS8iLA0KICAibmJmIjogMTU3MTE5MDM0OCwNCiAgImV4cCI6IDE1NzExOTA5NDgsDQogICJpc3MiOiAiaHR0cHM6Ly9hcGkuYm90ZnJhbWV3b3JrLmNvbSIsDQogICJhdWQiOiAiNGMwMDM5ZTUtNjgxNi00OGU4LWIzMTMtZjc3NjkxZmYxYzVlIg0KfQ.cEVHmQCTjL9HVHGk91sja5CqjgvM7B-nArkOg4bE83m762S_le94--GBb0_7aAy6DCdvkZP0d4yWwbpfOkukEXixCDZQM2kWPcOo6lz_VIuXxHFlZAGrTvJ1QkBsg7vk-6_HR8XSLJQZoWrVhE-E_dPj4GPBKE6s1aNxYytzazbKRAEYa8Cn4iVtuYbuj4XfH8PMDv5aC0APNvfgTGk-BlIiP6AGdo4JYs62lUZVSAYg5VLdBcJYMYcKt-h2n1saeapFDVHx_tdpRuke42M4RpGH_wzICeWC5tTExWEkQWApU85HRA5zzk4OpTv17Ct13JCvQ7cD5x9RK5f7CMnbhQ",
+        ),
+        (
+            False,
+            "Failed on: Invalid scheme",
+            "Potato ew0KICAiYWxnIjogIlJTMjU2IiwNCiAgImtpZCI6ICJKVzNFWGRudy13WTJFcUxyV1RxUTJyVWtCLWciLA0KICAieDV0IjogIkpXM0VYZG53LXdZMkVxTHJXVHFRMnJVa0ItZyIsDQogICJ0eXAiOiAiSldUIg0KfQ.ew0KICAic2VydmljZXVybCI6ICJodHRwczovL2RpcmVjdGxpbmUuYm90ZnJhbWV3b3JrLmNvbS8iLA0KICAibmJmIjogMTU3MTE5MDM0OCwNCiAgImV4cCI6IDE1NzExOTA5NDgsDQogICJpc3MiOiAiaHR0cHM6Ly9hcGkuYm90ZnJhbWV3b3JrLmNvbSIsDQogICJhdWQiOiAiNGMwMDM5ZTUtNjgxNi00OGU4LWIzMTMtZjc3NjkxZmYxYzVlIg0KfQ.cEVHmQCTjL9HVHGk91sja5CqjgvM7B-nArkOg4bE83m762S_le94--GBb0_7aAy6DCdvkZP0d4yWwbpfOkukEXixCDZQM2kWPcOo6lz_VIuXxHFlZAGrTvJ1QkBsg7vk-6_HR8XSLJQZoWrVhE-E_dPj4GPBKE6s1aNxYytzazbKRAEYa8Cn4iVtuYbuj4XfH8PMDv5aC0APNvfgTGk-BlIiP6AGdo4JYs62lUZVSAYg5VLdBcJYMYcKt-h2n1saeapFDVHx_tdpRuke42M4RpGH_wzICeWC5tTExWEkQWApU85HRA5zzk4OpTv17Ct13JCvQ7cD5x9RK5f7CMnbhQ",
+        ),
+        (
+            False,
+            "Failed on: To bot v2 from webchat",
+            "Bearer ew0KICAiYWxnIjogIlJTMjU2IiwNCiAgImtpZCI6ICJKVzNFWGRudy13WTJFcUxyV1RxUTJyVWtCLWciLA0KICAieDV0IjogIkpXM0VYZG53LXdZMkVxTHJXVHFRMnJVa0ItZyIsDQogICJ0eXAiOiAiSldUIg0KfQ.ew0KICAic2VydmljZXVybCI6ICJodHRwczovL2RpcmVjdGxpbmUuYm90ZnJhbWV3b3JrLmNvbS8iLA0KICAibmJmIjogMTU3MTE5MDM0OCwNCiAgImV4cCI6IDE1NzExOTA5NDgsDQogICJpc3MiOiAiaHR0cHM6Ly9hcGkuYm90ZnJhbWV3b3JrLmNvbSIsDQogICJhdWQiOiAiNGMwMDM5ZTUtNjgxNi00OGU4LWIzMTMtZjc3NjkxZmYxYzVlIg0KfQ.cEVHmQCTjL9HVHGk91sja5CqjgvM7B-nArkOg4bE83m762S_le94--GBb0_7aAy6DCdvkZP0d4yWwbpfOkukEXixCDZQM2kWPcOo6lz_VIuXxHFlZAGrTvJ1QkBsg7vk-6_HR8XSLJQZoWrVhE-E_dPj4GPBKE6s1aNxYytzazbKRAEYa8Cn4iVtuYbuj4XfH8PMDv5aC0APNvfgTGk-BlIiP6AGdo4JYs62lUZVSAYg5VLdBcJYMYcKt-h2n1saeapFDVHx_tdpRuke42M4RpGH_wzICeWC5tTExWEkQWApU85HRA5zzk4OpTv17Ct13JCvQ7cD5x9RK5f7CMnbhQ",
+        ),
+        (
+            False,
+            "Failed on: To bot v1 token from emulator",
+            "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyIsImtpZCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyJ9.eyJhdWQiOiI0YzMzYzQyMS1mN2QzLTRiNmMtOTkyYi0zNmU3ZTZkZTg3NjEiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9kNmQ0OTQyMC1mMzliLTRkZjctYTFkYy1kNTlhOTM1ODcxZGIvIiwiaWF0IjoxNTcxMTg5ODczLCJuYmYiOjE1NzExODk4NzMsImV4cCI6MTU3MTE5Mzc3MywiYWlvIjoiNDJWZ1lLaWJGUDIyMUxmL0NjL1Yzai8zcGF2RUFBPT0iLCJhcHBpZCI6IjRjMzNjNDIxLWY3ZDMtNGI2Yy05OTJiLTM2ZTdlNmRlODc2MSIsImFwcGlkYWNyIjoiMSIsImlkcCI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0L2Q2ZDQ5NDIwLWYzOWItNGRmNy1hMWRjLWQ1OWE5MzU4NzFkYi8iLCJ0aWQiOiJkNmQ0OTQyMC1mMzliLTRkZjctYTFkYy1kNTlhOTM1ODcxZGIiLCJ1dGkiOiJOdXJ3bTVOQnkwR2duT3dKRnFVREFBIiwidmVyIjoiMS4wIn0.GcKs3XZ_4GONVsAoPYI7otqUZPoNN8pULUnlJMxQa-JKXRKV0KtvTAdcMsfYudYxbz7HwcNYerFT1q3RZAimJFtfF4x_sMN23yEVxsQmYQrsf2YPmEsbCfNiEx0YEoWUdS38R1N0Iul2P_P_ZB7XreG4aR5dT6lY5TlXbhputv9pi_yAU7PB1aLuB05phQme5NwJEY22pUfx5pe1wVHogI0JyNLi-6gdoSL63DJ32tbQjr2DNYilPVtLsUkkz7fTky5OKd4p7FmG7P5EbEK4H5j04AGe_nIFs-X6x_FIS_5OSGK4LGA2RPnqa-JYpngzlNWVkUbnuH10AovcAprgdg",
+        ),
+        (
+            False,
+            "Failed on: To bot v2 token from emulator",
+            "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyJ9.eyJhdWQiOiI0YzAwMzllNS02ODE2LTQ4ZTgtYjMxMy1mNzc2OTFmZjFjNWUiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vZDZkNDk0MjAtZjM5Yi00ZGY3LWExZGMtZDU5YTkzNTg3MWRiL3YyLjAiLCJpYXQiOjE1NzExODkwMTEsIm5iZiI6MTU3MTE4OTAxMSwiZXhwIjoxNTcxMTkyOTExLCJhaW8iOiI0MlZnWUxnYWxmUE90Y2IxaEoxNzJvbmxIc3ZuQUFBPSIsImF6cCI6IjRjMDAzOWU1LTY4MTYtNDhlOC1iMzEzLWY3NzY5MWZmMWM1ZSIsImF6cGFjciI6IjEiLCJ0aWQiOiJkNmQ0OTQyMC1mMzliLTRkZjctYTFkYy1kNTlhOTM1ODcxZGIiLCJ1dGkiOiJucEVxVTFoR1pVbXlISy1MUVdJQ0FBIiwidmVyIjoiMi4wIn0.CXcPx7LfatlRsOX4QG-jaC-guwcY3PFxpFICqwfoOTxAjHpeJNFXOpFeA3Qb5VKM6Yw5LyA9eraL5QDJB_4uMLCCKErPXMyoSm8Hw-GGZkHgFV5ciQXSXhE-IfOinqHE_0Lkt_VLR2q6ekOncnJeCR111QCqt3D8R0Ud0gvyLv_oONxDtqg7HUgNGEfioB-BDnBsO4RN7NGrWQFbyPxPmhi8a_Xc7j5Bb9jeiiIQbVaWkIrrPN31aWY1tEZLvdN0VluYlOa0EBVrzpXXZkIyWx99mpklg0lsy7mRyjuM1xydmyyGkzbiCKtODOanf8UwTjkTg5XTIluxe79_hVk2JQ",
+        ),
+        (
+            True,
+            "Failed on: To skill valid v1 token",
+            "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyIsImtpZCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyJ9.eyJhdWQiOiI0YzMzYzQyMS1mN2QzLTRiNmMtOTkyYi0zNmU3ZTZkZTg3NjEiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9kNmQ0OTQyMC1mMzliLTRkZjctYTFkYy1kNTlhOTM1ODcxZGIvIiwiaWF0IjoxNTcxMTg5NjMwLCJuYmYiOjE1NzExODk2MzAsImV4cCI6MTU3MTE5MzUzMCwiYWlvIjoiNDJWZ1lJZzY1aDFXTUVPd2JmTXIwNjM5V1lLckFBPT0iLCJhcHBpZCI6IjRjMDAzOWU1LTY4MTYtNDhlOC1iMzEzLWY3NzY5MWZmMWM1ZSIsImFwcGlkYWNyIjoiMSIsImlkcCI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0L2Q2ZDQ5NDIwLWYzOWItNGRmNy1hMWRjLWQ1OWE5MzU4NzFkYi8iLCJ0aWQiOiJkNmQ0OTQyMC1mMzliLTRkZjctYTFkYy1kNTlhOTM1ODcxZGIiLCJ1dGkiOiJhWlpOUTY3RjRVNnNmY3d0S0R3RUFBIiwidmVyIjoiMS4wIn0.Yogk9fptxxJKO8jRkk6FrlLQsAulNNgoa0Lqv2JPkswyyizse8kcwQhxOaZOotY0UBduJ-pCcrejk6k4_O_ZReYXKz8biL9Q7Z02cU9WUMvuIGpAhttz8v0VlVSyaEJVJALc5B-U6XVUpZtG9LpE6MVror_0WMnT6T9Ijf9SuxUvdVCcmAJyZuoqudodseuFI-jtCpImEapZp0wVN4BUodrBacMbTeYjdZyAbNVBqF5gyzDztMKZR26HEz91gqulYZvJJZOJO6ejnm0j62s1tqvUVRBywvnSOon-MV0Xt2Vm0irhv6ipzTXKwWhT9rGHSLj0g8r6NqWRyPRFqLccvA",
+        ),
+        (
+            True,
+            "Failed on: To skill valid v2 token",
+            "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyJ9.eyJhdWQiOiI0YzAwMzllNS02ODE2LTQ4ZTgtYjMxMy1mNzc2OTFmZjFjNWUiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vZDZkNDk0MjAtZjM5Yi00ZGY3LWExZGMtZDU5YTkzNTg3MWRiL3YyLjAiLCJpYXQiOjE1NzExODk3NTUsIm5iZiI6MTU3MTE4OTc1NSwiZXhwIjoxNTcxMTkzNjU1LCJhaW8iOiI0MlZnWUpnZDROZkZKeG1tMTdPaVMvUk8wZll2QUE9PSIsImF6cCI6IjRjMzNjNDIxLWY3ZDMtNGI2Yy05OTJiLTM2ZTdlNmRlODc2MSIsImF6cGFjciI6IjEiLCJ0aWQiOiJkNmQ0OTQyMC1mMzliLTRkZjctYTFkYy1kNTlhOTM1ODcxZGIiLCJ1dGkiOiJMc2ZQME9JVkNVS1JzZ1IyYlFBQkFBIiwidmVyIjoiMi4wIn0.SggsEbEyXDYcg6EdhK-RA1y6S97z4hwEccXc6a3ymnHP-78frZ3N8rPLsqLoK5QPGA_cqOXsX1zduA4vlFSy3MfTV_npPfsyWa1FIse96-2_3qa9DIP8bhvOHXEVZeq-r-0iF972waFyPPC_KVYWnIgAcunGhFWvLhhOUx9dPgq7824qTq45ma1rOqRoYbhhlRn6PJDymIin5LeOzDGJJ8YVLnFUgntc6_4z0P_fnuMktzar88CUTtGvR4P7XNJhS8v9EwYQujglsJNXg7LNcwV7qOxDYWJtT_UMuMAts9ctD6FkuTGX_-6FTqmdUPPUS4RWwm4kkl96F_dXnos9JA",
+        ),
+    )
+    @unpack
+    def test_is_skill_token_test(self, expected: bool, message: str, token: str):
+        assert SkillValidation.is_skill_token(token) == expected, message
+
+    async def test_identity_validation(self):
+        # pylint: disable=protected-access
+        mock_credentials = Mock(spec=CredentialProvider)
+        audience = str(uuid.uuid4())
+        app_id = str(uuid.uuid4())
+        mock_identity = Mock(spec=ClaimsIdentity)
+        claims = {}
+
+        # Null identity
+        with self.assertRaises(PermissionError) as exception:
+            await SkillValidation._validate_identity(None, mock_credentials)
+        assert str(exception.exception), "Invalid Identity"
+
+        mock_identity.is_authenticated = False
+        # not authenticated identity
+        with self.assertRaises(PermissionError) as exception:
+            await SkillValidation._validate_identity(mock_identity, mock_credentials)
+        assert str(exception.exception), "Token Not Authenticated"
+
+        # No version claims
+        mock_identity.is_authenticated = True
+        mock_identity.claims = claims
+        with self.assertRaises(PermissionError) as exception:
+            await SkillValidation._validate_identity(mock_identity, mock_credentials)
+        assert (
+            str(exception.exception)
+            == f"'{AuthenticationConstants.VERSION_CLAIM}' claim is required on skill Tokens."
+        )
+
+        # No audience claim
+        claims[AuthenticationConstants.VERSION_CLAIM] = "1.0"
+        with self.assertRaises(PermissionError) as exception:
+            await SkillValidation._validate_identity(mock_identity, mock_credentials)
+        assert (
+            str(exception.exception)
+            == f"'{AuthenticationConstants.AUDIENCE_CLAIM}' claim is required on skill Tokens."
+        )
+
+        # Invalid AppId in audience
+
+        def validate_appid(app_id: str):
+            assert isinstance(app_id, str)
+            return DEFAULT
+
+        claims[AuthenticationConstants.AUDIENCE_CLAIM] = audience
+        mock_credentials.is_valid_appid.side_effect = validate_appid
+        mock_credentials.is_valid_appid.return_value = future_builder(return_val=False)
+        with self.assertRaises(PermissionError) as exception:
+            await SkillValidation._validate_identity(mock_identity, mock_credentials)
+        assert str(exception.exception), "Invalid audience."
+
+        # Invalid AppId in in app_id or azp
+        mock_credentials.is_valid_appid.return_value = future_builder(return_val=True)
+        with self.assertRaises(PermissionError) as exception:
+            await SkillValidation._validate_identity(mock_identity, mock_credentials)
+        assert str(exception.exception), "Invalid app_id."
+
+        # All checks pass (no exception)
+        claims[AuthenticationConstants.APP_ID_CLAIM] = app_id
+        await SkillValidation._validate_identity(mock_identity, mock_credentials)
+
+    @staticmethod
+    def test_create_anonymous_skill_claim():
+        sut = SkillValidation.create_anonymous_skill_claim()
+        assert (
+            JwtTokenValidation.get_app_id_from_claims(sut.claims)
+            == AuthenticationConstants.ANONYMOUS_SKILL_APP_ID
+        )
+        assert sut.authentication_type == AuthenticationConstants.ANONYMOUS_AUTH_TYPE
diff --git a/libraries/functional-tests/functionaltestbot/Dockerfile b/libraries/functional-tests/functionaltestbot/Dockerfile
new file mode 100644
index 000000000..3364fc380
--- /dev/null
+++ b/libraries/functional-tests/functionaltestbot/Dockerfile
@@ -0,0 +1,48 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+FROM  tiangolo/uwsgi-nginx-flask:python3.6
+
+
+RUN  mkdir /functionaltestbot 
+
+EXPOSE 443
+# EXPOSE 2222
+
+COPY ./functionaltestbot /functionaltestbot
+COPY setup.py /
+COPY test.sh /
+# RUN ls -ltr
+# RUN cat prestart.sh
+# RUN cat main.py
+
+ENV FLASK_APP=/functionaltestbot/app.py
+ENV LANG=C.UTF-8
+ENV LC_ALL=C.UTF-8
+ENV PATH ${PATH}:/home/site/wwwroot
+
+WORKDIR /
+
+# Initialize the bot
+RUN  pip3 install -e .
+
+# ssh
+ENV SSH_PASSWD "root:Docker!"
+RUN apt-get update \
+        && apt-get install -y --no-install-recommends dialog \
+        && apt-get update \
+	&& apt-get install -y --no-install-recommends openssh-server \
+	&& echo "$SSH_PASSWD" | chpasswd \
+    && apt install -y --no-install-recommends vim 
+COPY sshd_config /etc/ssh/
+COPY init.sh /usr/local/bin/
+RUN chmod u+x /usr/local/bin/init.sh
+
+# For Debugging, uncomment the following: 
+# ENTRYPOINT ["python3.6", "-c", "import time ; time.sleep(500000)"]
+ENTRYPOINT ["init.sh"]
+ 
+# For Devops, they don't like entry points.  This is now in the devops 
+# pipeline.
+# ENTRYPOINT [ "flask" ]
+# CMD [ "run", "--port", "3978", "--host", "0.0.0.0" ]
diff --git a/libraries/functional-tests/functionaltestbot/Dockfile b/libraries/functional-tests/functionaltestbot/Dockfile
new file mode 100644
index 000000000..8383f9a2b
--- /dev/null
+++ b/libraries/functional-tests/functionaltestbot/Dockfile
@@ -0,0 +1,27 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+FROM python:3.7-slim as pkg_holder
+
+ARG EXTRA_INDEX_URL
+RUN pip config set global.extra-index-url "${EXTRA_INDEX_URL}"
+
+COPY requirements.txt .
+RUN pip download -r requirements.txt -d packages
+
+FROM python:3.7-slim
+
+ENV VIRTUAL_ENV=/opt/venv
+RUN python3.7 -m venv $VIRTUAL_ENV
+ENV PATH="$VIRTUAL_ENV/bin:$PATH"
+
+COPY . /app
+WORKDIR /app
+
+COPY --from=pkg_holder packages packages
+
+RUN pip install -r requirements.txt --no-index --find-links=packages && rm -rf packages
+
+ENTRYPOINT ["python"]
+EXPOSE 3978
+CMD ["runserver.py"]
diff --git a/libraries/functional-tests/functionaltestbot/client_driver/README.md b/libraries/functional-tests/functionaltestbot/client_driver/README.md
new file mode 100644
index 000000000..317a457c9
--- /dev/null
+++ b/libraries/functional-tests/functionaltestbot/client_driver/README.md
@@ -0,0 +1,5 @@
+# Client Driver for Function E2E test
+
+This contains the client code that drives the bot functional test.
+
+It performs simple operations against the bot and validates results.
\ No newline at end of file
diff --git a/samples/06.using-cards/dialogs/__init__.py b/libraries/functional-tests/functionaltestbot/flask_bot_app/__init__.py
similarity index 61%
rename from samples/06.using-cards/dialogs/__init__.py
rename to libraries/functional-tests/functionaltestbot/flask_bot_app/__init__.py
index 74d870b7c..d5d099805 100644
--- a/samples/06.using-cards/dialogs/__init__.py
+++ b/libraries/functional-tests/functionaltestbot/flask_bot_app/__init__.py
@@ -1,6 +1,6 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License.
 
-from .main_dialog import MainDialog
+from .app import APP
 
-__all__ = ["MainDialog"]
+__all__ = ["APP"]
diff --git a/libraries/functional-tests/functionaltestbot/flask_bot_app/app.py b/libraries/functional-tests/functionaltestbot/flask_bot_app/app.py
new file mode 100644
index 000000000..10f99452e
--- /dev/null
+++ b/libraries/functional-tests/functionaltestbot/flask_bot_app/app.py
@@ -0,0 +1,21 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+"""Bot app with Flask routing."""
+
+from flask import Response
+
+from .bot_app import BotApp
+
+
+APP = BotApp()
+
+
+@APP.flask.route("/api/messages", methods=["POST"])
+def messages() -> Response:
+    return APP.messages()
+
+
+@APP.flask.route("/api/test", methods=["GET"])
+def test() -> Response:
+    return APP.test()
diff --git a/libraries/functional-tests/functionaltestbot/flask_bot_app/bot_app.py b/libraries/functional-tests/functionaltestbot/flask_bot_app/bot_app.py
new file mode 100644
index 000000000..5fb109576
--- /dev/null
+++ b/libraries/functional-tests/functionaltestbot/flask_bot_app/bot_app.py
@@ -0,0 +1,108 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import asyncio
+import sys
+from types import MethodType
+from flask import Flask, Response, request
+
+from botbuilder.core import (
+    BotFrameworkAdapter,
+    BotFrameworkAdapterSettings,
+    MessageFactory,
+    TurnContext,
+)
+from botbuilder.schema import Activity, InputHints
+
+from .default_config import DefaultConfig
+from .my_bot import MyBot
+
+
+class BotApp:
+    """A Flask echo bot."""
+
+    def __init__(self):
+        # Create the loop and Flask app
+        self.loop = asyncio.get_event_loop()
+        self.flask = Flask(__name__, instance_relative_config=True)
+        self.flask.config.from_object(DefaultConfig)
+
+        # Create adapter.
+        # See https://aka.ms/about-bot-adapter to learn more about how bots work.
+        self.settings = BotFrameworkAdapterSettings(
+            self.flask.config["APP_ID"], self.flask.config["APP_PASSWORD"]
+        )
+        self.adapter = BotFrameworkAdapter(self.settings)
+
+        # Catch-all for errors.
+        async def on_error(adapter, 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]: {error}", file=sys.stderr)
+
+            # Send a message to the user
+            error_message_text = "Sorry, it looks like something went wrong."
+            error_message = MessageFactory.text(
+                error_message_text, error_message_text, InputHints.expecting_input
+            )
+            await context.send_activity(error_message)
+
+            # pylint: disable=protected-access
+            if adapter._conversation_state:
+                # If state was defined, clear it.
+                await adapter._conversation_state.delete(context)
+
+        self.adapter.on_turn_error = MethodType(on_error, self.adapter)
+
+        # Create the main dialog
+        self.bot = MyBot()
+
+    def messages(self) -> Response:
+        """Main bot message handler that listens for incoming requests."""
+
+        if "application/json" in request.headers["Content-Type"]:
+            body = request.json
+        else:
+            return Response(status=415)
+
+        activity = Activity().deserialize(body)
+        auth_header = (
+            request.headers["Authorization"]
+            if "Authorization" in request.headers
+            else ""
+        )
+
+        async def aux_func(turn_context):
+            await self.bot.on_turn(turn_context)
+
+        try:
+            task = self.loop.create_task(
+                self.adapter.process_activity(activity, auth_header, aux_func)
+            )
+            self.loop.run_until_complete(task)
+            return Response(status=201)
+        except Exception as exception:
+            raise exception
+
+    @staticmethod
+    def test() -> Response:
+        """
+        For test only - verify if the flask app works locally - e.g. with:
+        ```bash
+        curl http://127.0.0.1:3978/api/test
+        ```
+        You shall get:
+        ```
+        test
+        ```
+        """
+        return Response(status=200, response="test\n")
+
+    def run(self, host=None) -> None:
+        try:
+            self.flask.run(
+                host=host, debug=False, port=self.flask.config["PORT"]
+            )  # nosec debug
+        except Exception as exception:
+            raise exception
diff --git a/libraries/functional-tests/functionaltestbot/flask_bot_app/default_config.py b/libraries/functional-tests/functionaltestbot/flask_bot_app/default_config.py
new file mode 100644
index 000000000..96c277e09
--- /dev/null
+++ b/libraries/functional-tests/functionaltestbot/flask_bot_app/default_config.py
@@ -0,0 +1,12 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from os import environ
+
+
+class DefaultConfig:
+    """ Bot Configuration """
+
+    PORT: int = 3978
+    APP_ID: str = environ.get("MicrosoftAppId", "")
+    APP_PASSWORD: str = environ.get("MicrosoftAppPassword", "")
diff --git a/libraries/functional-tests/functionaltestbot/flask_bot_app/my_bot.py b/libraries/functional-tests/functionaltestbot/flask_bot_app/my_bot.py
new file mode 100644
index 000000000..58f002986
--- /dev/null
+++ b/libraries/functional-tests/functionaltestbot/flask_bot_app/my_bot.py
@@ -0,0 +1,19 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.core import ActivityHandler, TurnContext
+from botbuilder.schema import ChannelAccount
+
+
+class MyBot(ActivityHandler):
+    """See https://aka.ms/about-bot-activity-message to learn more about the message and other activity types."""
+
+    async def on_message_activity(self, turn_context: TurnContext):
+        await turn_context.send_activity(f"You said '{ turn_context.activity.text }'")
+
+    async def on_members_added_activity(
+        self, members_added: ChannelAccount, turn_context: TurnContext
+    ):
+        for member_added in members_added:
+            if member_added.id != turn_context.activity.recipient.id:
+                await turn_context.send_activity("Hello and welcome!")
diff --git a/samples/01.console-echo/README.md b/libraries/functional-tests/functionaltestbot/functionaltestbot/README.md
similarity index 100%
rename from samples/01.console-echo/README.md
rename to libraries/functional-tests/functionaltestbot/functionaltestbot/README.md
diff --git a/libraries/functional-tests/functionaltestbot/functionaltestbot/about.py b/libraries/functional-tests/functionaltestbot/functionaltestbot/about.py
new file mode 100644
index 000000000..223c72f3d
--- /dev/null
+++ b/libraries/functional-tests/functionaltestbot/functionaltestbot/about.py
@@ -0,0 +1,14 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+"""Package information."""
+import os
+
+__title__ = "functionaltestbot"
+__version__ = (
+    os.environ["packageVersion"] if "packageVersion" in os.environ else "0.0.1"
+)
+__uri__ = "https://www.github.com/Microsoft/botbuilder-python"
+__author__ = "Microsoft"
+__description__ = "Microsoft Bot Framework Bot Builder"
+__summary__ = "Microsoft Bot Framework Bot Builder SDK for Python."
+__license__ = "MIT"
diff --git a/generators/app/templates/empty/{{cookiecutter.bot_name}}/app.py b/libraries/functional-tests/functionaltestbot/functionaltestbot/app.py
similarity index 73%
rename from generators/app/templates/empty/{{cookiecutter.bot_name}}/app.py
rename to libraries/functional-tests/functionaltestbot/functionaltestbot/app.py
index 5dfdc30f1..071a17d2b 100644
--- a/generators/app/templates/empty/{{cookiecutter.bot_name}}/app.py
+++ b/libraries/functional-tests/functionaltestbot/functionaltestbot/app.py
@@ -9,9 +9,10 @@
 from botbuilder.core import (
     BotFrameworkAdapter,
     BotFrameworkAdapterSettings,
+    MessageFactory,
     TurnContext,
 )
-from botbuilder.schema import Activity
+from botbuilder.schema import Activity, InputHints
 from bot import MyBot
 
 # Create the loop and Flask app
@@ -25,21 +26,32 @@
 ADAPTER = BotFrameworkAdapter(SETTINGS)
 
 # Catch-all for errors.
+# pylint: disable=unused-argument
 async def on_error(self, 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)
+    print(f"\n [on_turn_error]: {error}", file=sys.stderr)
 
     # Send a message to the user
-    await context.send_activity("The bot encounted an error or bug.")
-    await context.send_activity("To continue to run this bot, please fix the bot source code.")
+    error_message_text = "Sorry, it looks like something went wrong."
+    error_message = MessageFactory.text(
+        error_message_text, error_message_text, InputHints.expecting_input
+    )
+    await context.send_activity(error_message)
+
 
 ADAPTER.on_turn_error = MethodType(on_error, ADAPTER)
 
 # Create the main dialog
 BOT = MyBot()
 
+# Listen for incoming requests on GET / for Azure monitoring
+@APP.route("/", methods=["GET"])
+def ping():
+    return Response(status=200)
+
+
 # Listen for incoming requests on /api/messages.
 @APP.route("/api/messages", methods=["POST"])
 def messages():
@@ -54,9 +66,12 @@ def messages():
         request.headers["Authorization"] if "Authorization" in request.headers else ""
     )
 
+    async def aux_func(turn_context):
+        await BOT.on_turn(turn_context)
+
     try:
         task = LOOP.create_task(
-            ADAPTER.process_activity(activity, auth_header, BOT.on_turn)
+            ADAPTER.process_activity(activity, auth_header, aux_func)
         )
         LOOP.run_until_complete(task)
         return Response(status=201)
diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/bot.py b/libraries/functional-tests/functionaltestbot/functionaltestbot/bot.py
similarity index 89%
rename from generators/app/templates/echo/{{cookiecutter.bot_name}}/bot.py
rename to libraries/functional-tests/functionaltestbot/functionaltestbot/bot.py
index c1ea90861..128f47cf6 100644
--- a/generators/app/templates/echo/{{cookiecutter.bot_name}}/bot.py
+++ b/libraries/functional-tests/functionaltestbot/functionaltestbot/bot.py
@@ -12,9 +12,7 @@ async def on_message_activity(self, turn_context: TurnContext):
         await turn_context.send_activity(f"You said '{ turn_context.activity.text }'")
 
     async def on_members_added_activity(
-        self,
-        members_added: ChannelAccount,
-        turn_context: TurnContext
+        self, members_added: ChannelAccount, turn_context: TurnContext
     ):
         for member_added in members_added:
             if member_added.id != turn_context.activity.recipient.id:
diff --git a/libraries/functional-tests/functionaltestbot/functionaltestbot/config.py b/libraries/functional-tests/functionaltestbot/functionaltestbot/config.py
new file mode 100644
index 000000000..a3bd72174
--- /dev/null
+++ b/libraries/functional-tests/functionaltestbot/functionaltestbot/config.py
@@ -0,0 +1,13 @@
+#!/usr/bin/env python3
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+
+
+class DefaultConfig:
+    """ Bot Configuration """
+
+    PORT = 443
+    APP_ID = os.environ.get("MicrosoftAppId", "")
+    APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "")
diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/requirements.txt b/libraries/functional-tests/functionaltestbot/functionaltestbot/requirements.txt
similarity index 100%
rename from generators/app/templates/echo/{{cookiecutter.bot_name}}/requirements.txt
rename to libraries/functional-tests/functionaltestbot/functionaltestbot/requirements.txt
diff --git a/libraries/functional-tests/functionaltestbot/init.sh b/libraries/functional-tests/functionaltestbot/init.sh
new file mode 100644
index 000000000..4a5a5be78
--- /dev/null
+++ b/libraries/functional-tests/functionaltestbot/init.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+set -e
+
+echo "Starting SSH ..."
+service ssh start
+
+# flask run --port 3978 --host 0.0.0.0
+python /functionaltestbot/app.py --host 0.0.0.0
\ No newline at end of file
diff --git a/libraries/functional-tests/functionaltestbot/requirements.txt b/libraries/functional-tests/functionaltestbot/requirements.txt
new file mode 100644
index 000000000..313eb980c
--- /dev/null
+++ b/libraries/functional-tests/functionaltestbot/requirements.txt
@@ -0,0 +1,5 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+botbuilder-core>=4.9.0
+flask==1.1.1
diff --git a/libraries/functional-tests/functionaltestbot/runserver.py b/libraries/functional-tests/functionaltestbot/runserver.py
new file mode 100644
index 000000000..9b0e449a7
--- /dev/null
+++ b/libraries/functional-tests/functionaltestbot/runserver.py
@@ -0,0 +1,16 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+"""
+To run the Flask bot app, in a py virtual environment,
+```bash
+pip install -r requirements.txt
+python runserver.py
+```
+"""
+
+from flask_bot_app import APP
+
+
+if __name__ == "__main__":
+    APP.run(host="0.0.0.0")
diff --git a/libraries/functional-tests/functionaltestbot/setup.py b/libraries/functional-tests/functionaltestbot/setup.py
new file mode 100644
index 000000000..85d198662
--- /dev/null
+++ b/libraries/functional-tests/functionaltestbot/setup.py
@@ -0,0 +1,40 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+from setuptools import setup
+
+REQUIRES = [
+    "botbuilder-core>=4.9.0",
+    "flask==1.1.1",
+]
+
+root = os.path.abspath(os.path.dirname(__file__))
+
+with open(os.path.join(root, "functionaltestbot", "about.py")) as f:
+    package_info = {}
+    info = f.read()
+    exec(info, package_info)
+
+setup(
+    name=package_info["__title__"],
+    version=package_info["__version__"],
+    url=package_info["__uri__"],
+    author=package_info["__author__"],
+    description=package_info["__description__"],
+    keywords="botframework azure botbuilder",
+    long_description=package_info["__summary__"],
+    license=package_info["__license__"],
+    packages=["functionaltestbot"],
+    install_requires=REQUIRES,
+    dependency_links=["https://github.com/pytorch/pytorch"],
+    include_package_data=True,
+    classifiers=[
+        "Programming Language :: Python :: 3.6",
+        "Intended Audience :: Developers",
+        "License :: OSI Approved :: MIT License",
+        "Operating System :: OS Independent",
+        "Development Status :: 3 - Alpha",
+        "Topic :: Scientific/Engineering :: Artificial Intelligence",
+    ],
+)
diff --git a/libraries/functional-tests/functionaltestbot/sshd_config b/libraries/functional-tests/functionaltestbot/sshd_config
new file mode 100644
index 000000000..7afb7469f
--- /dev/null
+++ b/libraries/functional-tests/functionaltestbot/sshd_config
@@ -0,0 +1,21 @@
+#
+# /etc/ssh/sshd_config
+#
+
+Port 			2222
+ListenAddress 		0.0.0.0
+LoginGraceTime 		180
+X11Forwarding 		yes
+Ciphers                 aes128-cbc,3des-cbc,aes256-cbc
+MACs                    hmac-sha1,hmac-sha1-96
+StrictModes 		yes
+SyslogFacility 		DAEMON
+PrintMotd 		no
+IgnoreRhosts 		no
+#deprecated option 
+#RhostsAuthentication 	no
+RhostsRSAAuthentication yes
+RSAAuthentication 	no 
+PasswordAuthentication 	yes
+PermitEmptyPasswords 	no
+PermitRootLogin 	yes
\ No newline at end of file
diff --git a/libraries/functional-tests/functionaltestbot/test.sh b/libraries/functional-tests/functionaltestbot/test.sh
new file mode 100644
index 000000000..1c987232e
--- /dev/null
+++ b/libraries/functional-tests/functionaltestbot/test.sh
@@ -0,0 +1 @@
+curl -X POST --header 'Accept: application/json' -d '{"text": "Hi!"}' http://localhost:3979
diff --git a/libraries/functional-tests/requirements.txt b/libraries/functional-tests/requirements.txt
new file mode 100644
index 000000000..b1c2f0a5d
--- /dev/null
+++ b/libraries/functional-tests/requirements.txt
@@ -0,0 +1,2 @@
+requests==2.23.0
+aiounittest==1.3.0
diff --git a/libraries/functional-tests/slacktestbot/README.md b/libraries/functional-tests/slacktestbot/README.md
new file mode 100644
index 000000000..e27305746
--- /dev/null
+++ b/libraries/functional-tests/slacktestbot/README.md
@@ -0,0 +1,131 @@
+# Slack functional test pipeline setup
+
+This is a step by step guide to setup the Slack functional test pipeline.
+
+## Slack Application setup
+
+We'll need to create a Slack application to connect with the bot.
+
+1. Create App
+
+    Create a Slack App from [here](https://api.slack.com/apps), associate it to a workspace.
+
+    
+
+2. Get the Signing Secret and the Verification Token
+
+    Keep the Signing Secret and the Verification Token from the Basic Information tab.
+
+    These tokens will be needed to configure the pipeline.
+
+    - Signing Secret will become *SlackTestBotSlackClientSigningSecret*.
+    - Verification Token will become *SlackTestBotSlackVerificationToken*.
+
+    
+
+3. Grant Scopes
+
+    Go to the OAuth & Permissions tab and scroll to the Scopes section.
+
+    In the Bot Token Scopes, add chat:write, im:history, and im:read using the Add an Oauth Scope button.
+
+    
+
+4. Install App
+
+    On the same OAuth & Permissions tab, scroll up to the OAuth Tokens & Redirect URLs section and click on Install to Workspace.
+
+    A new window will be prompted, click on Allow.
+
+    
+
+5. Get the Bot User OAuth Access Token
+
+    You will be redirected back to OAuth & Permissions tab, keep the Bot User OAuth Access Token.
+
+    - Bot User OAuth Access Token will become *SlackTestBotSlackBotToken* later in the pipeline variables.
+
+    
+
+6. Get the Channel ID
+
+    Go to the Slack workspace you associated the app to. The new App should have appeared; if not, add it using the plus sign that shows up while hovering the mouse over the Apps tab.
+
+    Right click on it and then on Copy link.
+
+    
+
+    The link will look something like https://workspace.slack.com/archives/N074R34L1D.
+
+    The last segment of the URL represents the channel ID, in this case, **N074R34L1D**.
+
+    - Keep this ID as it will later become the *SlackTestBotSlackChannel* pipeline variable.
+
+## Azure setup
+
+We will need to create an Azure App Registration and setup a pipeline.
+
+### App Registration
+
+1. Create an App Registration
+
+    Go [here](https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade) and click on New Registration.
+
+    Set a name and change the supported account type to Multitenant, then Register.
+
+    
+
+ 1. Get the Application ID and client secret values
+
+    You will be redirected to the Overview tab.
+
+    Copy the Application ID then go to the Certificates and secrets tab.
+
+    Create a secret and copy its value.
+
+    - The Azure App Registration ID will be the *SlackTestBotAppId* for the pipeline.
+    - The Azure App Registration Secret value will be the *SlackTestBotAppSecret* for the pipeline.
+
+
+
+### Pipeline Setup
+
+1. Create the pipeline
+
+    From an Azure DevOps project, go to the Pipelines view and create a new one.
+
+    Using the classic editor, select GitHub, then set the repository and branch.
+
+    
+
+2. Set the YAML
+
+    On the following view, click on the Apply button of the YAML configuration.
+
+    Set the pipeline name and point to the YAML file clicking on the three highlighted dots.
+
+
+
+3. Set the pipeline variables
+
+    Finally, click on the variables tab.
+
+    You will need to set up the variables using the values you got throughout this guide:
+
+    |Variable|Value|
+    |---|---|
+    | AzureSubscription | Azure Resource Manager name, click [here](https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/overview) for more information. |
+    | SlackTestBotAppId | Azure App Registration ID. |
+    | SlackTestBotAppSecret | Azure App Registration Secret value. |
+    | SlackTestBotBotGroup | Name of the Azure resource group to be created. |
+    | SlackTestBotBotName | Name of the Bot to be created. |
+    | SlackTestBotSlackBotToken | Slack Bot User OAuth Access Token. |
+    | SlackTestBotSlackChannel | Slack Channel ID. |
+    | SlackTestBotSlackClientSigningSecret | Slack Signing Secret. |
+    | SlackTestBotSlackVerificationToken | Slack Verification Token. |
+
+    Once the variables are set up your panel should look something like this:
+
+    
+
+    Click Save and the pipeline is ready to run.
diff --git a/libraries/functional-tests/slacktestbot/app.py b/libraries/functional-tests/slacktestbot/app.py
new file mode 100644
index 000000000..e8fb9b63c
--- /dev/null
+++ b/libraries/functional-tests/slacktestbot/app.py
@@ -0,0 +1,78 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import sys
+import traceback
+from datetime import datetime
+
+from aiohttp import web
+from aiohttp.web import Request, Response
+from botbuilder.adapters.slack import SlackAdapterOptions
+from botbuilder.adapters.slack import SlackAdapter
+from botbuilder.adapters.slack import SlackClient
+from botbuilder.core import TurnContext
+from botbuilder.core.integration import aiohttp_error_middleware
+from botbuilder.schema import Activity, ActivityTypes
+
+from bots import EchoBot
+from config import DefaultConfig
+
+CONFIG = DefaultConfig()
+
+# Create adapter.
+SLACK_OPTIONS = SlackAdapterOptions(
+    CONFIG.SLACK_VERIFICATION_TOKEN,
+    CONFIG.SLACK_BOT_TOKEN,
+    CONFIG.SLACK_CLIENT_SIGNING_SECRET,
+)
+SLACK_CLIENT = SlackClient(SLACK_OPTIONS)
+ADAPTER = SlackAdapter(SLACK_CLIENT)
+
+
+# Catch-all for errors.
+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.")
+    await context.send_activity(
+        "To continue to run this bot, please fix the bot source code."
+    )
+    # Send a trace activity if we're talking to the Bot Framework Emulator
+    if context.activity.channel_id == "emulator":
+        # Create a trace activity that contains the error object
+        trace_activity = Activity(
+            label="TurnError",
+            name="on_turn_error Trace",
+            timestamp=datetime.utcnow(),
+            type=ActivityTypes.trace,
+            value=f"{error}",
+            value_type="https://www.botframework.com/schemas/error",
+        )
+        # Send a trace activity, which will be displayed in Bot Framework Emulator
+        await context.send_activity(trace_activity)
+
+
+ADAPTER.on_turn_error = on_error
+
+# Create the Bot
+BOT = EchoBot()
+
+
+# Listen for incoming requests on /api/messages
+async def messages(req: Request) -> Response:
+    return await ADAPTER.process(req, BOT.on_turn)
+
+
+APP = web.Application(middlewares=[aiohttp_error_middleware])
+APP.router.add_post("/api/messages", messages)
+
+if __name__ == "__main__":
+    try:
+        web.run_app(APP, host="localhost", port=CONFIG.PORT)
+    except Exception as error:
+        raise error
diff --git a/samples/06.using-cards/bots/__init__.py b/libraries/functional-tests/slacktestbot/bots/__init__.py
similarity index 58%
rename from samples/06.using-cards/bots/__init__.py
rename to libraries/functional-tests/slacktestbot/bots/__init__.py
index 393acb3e7..f95fbbbad 100644
--- a/samples/06.using-cards/bots/__init__.py
+++ b/libraries/functional-tests/slacktestbot/bots/__init__.py
@@ -1,6 +1,6 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License.
 
-from .rich_cards_bot import RichCardsBot
+from .echo_bot import EchoBot
 
-__all__ = ["RichCardsBot"]
+__all__ = ["EchoBot"]
diff --git a/libraries/functional-tests/slacktestbot/bots/echo_bot.py b/libraries/functional-tests/slacktestbot/bots/echo_bot.py
new file mode 100644
index 000000000..c396a42f5
--- /dev/null
+++ b/libraries/functional-tests/slacktestbot/bots/echo_bot.py
@@ -0,0 +1,52 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import json
+import os
+
+from botbuilder.adapters.slack import SlackRequestBody, SlackEvent
+from botbuilder.core import ActivityHandler, MessageFactory, TurnContext
+from botbuilder.schema import ChannelAccount, Attachment
+
+
+class EchoBot(ActivityHandler):
+    async def on_members_added_activity(
+        self, members_added: [ChannelAccount], turn_context: TurnContext
+    ):
+        for member in members_added:
+            if member.id != turn_context.activity.recipient.id:
+                await turn_context.send_activity("Hello and welcome!")
+
+    async def on_message_activity(self, turn_context: TurnContext):
+        return await turn_context.send_activity(
+            MessageFactory.text(f"Echo: {turn_context.activity.text}")
+        )
+
+    async def on_event_activity(self, turn_context: TurnContext):
+        body = turn_context.activity.channel_data
+        if not body:
+            return
+
+        if isinstance(body, SlackRequestBody) and body.command == "/test":
+            interactive_message = MessageFactory.attachment(
+                self.__create_interactive_message(
+                    os.path.join(os.getcwd(), "./resources/InteractiveMessage.json")
+                )
+            )
+            await turn_context.send_activity(interactive_message)
+
+        if isinstance(body, SlackEvent):
+            if body.subtype == "file_share":
+                await turn_context.send_activity("Echo: I received and attachment")
+            elif body.message and body.message.attachments:
+                await turn_context.send_activity("Echo: I received a link share")
+
+    def __create_interactive_message(self, file_path: str) -> Attachment:
+        with open(file_path, "rb") as in_file:
+            adaptive_card_attachment = json.load(in_file)
+
+        return Attachment(
+            content=adaptive_card_attachment,
+            content_type="application/json",
+            name="blocks",
+        )
diff --git a/libraries/functional-tests/slacktestbot/config.py b/libraries/functional-tests/slacktestbot/config.py
new file mode 100644
index 000000000..73916b758
--- /dev/null
+++ b/libraries/functional-tests/slacktestbot/config.py
@@ -0,0 +1,15 @@
+#!/usr/bin/env python3
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+
+
+class DefaultConfig:
+    """ Bot Configuration """
+
+    PORT = 3978
+
+    SLACK_VERIFICATION_TOKEN = os.environ.get("SlackVerificationToken", "")
+    SLACK_BOT_TOKEN = os.environ.get("SlackBotToken", "")
+    SLACK_CLIENT_SIGNING_SECRET = os.environ.get("SlackClientSigningSecret", "")
diff --git a/libraries/functional-tests/slacktestbot/deploymentTemplates/template-with-new-rg.json b/libraries/functional-tests/slacktestbot/deploymentTemplates/template-with-new-rg.json
new file mode 100644
index 000000000..456508b2d
--- /dev/null
+++ b/libraries/functional-tests/slacktestbot/deploymentTemplates/template-with-new-rg.json
@@ -0,0 +1,297 @@
+{
+   "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
+   "contentVersion": "1.0.0.0",
+   "parameters": {
+       "groupLocation": {
+           "type": "string",
+           "metadata": {
+               "description": "Specifies the location of the Resource Group."
+           }
+       },
+       "groupName": {
+           "type": "string",
+           "metadata": {
+               "description": "Specifies the name of the Resource Group."
+           }
+       },
+       "appId": {
+           "type": "string",
+           "metadata": {
+               "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings."
+           }
+       },
+       "appSecret": {
+           "type": "string",
+           "metadata": {
+               "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings."
+           }
+       },
+       "botId": {
+           "type": "string",
+           "metadata": {
+               "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable."
+           }
+       },
+       "botSku": {
+           "type": "string",
+           "metadata": {
+               "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1."
+           }
+       },
+       "newAppServicePlanName": {
+           "type": "string",
+           "metadata": {
+               "description": "The name of the App Service Plan."
+           }
+       },
+       "newAppServicePlanSku": {
+           "type": "object",
+           "defaultValue": {
+               "name": "S1",
+               "tier": "Standard",
+               "size": "S1",
+               "family": "S",
+               "capacity": 1
+           },
+           "metadata": {
+               "description": "The SKU of the App Service Plan. Defaults to Standard values."
+           }
+       },
+       "newAppServicePlanLocation": {
+           "type": "string",
+           "metadata": {
+               "description": "The location of the App Service Plan. Defaults to \"westus\"."
+           }
+       },
+       "newWebAppName": {
+           "type": "string",
+           "defaultValue": "",
+           "metadata": {
+               "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"."
+           }
+       },
+       "slackVerificationToken": {
+           "type": "string",
+           "defaultValue": "",
+           "metadata": {
+               "description": "The slack verification token, taken from the Slack page after create an app."
+           }
+       },
+       "slackBotToken": {
+           "type": "string",
+           "defaultValue": "",
+           "metadata": {
+               "description": "The slack bot token, taken from the Slack page after create an app."
+           }
+       },
+       "slackClientSigningSecret": {
+           "type": "string",
+           "defaultValue": "",
+           "metadata": {
+               "description": "The slack client signing secret, taken from the Slack page after create an app."
+           }
+       }
+   },
+   "variables": {
+       "appServicePlanName": "[parameters('newAppServicePlanName')]",
+       "resourcesLocation": "[parameters('newAppServicePlanLocation')]",
+       "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]",
+       "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]",
+       "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]",
+       "publishingUsername": "[concat('$', parameters('newWebAppName'))]",
+       "resourceGroupId": "[concat(subscription().id, '/resourceGroups/', parameters('groupName'))]"
+   },
+   "resources": [
+       {
+           "name": "[parameters('groupName')]",
+           "type": "Microsoft.Resources/resourceGroups",
+           "apiVersion": "2018-05-01",
+           "location": "[parameters('groupLocation')]",
+           "properties": {}
+       },
+       {
+           "type": "Microsoft.Resources/deployments",
+           "apiVersion": "2018-05-01",
+           "name": "storageDeployment",
+           "resourceGroup": "[parameters('groupName')]",
+           "dependsOn": [
+               "[resourceId('Microsoft.Resources/resourceGroups/', parameters('groupName'))]"
+           ],
+           "properties": {
+               "mode": "Incremental",
+               "template": {
+                   "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
+                   "contentVersion": "1.0.0.0",
+                   "parameters": {},
+                   "variables": {},
+                   "resources": [
+                       {
+                           "comments": "Create a new Linux App Service Plan if no existing App Service Plan name was passed in.",
+                           "type": "Microsoft.Web/serverfarms",
+                           "name": "[variables('appServicePlanName')]",
+                           "apiVersion": "2018-02-01",
+                           "location": "[variables('resourcesLocation')]",
+                           "sku": "[parameters('newAppServicePlanSku')]",
+                           "kind": "linux",
+                           "properties": {
+                               "name": "[variables('appServicePlanName')]",
+                               "perSiteScaling": false,
+                               "reserved": true,
+                               "targetWorkerCount": 0,
+                               "targetWorkerSizeId": 0
+                           }
+                       },
+                       {
+                           "comments": "Create a Web App using a Linux App Service Plan",
+                           "type": "Microsoft.Web/sites",
+                           "apiVersion": "2015-08-01",
+                           "location": "[variables('resourcesLocation')]",
+                           "kind": "app,linux",
+                           "dependsOn": [
+                               "[concat(variables('resourceGroupId'), '/providers/Microsoft.Web/serverfarms/', variables('appServicePlanName'))]"
+                           ],
+                           "name": "[variables('webAppName')]",
+                           "properties": {
+                               "name": "[variables('webAppName')]",
+                               "hostNameSslStates": [
+                                   {
+                                       "name": "[concat(parameters('newWebAppName'), '.azurewebsites.net')]",
+                                       "sslState": "Disabled",
+                                       "hostType": "Standard"
+                                   },
+                                   {
+                                       "name": "[concat(parameters('newWebAppName'), '.scm.azurewebsites.net')]",
+                                       "sslState": "Disabled",
+                                       "hostType": "Repository"
+                                   }
+                               ],
+                               "serverFarmId": "[variables('appServicePlanName')]",
+                               "siteConfig": {
+                                   "appSettings": [
+                                       {
+                                           "name": "SCM_DO_BUILD_DURING_DEPLOYMENT",
+                                           "value": "true"
+                                       },
+                                       {
+                                           "name": "MicrosoftAppId",
+                                           "value": "[parameters('appId')]"
+                                       },
+                                       {
+                                           "name": "MicrosoftAppPassword",
+                                           "value": "[parameters('appSecret')]"
+                                       },
+                                       {
+                                          "name": "SlackVerificationToken",
+                                          "value": "[parameters('slackVerificationToken')]"
+                                       },
+                                       {
+                                          "name": "SlackBotToken",
+                                          "value": "[parameters('slackBotToken')]"
+                                       },
+                                       {
+                                          "name": "SlackClientSigningSecret",
+                                          "value": "[parameters('slackClientSigningSecret')]"
+                                       }
+                                   ],
+                                   "cors": {
+                                       "allowedOrigins": [
+                                           "https://botservice.hosting.portal.azure.net",
+                                           "https://hosting.onecloud.azure-test.net/"
+                                       ]
+                                   }
+                               }
+                           }
+                       },
+                       {
+                           "type": "Microsoft.Web/sites/config",
+                           "apiVersion": "2016-08-01",
+                           "name": "[concat(variables('webAppName'), '/web')]",
+                           "location": "[variables('resourcesLocation')]",
+                           "dependsOn": [
+                               "[concat(variables('resourceGroupId'), '/providers/Microsoft.Web/sites/', variables('webAppName'))]"
+                           ],
+                           "properties": {
+                               "numberOfWorkers": 1,
+                               "defaultDocuments": [
+                                   "Default.htm",
+                                   "Default.html",
+                                   "Default.asp",
+                                   "index.htm",
+                                   "index.html",
+                                   "iisstart.htm",
+                                   "default.aspx",
+                                   "index.php",
+                                   "hostingstart.html"
+                               ],
+                               "netFrameworkVersion": "v4.0",
+                               "phpVersion": "",
+                               "pythonVersion": "",
+                               "nodeVersion": "",
+                               "linuxFxVersion": "PYTHON|3.7",
+                               "requestTracingEnabled": false,
+                               "remoteDebuggingEnabled": false,
+                               "remoteDebuggingVersion": "VS2017",
+                               "httpLoggingEnabled": true,
+                               "logsDirectorySizeLimit": 35,
+                               "detailedErrorLoggingEnabled": false,
+                               "publishingUsername": "[variables('publishingUsername')]",
+                               "scmType": "None",
+                               "use32BitWorkerProcess": true,
+                               "webSocketsEnabled": false,
+                               "alwaysOn": false,
+                               "appCommandLine": "gunicorn --bind 0.0.0.0 --worker-class aiohttp.worker.GunicornWebWorker --timeout 600 app:APP",
+                               "managedPipelineMode": "Integrated",
+                               "virtualApplications": [
+                                   {
+                                       "virtualPath": "/",
+                                       "physicalPath": "site\\wwwroot",
+                                       "preloadEnabled": false,
+                                       "virtualDirectories": null
+                                   }
+                               ],
+                               "winAuthAdminState": 0,
+                               "winAuthTenantState": 0,
+                               "customAppPoolIdentityAdminState": false,
+                               "customAppPoolIdentityTenantState": false,
+                               "loadBalancing": "LeastRequests",
+                               "routingRules": [],
+                               "experiments": {
+                                   "rampUpRules": []
+                               },
+                               "autoHealEnabled": false,
+                               "vnetName": "",
+                               "minTlsVersion": "1.2",
+                               "ftpsState": "AllAllowed",
+                               "reservedInstanceCount": 0
+                           }
+                       },
+                       {
+                           "apiVersion": "2017-12-01",
+                           "type": "Microsoft.BotService/botServices",
+                           "name": "[parameters('botId')]",
+                           "location": "global",
+                           "kind": "bot",
+                           "sku": {
+                               "name": "[parameters('botSku')]"
+                           },
+                           "properties": {
+                               "name": "[parameters('botId')]",
+                               "displayName": "[parameters('botId')]",
+                               "endpoint": "[variables('botEndpoint')]",
+                               "msaAppId": "[parameters('appId')]",
+                               "developerAppInsightsApplicationId": null,
+                               "developerAppInsightKey": null,
+                               "publishingCredentials": null,
+                               "storageResourceId": null
+                           },
+                           "dependsOn": [
+                               "[concat(variables('resourceGroupId'), '/providers/Microsoft.Web/sites/', variables('webAppName'))]"
+                           ]
+                       }
+                   ],
+                   "outputs": {}
+               }
+           }
+       }
+   ]
+}
diff --git a/libraries/functional-tests/slacktestbot/deploymentTemplates/template-with-preexisting-rg.json b/libraries/functional-tests/slacktestbot/deploymentTemplates/template-with-preexisting-rg.json
new file mode 100644
index 000000000..0a393754c
--- /dev/null
+++ b/libraries/functional-tests/slacktestbot/deploymentTemplates/template-with-preexisting-rg.json
@@ -0,0 +1,275 @@
+{
+   "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
+   "contentVersion": "1.0.0.0",
+   "parameters": {
+      "appId": {
+         "type": "string",
+         "metadata": {
+            "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings."
+         }
+      },
+      "appSecret": {
+         "type": "string",
+         "metadata": {
+            "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings."
+         }
+      },
+      "botId": {
+         "type": "string",
+         "metadata": {
+            "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable."
+         }
+      },
+      "botSku": {
+         "defaultValue": "F0",
+         "type": "string",
+         "metadata": {
+            "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1."
+         }
+      },
+      "newAppServicePlanName": {
+         "type": "string",
+         "defaultValue": "",
+         "metadata": {
+            "description": "The name of the new App Service Plan."
+         }
+      },
+      "newAppServicePlanSku": {
+         "type": "object",
+         "defaultValue": {
+            "name": "S1",
+            "tier": "Standard",
+            "size": "S1",
+            "family": "S",
+            "capacity": 1
+         },
+         "metadata": {
+            "description": "The SKU of the App Service Plan. Defaults to Standard values."
+         }
+      },
+      "appServicePlanLocation": {
+         "type": "string",
+         "metadata": {
+            "description": "The location of the App Service Plan."
+         }
+      },
+      "existingAppServicePlan": {
+         "type": "string",
+         "defaultValue": "",
+         "metadata": {
+            "description": "Name of the existing App Service Plan used to create the Web App for the bot."
+         }
+      },
+      "newWebAppName": {
+         "type": "string",
+         "defaultValue": "",
+         "metadata": {
+            "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"."
+         }
+      },
+      "slackVerificationToken": {
+         "type": "string",
+         "defaultValue": "",
+         "metadata": {
+            "description": "The slack verification token, taken from the Slack page after create an app."
+         }
+      },
+      "slackBotToken": {
+         "type": "string",
+         "defaultValue": "",
+         "metadata": {
+            "description": "The slack bot token, taken from the Slack page after create an app."
+         }
+      },
+      "slackClientSigningSecret": {
+         "type": "string",
+         "defaultValue": "",
+         "metadata": {
+            "description": "The slack client signing secret, taken from the Slack page after create an app."
+         }
+      }
+   },
+   "variables": {
+      "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlan')), 'createNewAppServicePlan', parameters('existingAppServicePlan'))]",
+      "useExistingAppServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]",
+      "servicePlanName": "[if(variables('useExistingAppServicePlan'), parameters('existingAppServicePlan'), parameters('newAppServicePlanName'))]",
+      "publishingUsername": "[concat('$', parameters('newWebAppName'))]",
+      "resourcesLocation": "[parameters('appServicePlanLocation')]",
+      "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]",
+      "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]",
+      "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]"
+   },
+   "resources": [
+      {
+         "comments": "Create a new Linux App Service Plan if no existing App Service Plan name was passed in.",
+         "type": "Microsoft.Web/serverfarms",
+         "apiVersion": "2016-09-01",
+         "name": "[variables('servicePlanName')]",
+         "location": "[variables('resourcesLocation')]",
+         "sku": "[parameters('newAppServicePlanSku')]",
+         "kind": "linux",
+         "properties": {
+            "name": "[variables('servicePlanName')]",
+            "perSiteScaling": false,
+            "reserved": true,
+            "targetWorkerCount": 0,
+            "targetWorkerSizeId": 0
+         }
+      },
+      {
+         "comments": "Create a Web App using a Linux App Service Plan",
+         "type": "Microsoft.Web/sites",
+         "apiVersion": "2016-08-01",
+         "name": "[variables('webAppName')]",
+         "location": "[variables('resourcesLocation')]",
+         "dependsOn": [
+            "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]"
+         ],
+         "kind": "app,linux",
+         "properties": {
+            "enabled": true,
+            "hostNameSslStates": [
+               {
+                  "name": "[concat(parameters('newWebAppName'), '.azurewebsites.net')]",
+                  "sslState": "Disabled",
+                  "hostType": "Standard"
+               },
+               {
+                  "name": "[concat(parameters('newWebAppName'), '.scm.azurewebsites.net')]",
+                  "sslState": "Disabled",
+                  "hostType": "Repository"
+               }
+            ],
+            "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]",
+            "reserved": true,
+            "scmSiteAlsoStopped": false,
+            "clientAffinityEnabled": false,
+            "clientCertEnabled": false,
+            "hostNamesDisabled": false,
+            "containerSize": 0,
+            "dailyMemoryTimeQuota": 0,
+            "httpsOnly": false,
+            "siteConfig": {
+               "appSettings": [
+                  {
+                     "name": "MicrosoftAppId",
+                     "value": "[parameters('appId')]"
+                  },
+                  {
+                     "name": "MicrosoftAppPassword",
+                     "value": "[parameters('appSecret')]"
+                  },
+                  {
+                     "name": "SlackVerificationToken",
+                     "value": "[parameters('slackVerificationToken')]"
+                  },
+                  {
+                     "name": "SlackBotToken",
+                     "value": "[parameters('slackBotToken')]"
+                  },
+                  {
+                     "name": "SlackClientSigningSecret",
+                     "value": "[parameters('slackClientSigningSecret')]"
+                  },
+                  {
+                     "name": "SCM_DO_BUILD_DURING_DEPLOYMENT",
+                     "value": "true"
+                  }
+               ],
+               "cors": {
+                  "allowedOrigins": [
+                     "https://botservice.hosting.portal.azure.net",
+                     "https://hosting.onecloud.azure-test.net/"
+                  ]
+               }
+            }
+         }
+      },
+      {
+         "type": "Microsoft.Web/sites/config",
+         "apiVersion": "2016-08-01",
+         "name": "[concat(variables('webAppName'), '/web')]",
+         "location": "[variables('resourcesLocation')]",
+         "dependsOn": [
+            "[resourceId('Microsoft.Web/sites', variables('webAppName'))]"
+         ],
+         "properties": {
+            "numberOfWorkers": 1,
+            "defaultDocuments": [
+               "Default.htm",
+               "Default.html",
+               "Default.asp",
+               "index.htm",
+               "index.html",
+               "iisstart.htm",
+               "default.aspx",
+               "index.php",
+               "hostingstart.html"
+            ],
+            "netFrameworkVersion": "v4.0",
+            "phpVersion": "",
+            "pythonVersion": "",
+            "nodeVersion": "",
+            "linuxFxVersion": "PYTHON|3.7",
+            "requestTracingEnabled": false,
+            "remoteDebuggingEnabled": false,
+            "remoteDebuggingVersion": "VS2017",
+            "httpLoggingEnabled": true,
+            "logsDirectorySizeLimit": 35,
+            "detailedErrorLoggingEnabled": false,
+            "publishingUsername": "[variables('publishingUsername')]",
+            "scmType": "None",
+            "use32BitWorkerProcess": true,
+            "webSocketsEnabled": false,
+            "alwaysOn": false,
+            "appCommandLine": "gunicorn --bind 0.0.0.0 --worker-class aiohttp.worker.GunicornWebWorker --timeout 600 app:APP",
+            "managedPipelineMode": "Integrated",
+            "virtualApplications": [
+               {
+                  "virtualPath": "/",
+                  "physicalPath": "site\\wwwroot",
+                  "preloadEnabled": false,
+                  "virtualDirectories": null
+               }
+            ],
+            "winAuthAdminState": 0,
+            "winAuthTenantState": 0,
+            "customAppPoolIdentityAdminState": false,
+            "customAppPoolIdentityTenantState": false,
+            "loadBalancing": "LeastRequests",
+            "routingRules": [],
+            "experiments": {
+               "rampUpRules": []
+            },
+            "autoHealEnabled": false,
+            "vnetName": "",
+            "minTlsVersion": "1.2",
+            "ftpsState": "AllAllowed",
+            "reservedInstanceCount": 0
+         }
+      },
+      {
+         "apiVersion": "2017-12-01",
+         "type": "Microsoft.BotService/botServices",
+         "name": "[parameters('botId')]",
+         "location": "global",
+         "kind": "bot",
+         "sku": {
+            "name": "[parameters('botSku')]"
+         },
+         "properties": {
+            "name": "[parameters('botId')]",
+            "displayName": "[parameters('botId')]",
+            "endpoint": "[variables('botEndpoint')]",
+            "msaAppId": "[parameters('appId')]",
+            "developerAppInsightsApplicationId": null,
+            "developerAppInsightKey": null,
+            "publishingCredentials": null,
+            "storageResourceId": null
+         },
+         "dependsOn": [
+            "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]"
+         ]
+      }
+   ]
+}
diff --git a/libraries/functional-tests/slacktestbot/media/AzureAppRegistration1.png b/libraries/functional-tests/slacktestbot/media/AzureAppRegistration1.png
new file mode 100644
index 000000000..c39964a14
Binary files /dev/null and b/libraries/functional-tests/slacktestbot/media/AzureAppRegistration1.png differ
diff --git a/libraries/functional-tests/slacktestbot/media/AzureAppRegistration2.png b/libraries/functional-tests/slacktestbot/media/AzureAppRegistration2.png
new file mode 100644
index 000000000..5f64b6220
Binary files /dev/null and b/libraries/functional-tests/slacktestbot/media/AzureAppRegistration2.png differ
diff --git a/libraries/functional-tests/slacktestbot/media/AzurePipelineSetup1.png b/libraries/functional-tests/slacktestbot/media/AzurePipelineSetup1.png
new file mode 100644
index 000000000..89cb0b303
Binary files /dev/null and b/libraries/functional-tests/slacktestbot/media/AzurePipelineSetup1.png differ
diff --git a/libraries/functional-tests/slacktestbot/media/AzurePipelineSetup2.png b/libraries/functional-tests/slacktestbot/media/AzurePipelineSetup2.png
new file mode 100644
index 000000000..a5ca27f38
Binary files /dev/null and b/libraries/functional-tests/slacktestbot/media/AzurePipelineSetup2.png differ
diff --git a/libraries/functional-tests/slacktestbot/media/AzurePipelineVariables.png b/libraries/functional-tests/slacktestbot/media/AzurePipelineVariables.png
new file mode 100644
index 000000000..15554ac3a
Binary files /dev/null and b/libraries/functional-tests/slacktestbot/media/AzurePipelineVariables.png differ
diff --git a/libraries/functional-tests/slacktestbot/media/SlackAppCredentials.png b/libraries/functional-tests/slacktestbot/media/SlackAppCredentials.png
new file mode 100644
index 000000000..abd5b1e2f
Binary files /dev/null and b/libraries/functional-tests/slacktestbot/media/SlackAppCredentials.png differ
diff --git a/libraries/functional-tests/slacktestbot/media/SlackChannelID.png b/libraries/functional-tests/slacktestbot/media/SlackChannelID.png
new file mode 100644
index 000000000..f2abf665f
Binary files /dev/null and b/libraries/functional-tests/slacktestbot/media/SlackChannelID.png differ
diff --git a/libraries/functional-tests/slacktestbot/media/SlackCreateSlackApp.png b/libraries/functional-tests/slacktestbot/media/SlackCreateSlackApp.png
new file mode 100644
index 000000000..157e94639
Binary files /dev/null and b/libraries/functional-tests/slacktestbot/media/SlackCreateSlackApp.png differ
diff --git a/libraries/functional-tests/slacktestbot/media/SlackGrantScopes.png b/libraries/functional-tests/slacktestbot/media/SlackGrantScopes.png
new file mode 100644
index 000000000..d2969aae1
Binary files /dev/null and b/libraries/functional-tests/slacktestbot/media/SlackGrantScopes.png differ
diff --git a/libraries/functional-tests/slacktestbot/media/SlackInstallApp.png b/libraries/functional-tests/slacktestbot/media/SlackInstallApp.png
new file mode 100644
index 000000000..f6ae3ee08
Binary files /dev/null and b/libraries/functional-tests/slacktestbot/media/SlackInstallApp.png differ
diff --git a/libraries/functional-tests/slacktestbot/media/SlackOAuthToken.png b/libraries/functional-tests/slacktestbot/media/SlackOAuthToken.png
new file mode 100644
index 000000000..322b7cdee
Binary files /dev/null and b/libraries/functional-tests/slacktestbot/media/SlackOAuthToken.png differ
diff --git a/libraries/functional-tests/slacktestbot/requirements.txt b/libraries/functional-tests/slacktestbot/requirements.txt
new file mode 100644
index 000000000..0cdcf62b8
--- /dev/null
+++ b/libraries/functional-tests/slacktestbot/requirements.txt
@@ -0,0 +1,2 @@
+botbuilder-integration-aiohttp>=4.11.0
+botbuilder-adapters-slack>=4.11.0
diff --git a/libraries/functional-tests/slacktestbot/resources/InteractiveMessage.json b/libraries/functional-tests/slacktestbot/resources/InteractiveMessage.json
new file mode 100644
index 000000000..91637db25
--- /dev/null
+++ b/libraries/functional-tests/slacktestbot/resources/InteractiveMessage.json
@@ -0,0 +1,62 @@
+[
+  {
+    "type": "section",
+    "text": {
+      "type": "mrkdwn",
+      "text": "Hello, Assistant to the Regional Manager Dwight! *Michael Scott* wants to know where you'd like to take the Paper Company investors to dinner tonight.\n\n *Please select a restaurant:*"
+    }
+  },
+  {
+    "type": "divider"
+  },
+  {
+    "type": "section",
+    "text": {
+      "type": "mrkdwn",
+      "text": "*Farmhouse Thai Cuisine*\n:star::star::star::star: 1528 reviews\n They do have some vegan options, like the roti and curry, plus they have a ton of salad stuff and noodles can be ordered without meat!! They have something for everyone here"
+    },
+    "accessory": {
+      "type": "image",
+      "image_url": "https://s3-media3.fl.yelpcdn.com/bphoto/c7ed05m9lC2EmA3Aruue7A/o.jpg",
+      "alt_text": "alt text for image"
+    }
+  },
+  {
+    "type": "section",
+    "text": {
+      "type": "mrkdwn",
+      "text": "*Ler Ros*\n:star::star::star::star: 2082 reviews\n I would really recommend the  Yum Koh Moo Yang - Spicy lime dressing and roasted quick marinated pork shoulder, basil leaves, chili & rice powder."
+    },
+    "accessory": {
+      "type": "image",
+      "image_url": "https://s3-media2.fl.yelpcdn.com/bphoto/DawwNigKJ2ckPeDeDM7jAg/o.jpg",
+      "alt_text": "alt text for image"
+    }
+  },
+  {
+    "type": "divider"
+  },
+  {
+    "type": "actions",
+    "elements": [
+      {
+        "type": "button",
+        "text": {
+          "type": "plain_text",
+          "text": "Farmhouse",
+          "emoji": true
+        },
+        "value": "Farmhouse"
+      },
+      {
+        "type": "button",
+        "text": {
+          "type": "plain_text",
+          "text": "Ler Ros",
+          "emoji": true
+        },
+        "value": "Ler Ros"
+      }
+    ]
+  }
+]
\ No newline at end of file
diff --git a/libraries/functional-tests/tests/direct_line_client.py b/libraries/functional-tests/tests/direct_line_client.py
new file mode 100644
index 000000000..2adda6b0d
--- /dev/null
+++ b/libraries/functional-tests/tests/direct_line_client.py
@@ -0,0 +1,92 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from typing import Tuple
+
+import requests
+from requests import Response
+
+
+class DirectLineClient:
+    """A direct line client that sends and receives messages."""
+
+    def __init__(self, direct_line_secret: str):
+        self._direct_line_secret: str = direct_line_secret
+        self._base_url: str = "https://directline.botframework.com/v3/directline"
+        self._set_headers()
+        self._start_conversation()
+        self._watermark: str = ""
+
+    def send_message(self, text: str, retry_count: int = 3) -> Response:
+        """Send raw text to bot framework using direct line api"""
+
+        url = "/".join(
+            [self._base_url, "conversations", self._conversation_id, "activities"]
+        )
+        json_payload = {
+            "conversationId": self._conversation_id,
+            "type": "message",
+            "from": {"id": "user1"},
+            "text": text,
+        }
+
+        success = False
+        current_retry = 0
+        bot_response = None
+        while not success and current_retry < retry_count:
+            bot_response = requests.post(url, headers=self._headers, json=json_payload)
+            current_retry += 1
+            if bot_response.status_code == 200:
+                success = True
+
+        return bot_response
+
+    def get_message(self, retry_count: int = 3) -> Tuple[Response, str]:
+        """Get a response message back from the bot framework using direct line api"""
+
+        url = "/".join(
+            [self._base_url, "conversations", self._conversation_id, "activities"]
+        )
+        url = url + "?watermark=" + self._watermark
+
+        success = False
+        current_retry = 0
+        bot_response = None
+        while not success and current_retry < retry_count:
+            bot_response = requests.get(
+                url,
+                headers=self._headers,
+                json={"conversationId": self._conversation_id},
+            )
+            current_retry += 1
+            if bot_response.status_code == 200:
+                success = True
+                json_response = bot_response.json()
+
+                if "watermark" in json_response:
+                    self._watermark = json_response["watermark"]
+
+                if "activities" in json_response:
+                    activities_count = len(json_response["activities"])
+                    if activities_count > 0:
+                        return (
+                            bot_response,
+                            json_response["activities"][activities_count - 1]["text"],
+                        )
+                    return bot_response, "No new messages"
+        return bot_response, "error contacting bot for response"
+
+    def _set_headers(self) -> None:
+        headers = {"Content-Type": "application/json"}
+        value = " ".join(["Bearer", self._direct_line_secret])
+        headers.update({"Authorization": value})
+        self._headers = headers
+
+    def _start_conversation(self) -> None:
+        # Start conversation and get us a conversationId to use
+        url = "/".join([self._base_url, "conversations"])
+        bot_response = requests.post(url, headers=self._headers)
+
+        # Extract the conversationID for sending messages to bot
+        json_response = bot_response.json()
+        self._conversation_id = json_response["conversationId"]
diff --git a/libraries/functional-tests/tests/test_py_bot.py b/libraries/functional-tests/tests/test_py_bot.py
new file mode 100644
index 000000000..bdea7fd6c
--- /dev/null
+++ b/libraries/functional-tests/tests/test_py_bot.py
@@ -0,0 +1,26 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+from unittest import TestCase
+
+from direct_line_client import DirectLineClient
+
+
+class PyBotTest(TestCase):
+    def test_deployed_bot_answer(self):
+        direct_line_secret = os.environ.get("DIRECT_LINE_KEY", "")
+        if direct_line_secret == "":
+            return
+
+        client = DirectLineClient(direct_line_secret)
+        user_message: str = "Contoso"
+
+        send_result = client.send_message(user_message)
+        self.assertIsNotNone(send_result)
+        self.assertEqual(200, send_result.status_code)
+
+        response, text = client.get_message()
+        self.assertIsNotNone(response)
+        self.assertEqual(200, response.status_code)
+        self.assertEqual(f"You said '{user_message}'", text)
diff --git a/libraries/functional-tests/tests/test_slack_client.py b/libraries/functional-tests/tests/test_slack_client.py
new file mode 100644
index 000000000..cc5abd74d
--- /dev/null
+++ b/libraries/functional-tests/tests/test_slack_client.py
@@ -0,0 +1,113 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import hashlib
+import hmac
+import json
+import os
+import uuid
+import datetime
+import time
+import aiounittest
+import requests
+
+
+class SlackClient(aiounittest.AsyncTestCase):
+    async def test_send_and_receive_slack_message(self):
+        # Arrange
+        echo_guid = str(uuid.uuid4())
+
+        # Act
+        await self._send_message_async(echo_guid)
+        response = await self._receive_message_async()
+
+        # Assert
+        self.assertEqual(f"Echo: {echo_guid}", response)
+
+    async def _receive_message_async(self) -> str:
+        last_message = ""
+        i = 0
+
+        while "Echo" not in last_message and i < 60:
+            url = (
+                f"{self._slack_url_base}/conversations.history?token="
+                f"{self._slack_bot_token}&channel={self._slack_channel}"
+            )
+            response = requests.get(url,)
+            last_message = response.json()["messages"][0]["text"]
+
+            time.sleep(1)
+            i += 1
+
+        return last_message
+
+    async def _send_message_async(self, echo_guid: str) -> None:
+        timestamp = str(int(datetime.datetime.utcnow().timestamp()))
+        message = self._create_message(echo_guid)
+        hub_signature = self._create_hub_signature(message, timestamp)
+        headers = {
+            "X-Slack-Request-Timestamp": timestamp,
+            "X-Slack-Signature": hub_signature,
+            "Content-type": "application/json",
+        }
+        url = f"https://{self._bot_name}.azurewebsites.net/api/messages"
+
+        requests.post(url, headers=headers, data=message)
+
+    def _create_message(self, echo_guid: str) -> str:
+        slack_event = {
+            "client_msg_id": "client_msg_id",
+            "type": "message",
+            "text": echo_guid,
+            "user": "userId",
+            "channel": self._slack_channel,
+            "channel_type": "im",
+        }
+
+        message = {
+            "token": self._slack_verification_token,
+            "team_id": "team_id",
+            "api_app_id": "apiAppId",
+            "event": slack_event,
+            "type": "event_callback",
+        }
+
+        return json.dumps(message)
+
+    def _create_hub_signature(self, message: str, timestamp: str) -> str:
+        signature = ["v0", timestamp, message]
+        base_string = ":".join(signature)
+
+        computed_signature = "V0=" + hmac.new(
+            bytes(self._slack_client_signing_secret, encoding="utf8"),
+            msg=bytes(base_string, "utf-8"),
+            digestmod=hashlib.sha256,
+        ).hexdigest().upper().replace("-", "")
+
+        return computed_signature
+
+    @classmethod
+    def setUpClass(cls) -> None:
+        cls._slack_url_base: str = "https://slack.com/api"
+
+        cls._slack_channel = os.getenv("SlackChannel")
+        if not cls._slack_channel:
+            raise Exception('Environment variable "SlackChannel" not found.')
+
+        cls._slack_bot_token = os.getenv("SlackBotToken")
+        if not cls._slack_bot_token:
+            raise Exception('Environment variable "SlackBotToken" not found.')
+
+        cls._slack_client_signing_secret = os.getenv("SlackClientSigningSecret")
+        if not cls._slack_client_signing_secret:
+            raise Exception(
+                'Environment variable "SlackClientSigningSecret" not found.'
+            )
+
+        cls._slack_verification_token = os.getenv("SlackVerificationToken")
+        if not cls._slack_verification_token:
+            raise Exception('Environment variable "SlackVerificationToken" not found.')
+
+        cls._bot_name = os.getenv("BotName")
+        if not cls._bot_name:
+            raise Exception('Environment variable "BotName" not found.')
diff --git a/libraries/swagger/ConnectorAPI.json b/libraries/swagger/ConnectorAPI.json
index 827186e12..af940d70d 100644
--- a/libraries/swagger/ConnectorAPI.json
+++ b/libraries/swagger/ConnectorAPI.json
@@ -1,2674 +1,2693 @@
 {
-  "swagger": "2.0",
-  "info": {
-    "version": "v3",
-    "title": "Microsoft Bot Connector API - v3.0",
-    "description": "The Bot Connector REST API allows your bot to send and receive messages to channels configured in the\r\n[Bot Framework Developer Portal](https://dev.botframework.com). The Connector service uses industry-standard REST\r\nand JSON over HTTPS.\r\n\r\nClient libraries for this REST API are available. See below for a list.\r\n\r\nMany bots will use both the Bot Connector REST API and the associated [Bot State REST API](/en-us/restapi/state). The\r\nBot State REST API allows a bot to store and retrieve state associated with users and conversations.\r\n\r\nAuthentication for both the Bot Connector and Bot State REST APIs is accomplished with JWT Bearer tokens, and is\r\ndescribed in detail in the [Connector Authentication](/en-us/restapi/authentication) document.\r\n\r\n# Client Libraries for the Bot Connector REST API\r\n\r\n* [Bot Builder for C#](/en-us/csharp/builder/sdkreference/)\r\n* [Bot Builder for Node.js](/en-us/node/builder/overview/)\r\n* Generate your own from the [Connector API Swagger file](https://raw.githubusercontent.com/Microsoft/BotBuilder/master/CSharp/Library/Microsoft.Bot.Connector.Shared/Swagger/ConnectorAPI.json)\r\n\r\n© 2016 Microsoft",
-    "termsOfService": "https://www.microsoft.com/en-us/legal/intellectualproperty/copyright/default.aspx",
-    "contact": {
-      "name": "Bot Framework",
-      "url": "https://botframework.com",
-      "email": "botframework@microsoft.com"
-    },
-    "license": {
-      "name": "The MIT License (MIT)",
-      "url": "https://opensource.org/licenses/MIT"
-    }
-  },
-  "host": "api.botframework.com",
-  "schemes": [
-    "https"
-  ],
-  "paths": {
-    "/v3/attachments/{attachmentId}": {
-      "get": {
-        "tags": [
-          "Attachments"
-        ],
-        "summary": "GetAttachmentInfo",
-        "description": "Get AttachmentInfo structure describing the attachment views",
-        "operationId": "Attachments_GetAttachmentInfo",
-        "consumes": [],
-        "produces": [
-          "application/json",
-          "text/json",
-          "application/xml",
-          "text/xml"
-        ],
-        "parameters": [
-          {
-            "name": "attachmentId",
-            "in": "path",
-            "description": "attachment id",
-            "required": true,
-            "type": "string"
+    "swagger": "2.0",
+    "info": {
+      "version": "v3",
+      "title": "Microsoft Bot Connector API - v3.0",
+      "description": "The Bot Connector REST API allows your bot to send and receive messages to channels configured in the\r\n[Bot Framework Developer Portal](https://dev.botframework.com). The Connector service uses industry-standard REST\r\nand JSON over HTTPS.\r\n\r\nClient libraries for this REST API are available. See below for a list.\r\n\r\nMany bots will use both the Bot Connector REST API and the associated [Bot State REST API](/en-us/restapi/state). The\r\nBot State REST API allows a bot to store and retrieve state associated with users and conversations.\r\n\r\nAuthentication for both the Bot Connector and Bot State REST APIs is accomplished with JWT Bearer tokens, and is\r\ndescribed in detail in the [Connector Authentication](/en-us/restapi/authentication) document.\r\n\r\n# Client Libraries for the Bot Connector REST API\r\n\r\n* [Bot Builder for C#](/en-us/csharp/builder/sdkreference/)\r\n* [Bot Builder for Node.js](/en-us/node/builder/overview/)\r\n* Generate your own from the [Connector API Swagger file](https://raw.githubusercontent.com/Microsoft/BotBuilder/master/CSharp/Library/Microsoft.Bot.Connector.Shared/Swagger/ConnectorAPI.json)\r\n\r\n© 2016 Microsoft",
+      "termsOfService": "https://www.microsoft.com/en-us/legal/intellectualproperty/copyright/default.aspx",
+      "contact": {
+        "name": "Bot Framework",
+        "url": "https://botframework.com",
+        "email": "botframework@microsoft.com"
+      },
+      "license": {
+        "name": "The MIT License (MIT)",
+        "url": "https://opensource.org/licenses/MIT"
+      }
+    },
+    "host": "api.botframework.com",
+    "schemes": [
+      "https"
+    ],
+    "paths": {
+      "/v3/attachments/{attachmentId}": {
+        "get": {
+          "tags": [
+            "Attachments"
+          ],
+          "summary": "GetAttachmentInfo",
+          "description": "Get AttachmentInfo structure describing the attachment views",
+          "operationId": "Attachments_GetAttachmentInfo",
+          "consumes": [],
+          "produces": [
+            "application/json",
+            "text/json",
+            "application/xml",
+            "text/xml"
+          ],
+          "parameters": [
+            {
+              "name": "attachmentId",
+              "in": "path",
+              "description": "attachment id",
+              "required": true,
+              "type": "string"
+            }
+          ],
+          "responses": {
+            "200": {
+              "description": "An attachmentInfo object is returned which describes the:\r\n* type of the attachment\r\n* name of the attachment\r\n\r\n\r\nand an array of views:\r\n* Size - size of the object\r\n* ViewId - View Id which can be used to fetch a variation on the content (ex: original or thumbnail)",
+              "schema": {
+                "$ref": "#/definitions/AttachmentInfo"
+              }
+            },
+            "default": {
+              "description": "The operation failed and the response is an error object describing the status code and failure.",
+              "schema": {
+                "$ref": "#/definitions/ErrorResponse"
+              }
+            }
           }
-        ],
-        "responses": {
-          "200": {
-            "description": "An attachmentInfo object is returned which describes the:\r\n* type of the attachment\r\n* name of the attachment\r\n\r\n\r\nand an array of views:\r\n* Size - size of the object\r\n* ViewId - View Id which can be used to fetch a variation on the content (ex: original or thumbnail)",
-            "schema": {
-              "$ref": "#/definitions/AttachmentInfo"
+        }
+      },
+      "/v3/attachments/{attachmentId}/views/{viewId}": {
+        "get": {
+          "tags": [
+            "Attachments"
+          ],
+          "summary": "GetAttachment",
+          "description": "Get the named view as binary content",
+          "operationId": "Attachments_GetAttachment",
+          "consumes": [],
+          "produces": [
+            "application/json",
+            "text/json",
+            "application/xml",
+            "text/xml"
+          ],
+          "parameters": [
+            {
+              "name": "attachmentId",
+              "in": "path",
+              "description": "attachment id",
+              "required": true,
+              "type": "string"
+            },
+            {
+              "name": "viewId",
+              "in": "path",
+              "description": "View id from attachmentInfo",
+              "required": true,
+              "type": "string"
             }
-          },
-          "default": {
-            "description": "The operation failed and the response is an error object describing the status code and failure.",
-            "schema": {
-              "$ref": "#/definitions/ErrorResponse"
+          ],
+          "responses": {
+            "200": {
+              "description": "Attachment stream",
+              "schema": {
+                "format": "byte",
+                "type": "file"
+              }
+            },
+            "301": {
+              "description": "The Location header describes where the content is now."
+            },
+            "302": {
+              "description": "The Location header describes where the content is now."
+            },
+            "default": {
+              "description": "The operation failed and the response is an error object describing the status code and failure.",
+              "schema": {
+                "$ref": "#/definitions/ErrorResponse"
+              }
             }
           }
         }
-      }
-    },
-    "/v3/attachments/{attachmentId}/views/{viewId}": {
-      "get": {
-        "tags": [
-          "Attachments"
-        ],
-        "summary": "GetAttachment",
-        "description": "Get the named view as binary content",
-        "operationId": "Attachments_GetAttachment",
-        "consumes": [],
-        "produces": [
-          "application/json",
-          "text/json",
-          "application/xml",
-          "text/xml"
-        ],
-        "parameters": [
-          {
-            "name": "attachmentId",
-            "in": "path",
-            "description": "attachment id",
-            "required": true,
-            "type": "string"
-          },
-          {
-            "name": "viewId",
-            "in": "path",
-            "description": "View id from attachmentInfo",
-            "required": true,
-            "type": "string"
+      },
+      "/v3/conversations": {
+        "get": {
+          "tags": [
+            "Conversations"
+          ],
+          "summary": "GetConversations",
+          "description": "List the Conversations in which this bot has participated.\r\n\r\nGET from this method with a skip token\r\n\r\nThe return value is a ConversationsResult, which contains an array of ConversationMembers and a skip token.  If the skip token is not empty, then \r\nthere are further values to be returned. Call this method again with the returned token to get more values.\r\n\r\nEach ConversationMembers object contains the ID of the conversation and an array of ChannelAccounts that describe the members of the conversation.",
+          "operationId": "Conversations_GetConversations",
+          "consumes": [],
+          "produces": [
+            "application/json",
+            "text/json",
+            "application/xml",
+            "text/xml"
+          ],
+          "parameters": [
+            {
+              "name": "continuationToken",
+              "in": "query",
+              "description": "skip or continuation token",
+              "required": false,
+              "type": "string"
+            }
+          ],
+          "responses": {
+            "200": {
+              "description": "An object will be returned containing \r\n* an array (Conversations) of ConversationMembers objects\r\n* a continuation token\r\n\r\nEach ConversationMembers object contains:\r\n* the Id of the conversation\r\n* an array (Members) of ChannelAccount objects",
+              "schema": {
+                "$ref": "#/definitions/ConversationsResult"
+              }
+            },
+            "default": {
+              "description": "The operation failed and the response is an error object describing the status code and failure.",
+              "schema": {
+                "$ref": "#/definitions/ErrorResponse"
+              }
+            }
           }
-        ],
-        "responses": {
-          "200": {
-            "description": "Attachment stream",
-            "schema": {
-              "format": "byte",
-              "type": "file"
+        },
+        "post": {
+          "tags": [
+            "Conversations"
+          ],
+          "summary": "CreateConversation",
+          "description": "Create a new Conversation.\r\n\r\nPOST to this method with a\r\n* Bot being the bot creating the conversation\r\n* IsGroup set to true if this is not a direct message (default is false)\r\n* Array containing the members to include in the conversation\r\n\r\nThe return value is a ResourceResponse which contains a conversation id which is suitable for use\r\nin the message payload and REST API uris.\r\n\r\nMost channels only support the semantics of bots initiating a direct message conversation.  An example of how to do that would be:\r\n\r\n```\r\nvar resource = await connector.conversations.CreateConversation(new ConversationParameters(){ Bot = bot, members = new ChannelAccount[] { new ChannelAccount(\"user1\") } );\r\nawait connect.Conversations.SendToConversationAsync(resource.Id, new Activity() ... ) ;\r\n\r\n```",
+          "operationId": "Conversations_CreateConversation",
+          "consumes": [
+            "application/json",
+            "text/json",
+            "application/xml",
+            "text/xml",
+            "application/x-www-form-urlencoded"
+          ],
+          "produces": [
+            "application/json",
+            "text/json",
+            "application/xml",
+            "text/xml"
+          ],
+          "parameters": [
+            {
+              "name": "parameters",
+              "in": "body",
+              "description": "Parameters to create the conversation from",
+              "required": true,
+              "schema": {
+                "$ref": "#/definitions/ConversationParameters"
+              }
             }
-          },
-          "301": {
-            "description": "The Location header describes where the content is now."
-          },
-          "302": {
-            "description": "The Location header describes where the content is now."
-          },
-          "default": {
-            "description": "The operation failed and the response is an error object describing the status code and failure.",
-            "schema": {
-              "$ref": "#/definitions/ErrorResponse"
+          ],
+          "responses": {
+            "200": {
+              "description": "An object will be returned containing \r\n* the ID for the conversation\r\n* ActivityId for the activity if provided.  If ActivityId is null then the channel doesn't support returning resource id's for activity.",
+              "schema": {
+                "$ref": "#/definitions/ConversationResourceResponse"
+              }
+            },
+            "201": {
+              "description": "An object will be returned containing \r\n* the ID for the conversation\r\n* ActivityId for the activity if provided.  If ActivityId is null then the channel doesn't support returning resource id's for activity.",
+              "schema": {
+                "$ref": "#/definitions/ConversationResourceResponse"
+              }
+            },
+            "202": {
+              "description": "An object will be returned containing \r\n* the ID for the conversation\r\n* ActivityId for the activity if provided.  If ActivityId is null then the channel doesn't support returning resource id's for activity.",
+              "schema": {
+                "$ref": "#/definitions/ConversationResourceResponse"
+              }
+            },
+            "default": {
+              "description": "The operation failed and the response is an error object describing the status code and failure.",
+              "schema": {
+                "$ref": "#/definitions/ErrorResponse"
+              }
             }
           }
         }
-      }
-    },
-    "/v3/conversations": {
-      "get": {
-        "tags": [
-          "Conversations"
-        ],
-        "summary": "GetConversations",
-        "description": "List the Conversations in which this bot has participated.\r\n\r\nGET from this method with a skip token\r\n\r\nThe return value is a ConversationsResult, which contains an array of ConversationMembers and a skip token.  If the skip token is not empty, then \r\nthere are further values to be returned. Call this method again with the returned token to get more values.\r\n\r\nEach ConversationMembers object contains the ID of the conversation and an array of ChannelAccounts that describe the members of the conversation.",
-        "operationId": "Conversations_GetConversations",
-        "consumes": [],
-        "produces": [
-          "application/json",
-          "text/json",
-          "application/xml",
-          "text/xml"
-        ],
-        "parameters": [
-          {
-            "name": "continuationToken",
-            "in": "query",
-            "description": "skip or continuation token",
-            "required": false,
-            "type": "string"
-          }
-        ],
-        "responses": {
-          "200": {
-            "description": "An object will be returned containing \r\n* an array (Conversations) of ConversationMembers objects\r\n* a continuation token\r\n\r\nEach ConversationMembers object contains:\r\n* the Id of the conversation\r\n* an array (Members) of ChannelAccount objects",
-            "schema": {
-              "$ref": "#/definitions/ConversationsResult"
+      },
+      "/v3/conversations/{conversationId}/activities": {
+        "post": {
+          "tags": [
+            "Conversations"
+          ],
+          "summary": "SendToConversation",
+          "description": "This method allows you to send an activity to the end of a conversation.\r\n\r\nThis is slightly different from ReplyToActivity().\r\n* SendToConversation(conversationId) - will append the activity to the end of the conversation according to the timestamp or semantics of the channel.\r\n* ReplyToActivity(conversationId,ActivityId) - adds the activity as a reply to another activity, if the channel supports it. If the channel does not support nested replies, ReplyToActivity falls back to SendToConversation.\r\n\r\nUse ReplyToActivity when replying to a specific activity in the conversation.\r\n\r\nUse SendToConversation in all other cases.",
+          "operationId": "Conversations_SendToConversation",
+          "consumes": [
+            "application/json",
+            "text/json",
+            "application/xml",
+            "text/xml",
+            "application/x-www-form-urlencoded"
+          ],
+          "produces": [
+            "application/json",
+            "text/json"
+          ],
+          "parameters": [
+            {
+              "name": "conversationId",
+              "in": "path",
+              "description": "Conversation ID",
+              "required": true,
+              "type": "string"
+            },
+            {
+              "name": "activity",
+              "in": "body",
+              "description": "Activity to send",
+              "required": true,
+              "schema": {
+                "$ref": "#/definitions/Activity"
+              }
             }
-          },
-          "default": {
-            "description": "The operation failed and the response is an error object describing the status code and failure.",
-            "schema": {
-              "$ref": "#/definitions/ErrorResponse"
+          ],
+          "responses": {
+            "200": {
+              "description": "An object will be returned containing the ID for the resource.",
+              "schema": {
+                "$ref": "#/definitions/ResourceResponse"
+              }
+            },
+            "201": {
+              "description": "A ResourceResponse object will be returned containing the ID for the resource.",
+              "schema": {
+                "$ref": "#/definitions/ResourceResponse"
+              }
+            },
+            "202": {
+              "description": "An object will be returned containing the ID for the resource.",
+              "schema": {
+                "$ref": "#/definitions/ResourceResponse"
+              }
+            },
+            "default": {
+              "description": "The operation failed and the response is an error object describing the status code and failure.",
+              "schema": {
+                "$ref": "#/definitions/ErrorResponse"
+              }
             }
           }
         }
       },
-      "post": {
-        "tags": [
-          "Conversations"
-        ],
-        "summary": "CreateConversation",
-        "description": "Create a new Conversation.\r\n\r\nPOST to this method with a\r\n* Bot being the bot creating the conversation\r\n* IsGroup set to true if this is not a direct message (default is false)\r\n* Array containing the members to include in the conversation\r\n\r\nThe return value is a ResourceResponse which contains a conversation id which is suitable for use\r\nin the message payload and REST API uris.\r\n\r\nMost channels only support the semantics of bots initiating a direct message conversation.  An example of how to do that would be:\r\n\r\n```\r\nvar resource = await connector.conversations.CreateConversation(new ConversationParameters(){ Bot = bot, members = new ChannelAccount[] { new ChannelAccount(\"user1\") } );\r\nawait connect.Conversations.SendToConversationAsync(resource.Id, new Activity() ... ) ;\r\n\r\n```",
-        "operationId": "Conversations_CreateConversation",
-        "consumes": [
-          "application/json",
-          "text/json",
-          "application/xml",
-          "text/xml",
-          "application/x-www-form-urlencoded"
-        ],
-        "produces": [
-          "application/json",
-          "text/json",
-          "application/xml",
-          "text/xml"
-        ],
-        "parameters": [
-          {
-            "name": "parameters",
-            "in": "body",
-            "description": "Parameters to create the conversation from",
-            "required": true,
-            "schema": {
-              "$ref": "#/definitions/ConversationParameters"
+      "/v3/conversations/{conversationId}/activities/history": {
+        "post": {
+          "tags": [
+            "Conversations"
+          ],
+          "summary": "SendConversationHistory",
+          "description": "This method allows you to upload the historic activities to the conversation.\r\n\r\nSender must ensure that the historic activities have unique ids and appropriate timestamps. The ids are used by the client to deal with duplicate activities and the timestamps are used by the client to render the activities in the right order.",
+          "operationId": "Conversations_SendConversationHistory",
+          "consumes": [
+            "application/json",
+            "text/json",
+            "application/xml",
+            "text/xml",
+            "application/x-www-form-urlencoded"
+          ],
+          "produces": [
+            "application/json",
+            "text/json"
+          ],
+          "parameters": [
+            {
+              "name": "conversationId",
+              "in": "path",
+              "description": "Conversation ID",
+              "required": true,
+              "type": "string"
+            },
+            {
+              "name": "history",
+              "in": "body",
+              "description": "Historic activities",
+              "required": true,
+              "schema": {
+                "$ref": "#/definitions/Transcript"
+              }
+            }
+          ],
+          "responses": {
+            "200": {
+              "description": "An object will be returned containing the ID for the resource.",
+              "schema": {
+                "$ref": "#/definitions/ResourceResponse"
+              }
+            },
+            "201": {
+              "description": "A ResourceResponse object will be returned containing the ID for the resource.",
+              "schema": {
+                "$ref": "#/definitions/ResourceResponse"
+              }
+            },
+            "202": {
+              "description": "An object will be returned containing the ID for the resource.",
+              "schema": {
+                "$ref": "#/definitions/ResourceResponse"
+              }
+            },
+            "default": {
+              "description": "The operation failed and the response is an error object describing the status code and failure.",
+              "schema": {
+                "$ref": "#/definitions/ErrorResponse"
+              }
             }
           }
-        ],
-        "responses": {
-          "200": {
-            "description": "An object will be returned containing \r\n* the ID for the conversation\r\n* ActivityId for the activity if provided.  If ActivityId is null then the channel doesn't support returning resource id's for activity.",
-            "schema": {
-              "$ref": "#/definitions/ConversationResourceResponse"
+        }
+      },
+      "/v3/conversations/{conversationId}/activities/{activityId}": {
+        "put": {
+          "tags": [
+            "Conversations"
+          ],
+          "summary": "UpdateActivity",
+          "description": "Edit an existing activity.\r\n\r\nSome channels allow you to edit an existing activity to reflect the new state of a bot conversation.\r\n\r\nFor example, you can remove buttons after someone has clicked \"Approve\" button.",
+          "operationId": "Conversations_UpdateActivity",
+          "consumes": [
+            "application/json",
+            "text/json",
+            "application/xml",
+            "text/xml",
+            "application/x-www-form-urlencoded"
+          ],
+          "produces": [
+            "application/json",
+            "text/json"
+          ],
+          "parameters": [
+            {
+              "name": "conversationId",
+              "in": "path",
+              "description": "Conversation ID",
+              "required": true,
+              "type": "string"
+            },
+            {
+              "name": "activityId",
+              "in": "path",
+              "description": "activityId to update",
+              "required": true,
+              "type": "string"
+            },
+            {
+              "name": "activity",
+              "in": "body",
+              "description": "replacement Activity",
+              "required": true,
+              "schema": {
+                "$ref": "#/definitions/Activity"
+              }
             }
-          },
-          "201": {
-            "description": "An object will be returned containing \r\n* the ID for the conversation\r\n* ActivityId for the activity if provided.  If ActivityId is null then the channel doesn't support returning resource id's for activity.",
-            "schema": {
-              "$ref": "#/definitions/ConversationResourceResponse"
+          ],
+          "responses": {
+            "200": {
+              "description": "An object will be returned containing the ID for the resource.",
+              "schema": {
+                "$ref": "#/definitions/ResourceResponse"
+              }
+            },
+            "201": {
+              "description": "A ResourceResponse object will be returned containing the ID for the resource.",
+              "schema": {
+                "$ref": "#/definitions/ResourceResponse"
+              }
+            },
+            "202": {
+              "description": "An object will be returned containing the ID for the resource.",
+              "schema": {
+                "$ref": "#/definitions/ResourceResponse"
+              }
+            },
+            "default": {
+              "description": "The operation failed and the response is an error object describing the status code and failure.",
+              "schema": {
+                "$ref": "#/definitions/ErrorResponse"
+              }
             }
-          },
-          "202": {
-            "description": "An object will be returned containing \r\n* the ID for the conversation\r\n* ActivityId for the activity if provided.  If ActivityId is null then the channel doesn't support returning resource id's for activity.",
-            "schema": {
-              "$ref": "#/definitions/ConversationResourceResponse"
+          }
+        },
+        "post": {
+          "tags": [
+            "Conversations"
+          ],
+          "summary": "ReplyToActivity",
+          "description": "This method allows you to reply to an activity.\r\n\r\nThis is slightly different from SendToConversation().\r\n* SendToConversation(conversationId) - will append the activity to the end of the conversation according to the timestamp or semantics of the channel.\r\n* ReplyToActivity(conversationId,ActivityId) - adds the activity as a reply to another activity, if the channel supports it. If the channel does not support nested replies, ReplyToActivity falls back to SendToConversation.\r\n\r\nUse ReplyToActivity when replying to a specific activity in the conversation.\r\n\r\nUse SendToConversation in all other cases.",
+          "operationId": "Conversations_ReplyToActivity",
+          "consumes": [
+            "application/json",
+            "text/json",
+            "application/xml",
+            "text/xml",
+            "application/x-www-form-urlencoded"
+          ],
+          "produces": [
+            "application/json",
+            "text/json"
+          ],
+          "parameters": [
+            {
+              "name": "conversationId",
+              "in": "path",
+              "description": "Conversation ID",
+              "required": true,
+              "type": "string"
+            },
+            {
+              "name": "activityId",
+              "in": "path",
+              "description": "activityId the reply is to (OPTIONAL)",
+              "required": true,
+              "type": "string"
+            },
+            {
+              "name": "activity",
+              "in": "body",
+              "description": "Activity to send",
+              "required": true,
+              "schema": {
+                "$ref": "#/definitions/Activity"
+              }
             }
-          },
-          "default": {
-            "description": "The operation failed and the response is an error object describing the status code and failure.",
-            "schema": {
-              "$ref": "#/definitions/ErrorResponse"
+          ],
+          "responses": {
+            "200": {
+              "description": "An object will be returned containing the ID for the resource.",
+              "schema": {
+                "$ref": "#/definitions/ResourceResponse"
+              }
+            },
+            "201": {
+              "description": "A ResourceResponse object will be returned containing the ID for the resource.",
+              "schema": {
+                "$ref": "#/definitions/ResourceResponse"
+              }
+            },
+            "202": {
+              "description": "An object will be returned containing the ID for the resource.",
+              "schema": {
+                "$ref": "#/definitions/ResourceResponse"
+              }
+            },
+            "default": {
+              "description": "The operation failed and the response is an error object describing the status code and failure.",
+              "schema": {
+                "$ref": "#/definitions/ErrorResponse"
+              }
             }
           }
-        }
-      }
-    },
-    "/v3/conversations/{conversationId}/activities": {
-      "post": {
-        "tags": [
-          "Conversations"
-        ],
-        "summary": "SendToConversation",
-        "description": "This method allows you to send an activity to the end of a conversation.\r\n\r\nThis is slightly different from ReplyToActivity().\r\n* SendToConversation(conversationId) - will append the activity to the end of the conversation according to the timestamp or semantics of the channel.\r\n* ReplyToActivity(conversationId,ActivityId) - adds the activity as a reply to another activity, if the channel supports it. If the channel does not support nested replies, ReplyToActivity falls back to SendToConversation.\r\n\r\nUse ReplyToActivity when replying to a specific activity in the conversation.\r\n\r\nUse SendToConversation in all other cases.",
-        "operationId": "Conversations_SendToConversation",
-        "consumes": [
-          "application/json",
-          "text/json",
-          "application/xml",
-          "text/xml",
-          "application/x-www-form-urlencoded"
-        ],
-        "produces": [
-          "application/json",
-          "text/json"
-        ],
-        "parameters": [
-          {
-            "name": "conversationId",
-            "in": "path",
-            "description": "Conversation ID",
-            "required": true,
-            "type": "string"
-          },
-          {
-            "name": "activity",
-            "in": "body",
-            "description": "Activity to send",
-            "required": true,
-            "schema": {
-              "$ref": "#/definitions/Activity"
+        },
+        "delete": {
+          "tags": [
+            "Conversations"
+          ],
+          "summary": "DeleteActivity",
+          "description": "Delete an existing activity.\r\n\r\nSome channels allow you to delete an existing activity, and if successful this method will remove the specified activity.",
+          "operationId": "Conversations_DeleteActivity",
+          "consumes": [],
+          "produces": [
+            "application/json",
+            "text/json",
+            "application/xml",
+            "text/xml"
+          ],
+          "parameters": [
+            {
+              "name": "conversationId",
+              "in": "path",
+              "description": "Conversation ID",
+              "required": true,
+              "type": "string"
+            },
+            {
+              "name": "activityId",
+              "in": "path",
+              "description": "activityId to delete",
+              "required": true,
+              "type": "string"
+            }
+          ],
+          "responses": {
+            "200": {
+              "description": "The operation succeeded, there is no response."
+            },
+            "202": {
+              "description": "The request has been accepted for processing, but the processing has not been completed"
+            },
+            "default": {
+              "description": "The operation failed and the response is an error object describing the status code and failure.",
+              "schema": {
+                "$ref": "#/definitions/ErrorResponse"
+              }
             }
           }
-        ],
-        "responses": {
-          "200": {
-            "description": "An object will be returned containing the ID for the resource.",
-            "schema": {
-              "$ref": "#/definitions/ResourceResponse"
+        }
+      },
+      "/v3/conversations/{conversationId}/members": {
+        "get": {
+          "tags": [
+            "Conversations"
+          ],
+          "summary": "GetConversationMembers",
+          "description": "Enumerate the members of a conversation. \r\n\r\nThis REST API takes a ConversationId and returns an array of ChannelAccount objects representing the members of the conversation.",
+          "operationId": "Conversations_GetConversationMembers",
+          "consumes": [],
+          "produces": [
+            "application/json",
+            "text/json",
+            "application/xml",
+            "text/xml"
+          ],
+          "parameters": [
+            {
+              "name": "conversationId",
+              "in": "path",
+              "description": "Conversation ID",
+              "required": true,
+              "type": "string"
             }
-          },
-          "201": {
-            "description": "A ResourceResponse object will be returned containing the ID for the resource.",
-            "schema": {
-              "$ref": "#/definitions/ResourceResponse"
+          ],
+          "responses": {
+            "200": {
+              "description": "An array of ChannelAccount objects",
+              "schema": {
+                "type": "array",
+                "items": {
+                  "$ref": "#/definitions/ChannelAccount"
+                }
+              }
+            },
+            "default": {
+              "description": "The operation failed and the response is an error object describing the status code and failure.",
+              "schema": {
+                "$ref": "#/definitions/ErrorResponse"
+              }
             }
-          },
-          "202": {
-            "description": "An object will be returned containing the ID for the resource.",
-            "schema": {
-              "$ref": "#/definitions/ResourceResponse"
+          }
+        }
+      },
+      "/v3/conversations/{conversationId}/pagedmembers": {
+        "get": {
+          "tags": [
+            "Conversations"
+          ],
+          "summary": "GetConversationPagedMembers",
+          "description": "Enumerate the members of a conversation one page at a time.\r\n\r\nThis REST API takes a ConversationId. Optionally a pageSize and/or continuationToken can be provided. It returns a PagedMembersResult, which contains an array\r\nof ChannelAccounts representing the members of the conversation and a continuation token that can be used to get more values.\r\n\r\nOne page of ChannelAccounts records are returned with each call. The number of records in a page may vary between channels and calls. The pageSize parameter can be used as \r\na suggestion. If there are no additional results the response will not contain a continuation token. If there are no members in the conversation the Members will be empty or not present in the response.\r\n\r\nA response to a request that has a continuation token from a prior request may rarely return members from a previous request.",
+          "operationId": "Conversations_GetConversationPagedMembers",
+          "consumes": [],
+          "produces": [
+            "application/json",
+            "text/json",
+            "application/xml",
+            "text/xml"
+          ],
+          "parameters": [
+            {
+              "name": "conversationId",
+              "in": "path",
+              "description": "Conversation ID",
+              "required": true,
+              "type": "string"
+            },
+            {
+              "name": "pageSize",
+              "in": "query",
+              "description": "Suggested page size",
+              "required": false,
+              "type": "integer",
+              "format": "int32"
+            },
+            {
+              "name": "continuationToken",
+              "in": "query",
+              "description": "Continuation Token",
+              "required": false,
+              "type": "string"
             }
-          },
-          "default": {
-            "description": "The operation failed and the response is an error object describing the status code and failure.",
-            "schema": {
-              "$ref": "#/definitions/ErrorResponse"
+          ],
+          "responses": {
+            "200": {
+              "description": "OK",
+              "schema": {
+                "$ref": "#/definitions/PagedMembersResult"
+              }
             }
           }
         }
-      }
-    },
-    "/v3/conversations/{conversationId}/activities/history": {
-      "post": {
-        "tags": [
-          "Conversations"
-        ],
-        "summary": "SendConversationHistory",
-        "description": "This method allows you to upload the historic activities to the conversation.\r\n\r\nSender must ensure that the historic activities have unique ids and appropriate timestamps. The ids are used by the client to deal with duplicate activities and the timestamps are used by the client to render the activities in the right order.",
-        "operationId": "Conversations_SendConversationHistory",
-        "consumes": [
-          "application/json",
-          "text/json",
-          "application/xml",
-          "text/xml",
-          "application/x-www-form-urlencoded"
-        ],
-        "produces": [
-          "application/json",
-          "text/json"
-        ],
-        "parameters": [
-          {
-            "name": "conversationId",
-            "in": "path",
-            "description": "Conversation ID",
-            "required": true,
-            "type": "string"
-          },
-          {
-            "name": "history",
-            "in": "body",
-            "description": "Historic activities",
-            "required": true,
-            "schema": {
-              "$ref": "#/definitions/Transcript"
+      },
+      "/v3/conversations/{conversationId}/members/{memberId}": {
+        "delete": {
+          "tags": [
+            "Conversations"
+          ],
+          "summary": "DeleteConversationMember",
+          "description": "Deletes a member from a conversation. \r\n\r\nThis REST API takes a ConversationId and a memberId (of type string) and removes that member from the conversation. If that member was the last member\r\nof the conversation, the conversation will also be deleted.",
+          "operationId": "Conversations_DeleteConversationMember",
+          "consumes": [],
+          "produces": [
+            "application/json",
+            "text/json",
+            "application/xml",
+            "text/xml"
+          ],
+          "parameters": [
+            {
+              "name": "conversationId",
+              "in": "path",
+              "description": "Conversation ID",
+              "required": true,
+              "type": "string"
+            },
+            {
+              "name": "memberId",
+              "in": "path",
+              "description": "ID of the member to delete from this conversation",
+              "required": true,
+              "type": "string"
+            }
+          ],
+          "responses": {
+            "200": {
+              "description": "The operation succeeded, there is no response."
+            },
+            "204": {
+              "description": "The operation succeeded but no content was returned."
+            },
+            "default": {
+              "description": "The operation failed and the response is an error object describing the status code and failure.",
+              "schema": {
+                "$ref": "#/definitions/ErrorResponse"
+              }
             }
           }
-        ],
-        "responses": {
-          "200": {
-            "description": "An object will be returned containing the ID for the resource.",
-            "schema": {
-              "$ref": "#/definitions/ResourceResponse"
+        }
+      },
+      "/v3/conversations/{conversationId}/activities/{activityId}/members": {
+        "get": {
+          "tags": [
+            "Conversations"
+          ],
+          "summary": "GetActivityMembers",
+          "description": "Enumerate the members of an activity. \r\n\r\nThis REST API takes a ConversationId and a ActivityId, returning an array of ChannelAccount objects representing the members of the particular activity in the conversation.",
+          "operationId": "Conversations_GetActivityMembers",
+          "consumes": [],
+          "produces": [
+            "application/json",
+            "text/json",
+            "application/xml",
+            "text/xml"
+          ],
+          "parameters": [
+            {
+              "name": "conversationId",
+              "in": "path",
+              "description": "Conversation ID",
+              "required": true,
+              "type": "string"
+            },
+            {
+              "name": "activityId",
+              "in": "path",
+              "description": "Activity ID",
+              "required": true,
+              "type": "string"
             }
-          },
-          "201": {
-            "description": "A ResourceResponse object will be returned containing the ID for the resource.",
-            "schema": {
-              "$ref": "#/definitions/ResourceResponse"
+          ],
+          "responses": {
+            "200": {
+              "description": "An array of ChannelAccount objects",
+              "schema": {
+                "type": "array",
+                "items": {
+                  "$ref": "#/definitions/ChannelAccount"
+                }
+              }
+            },
+            "default": {
+              "description": "The operation failed and the response is an error object describing the status code and failure.",
+              "schema": {
+                "$ref": "#/definitions/ErrorResponse"
+              }
             }
-          },
-          "202": {
-            "description": "An object will be returned containing the ID for the resource.",
-            "schema": {
-              "$ref": "#/definitions/ResourceResponse"
+          }
+        }
+      },
+      "/v3/conversations/{conversationId}/attachments": {
+        "post": {
+          "tags": [
+            "Conversations"
+          ],
+          "summary": "UploadAttachment",
+          "description": "Upload an attachment directly into a channel's blob storage.\r\n\r\nThis is useful because it allows you to store data in a compliant store when dealing with enterprises.\r\n\r\nThe response is a ResourceResponse which contains an AttachmentId which is suitable for using with the attachments API.",
+          "operationId": "Conversations_UploadAttachment",
+          "consumes": [
+            "application/json",
+            "text/json",
+            "application/xml",
+            "text/xml",
+            "application/x-www-form-urlencoded"
+          ],
+          "produces": [
+            "application/json",
+            "text/json"
+          ],
+          "parameters": [
+            {
+              "name": "conversationId",
+              "in": "path",
+              "description": "Conversation ID",
+              "required": true,
+              "type": "string"
+            },
+            {
+              "name": "attachmentUpload",
+              "in": "body",
+              "description": "Attachment data",
+              "required": true,
+              "schema": {
+                "$ref": "#/definitions/AttachmentData"
+              }
             }
-          },
-          "default": {
-            "description": "The operation failed and the response is an error object describing the status code and failure.",
-            "schema": {
-              "$ref": "#/definitions/ErrorResponse"
+          ],
+          "responses": {
+            "200": {
+              "description": "An object will be returned containing the ID for the resource.",
+              "schema": {
+                "$ref": "#/definitions/ResourceResponse"
+              }
+            },
+            "201": {
+              "description": "A ResourceResponse object will be returned containing the ID for the resource.",
+              "schema": {
+                "$ref": "#/definitions/ResourceResponse"
+              }
+            },
+            "202": {
+              "description": "An object will be returned containing the ID for the resource.",
+              "schema": {
+                "$ref": "#/definitions/ResourceResponse"
+              }
+            },
+            "default": {
+              "description": "The operation failed and the response is an error object describing the status code and failure.",
+              "schema": {
+                "$ref": "#/definitions/ErrorResponse"
+              }
             }
           }
         }
       }
     },
-    "/v3/conversations/{conversationId}/activities/{activityId}": {
-      "put": {
-        "tags": [
-          "Conversations"
-        ],
-        "summary": "UpdateActivity",
-        "description": "Edit an existing activity.\r\n\r\nSome channels allow you to edit an existing activity to reflect the new state of a bot conversation.\r\n\r\nFor example, you can remove buttons after someone has clicked \"Approve\" button.",
-        "operationId": "Conversations_UpdateActivity",
-        "consumes": [
-          "application/json",
-          "text/json",
-          "application/xml",
-          "text/xml",
-          "application/x-www-form-urlencoded"
-        ],
-        "produces": [
-          "application/json",
-          "text/json"
-        ],
-        "parameters": [
-          {
-            "name": "conversationId",
-            "in": "path",
-            "description": "Conversation ID",
-            "required": true,
+    "definitions": {
+      "AttachmentInfo": {
+        "description": "Metadata for an attachment",
+        "type": "object",
+        "properties": {
+          "name": {
+            "description": "Name of the attachment",
             "type": "string"
           },
-          {
-            "name": "activityId",
-            "in": "path",
-            "description": "activityId to update",
-            "required": true,
+          "type": {
+            "description": "ContentType of the attachment",
             "type": "string"
           },
-          {
-            "name": "activity",
-            "in": "body",
-            "description": "replacement Activity",
-            "required": true,
-            "schema": {
-              "$ref": "#/definitions/Activity"
+          "views": {
+            "description": "attachment views",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/AttachmentView"
             }
           }
-        ],
-        "responses": {
-          "200": {
-            "description": "An object will be returned containing the ID for the resource.",
-            "schema": {
-              "$ref": "#/definitions/ResourceResponse"
-            }
-          },
-          "201": {
-            "description": "A ResourceResponse object will be returned containing the ID for the resource.",
-            "schema": {
-              "$ref": "#/definitions/ResourceResponse"
-            }
-          },
-          "202": {
-            "description": "An object will be returned containing the ID for the resource.",
-            "schema": {
-              "$ref": "#/definitions/ResourceResponse"
-            }
+        }
+      },
+      "AttachmentView": {
+        "description": "Attachment View name and size",
+        "type": "object",
+        "properties": {
+          "viewId": {
+            "description": "Id of the attachment",
+            "type": "string"
           },
-          "default": {
-            "description": "The operation failed and the response is an error object describing the status code and failure.",
-            "schema": {
-              "$ref": "#/definitions/ErrorResponse"
-            }
+          "size": {
+            "format": "int32",
+            "description": "Size of the attachment",
+            "type": "integer"
           }
         }
       },
-      "post": {
-        "tags": [
-          "Conversations"
-        ],
-        "summary": "ReplyToActivity",
-        "description": "This method allows you to reply to an activity.\r\n\r\nThis is slightly different from SendToConversation().\r\n* SendToConversation(conversationId) - will append the activity to the end of the conversation according to the timestamp or semantics of the channel.\r\n* ReplyToActivity(conversationId,ActivityId) - adds the activity as a reply to another activity, if the channel supports it. If the channel does not support nested replies, ReplyToActivity falls back to SendToConversation.\r\n\r\nUse ReplyToActivity when replying to a specific activity in the conversation.\r\n\r\nUse SendToConversation in all other cases.",
-        "operationId": "Conversations_ReplyToActivity",
-        "consumes": [
-          "application/json",
-          "text/json",
-          "application/xml",
-          "text/xml",
-          "application/x-www-form-urlencoded"
-        ],
-        "produces": [
-          "application/json",
-          "text/json"
-        ],
-        "parameters": [
-          {
-            "name": "conversationId",
-            "in": "path",
-            "description": "Conversation ID",
-            "required": true,
+      "ErrorResponse": {
+        "description": "An HTTP API response",
+        "type": "object",
+        "properties": {
+          "error": {
+            "$ref": "#/definitions/Error",
+            "description": "Error message"
+          }
+        }
+      },
+      "Error": {
+        "description": "Object representing error information",
+        "type": "object",
+        "properties": {
+          "code": {
+            "description": "Error code",
             "type": "string"
           },
-          {
-            "name": "activityId",
-            "in": "path",
-            "description": "activityId the reply is to (OPTIONAL)",
-            "required": true,
+          "message": {
+            "description": "Error message",
             "type": "string"
           },
-          {
-            "name": "activity",
-            "in": "body",
-            "description": "Activity to send",
-            "required": true,
-            "schema": {
-              "$ref": "#/definitions/Activity"
-            }
+          "innerHttpError": {
+            "$ref": "#/definitions/InnerHttpError",
+            "description": "Error from inner http call"
           }
-        ],
-        "responses": {
-          "200": {
-            "description": "An object will be returned containing the ID for the resource.",
-            "schema": {
-              "$ref": "#/definitions/ResourceResponse"
-            }
-          },
-          "201": {
-            "description": "A ResourceResponse object will be returned containing the ID for the resource.",
-            "schema": {
-              "$ref": "#/definitions/ResourceResponse"
-            }
-          },
-          "202": {
-            "description": "An object will be returned containing the ID for the resource.",
-            "schema": {
-              "$ref": "#/definitions/ResourceResponse"
-            }
+        }
+      },
+      "InnerHttpError": {
+        "description": "Object representing inner http error",
+        "type": "object",
+        "properties": {
+          "statusCode": {
+            "format": "int32",
+            "description": "HttpStatusCode from failed request",
+            "type": "integer"
           },
-          "default": {
-            "description": "The operation failed and the response is an error object describing the status code and failure.",
-            "schema": {
-              "$ref": "#/definitions/ErrorResponse"
-            }
+          "body": {
+            "description": "Body from failed request",
+            "type": "object"
           }
         }
       },
-      "delete": {
-        "tags": [
-          "Conversations"
-        ],
-        "summary": "DeleteActivity",
-        "description": "Delete an existing activity.\r\n\r\nSome channels allow you to delete an existing activity, and if successful this method will remove the specified activity.",
-        "operationId": "Conversations_DeleteActivity",
-        "consumes": [],
-        "produces": [
-          "application/json",
-          "text/json",
-          "application/xml",
-          "text/xml"
-        ],
-        "parameters": [
-          {
-            "name": "conversationId",
-            "in": "path",
-            "description": "Conversation ID",
-            "required": true,
+      "ConversationParameters": {
+        "description": "Parameters for creating a new conversation",
+        "type": "object",
+        "properties": {
+          "isGroup": {
+            "description": "IsGroup",
+            "type": "boolean"
+          },
+          "bot": {
+            "$ref": "#/definitions/ChannelAccount",
+            "description": "The bot address for this conversation"
+          },
+          "members": {
+            "description": "Members to add to the conversation",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/ChannelAccount"
+            }
+          },
+          "topicName": {
+            "description": "(Optional) Topic of the conversation (if supported by the channel)",
             "type": "string"
           },
-          {
-            "name": "activityId",
-            "in": "path",
-            "description": "activityId to delete",
-            "required": true,
+          "tenantId": {
+            "description": "(Optional) The tenant ID in which the conversation should be created",
             "type": "string"
-          }
-        ],
-        "responses": {
-          "200": {
-            "description": "The operation succeeded, there is no response."
           },
-          "202": {
-            "description": "The request has been accepted for processing, but the processing has not been completed"
+          "activity": {
+            "$ref": "#/definitions/Activity",
+            "description": "(Optional) When creating a new conversation, use this activity as the initial message to the conversation"
           },
-          "default": {
-            "description": "The operation failed and the response is an error object describing the status code and failure.",
-            "schema": {
-              "$ref": "#/definitions/ErrorResponse"
-            }
+          "channelData": {
+            "description": "Channel specific payload for creating the conversation",
+            "type": "object"
           }
         }
-      }
-    },
-    "/v3/conversations/{conversationId}/members": {
-      "get": {
-        "tags": [
-          "Conversations"
-        ],
-        "summary": "GetConversationMembers",
-        "description": "Enumerate the members of a conversation. \r\n\r\nThis REST API takes a ConversationId and returns an array of ChannelAccount objects representing the members of the conversation.",
-        "operationId": "Conversations_GetConversationMembers",
-        "consumes": [],
-        "produces": [
-          "application/json",
-          "text/json",
-          "application/xml",
-          "text/xml"
-        ],
-        "parameters": [
-          {
-            "name": "conversationId",
-            "in": "path",
-            "description": "Conversation ID",
-            "required": true,
+      },
+      "ChannelAccount": {
+        "description": "Channel account information needed to route a message",
+        "type": "object",
+        "properties": {
+          "id": {
+            "description": "Channel id for the user or bot on this channel (Example: joe@smith.com, or @joesmith or 123456)",
             "type": "string"
-          }
-        ],
-        "responses": {
-          "200": {
-            "description": "An array of ChannelAccount objects",
-            "schema": {
-              "type": "array",
-              "items": {
-                "$ref": "#/definitions/ChannelAccount"
-              }
-            }
           },
-          "default": {
-            "description": "The operation failed and the response is an error object describing the status code and failure.",
-            "schema": {
-              "$ref": "#/definitions/ErrorResponse"
-            }
-          }
-        }
-      }
-    },
-    "/v3/conversations/{conversationId}/pagedmembers": {
-      "get": {
-        "tags": [
-          "Conversations"
-        ],
-        "summary": "GetConversationPagedMembers",
-        "description": "Enumerate the members of a conversation one page at a time.\r\n\r\nThis REST API takes a ConversationId. Optionally a pageSize and/or continuationToken can be provided. It returns a PagedMembersResult, which contains an array\r\nof ChannelAccounts representing the members of the conversation and a continuation token that can be used to get more values.\r\n\r\nOne page of ChannelAccounts records are returned with each call. The number of records in a page may vary between channels and calls. The pageSize parameter can be used as \r\na suggestion. If there are no additional results the response will not contain a continuation token. If there are no members in the conversation the Members will be empty or not present in the response.\r\n\r\nA response to a request that has a continuation token from a prior request may rarely return members from a previous request.",
-        "operationId": "Conversations_GetConversationPagedMembers",
-        "consumes": [],
-        "produces": [
-          "application/json",
-          "text/json",
-          "application/xml",
-          "text/xml"
-        ],
-        "parameters": [
-          {
-            "name": "conversationId",
-            "in": "path",
-            "description": "Conversation ID",
-            "required": true,
+          "name": {
+            "description": "Display friendly name",
             "type": "string"
           },
-          {
-            "name": "pageSize",
-            "in": "query",
-            "description": "Suggested page size",
-            "required": false,
-            "type": "integer",
-            "format": "int32"
-          },
-          {
-            "name": "continuationToken",
-            "in": "query",
-            "description": "Continuation Token",
-            "required": false,
+          "aadObjectId": {
+            "description": "This account's object ID within Azure Active Directory (AAD)",
             "type": "string"
-          }
-        ],
-        "responses": {
-          "200": {
-            "description": "OK",
-            "schema": {
-              "$ref": "#/definitions/PagedMembersResult"
-            }
+          },
+          "role": {
+            "$ref": "#/definitions/RoleTypes",
+            "description": "Role of the entity behind the account (Example: User, Bot, Skill, etc.)"
           }
         }
-      }
-    },
-    "/v3/conversations/{conversationId}/members/{memberId}": {
-      "delete": {
-        "tags": [
-          "Conversations"
-        ],
-        "summary": "DeleteConversationMember",
-        "description": "Deletes a member from a conversation. \r\n\r\nThis REST API takes a ConversationId and a memberId (of type string) and removes that member from the conversation. If that member was the last member\r\nof the conversation, the conversation will also be deleted.",
-        "operationId": "Conversations_DeleteConversationMember",
-        "consumes": [],
-        "produces": [
-          "application/json",
-          "text/json",
-          "application/xml",
-          "text/xml"
-        ],
-        "parameters": [
-          {
-            "name": "conversationId",
-            "in": "path",
-            "description": "Conversation ID",
-            "required": true,
+      },
+      "Activity": {
+        "description": "An Activity is the basic communication type for the Bot Framework 3.0 protocol.",
+        "type": "object",
+        "properties": {
+          "type": {
+            "$ref": "#/definitions/ActivityTypes",
+            "description": "Contains the activity type."
+          },
+          "id": {
+            "description": "Contains an ID that uniquely identifies the activity on the channel.",
             "type": "string"
           },
-          {
-            "name": "memberId",
-            "in": "path",
-            "description": "ID of the member to delete from this conversation",
-            "required": true,
+          "timestamp": {
+            "format": "date-time",
+            "description": "Contains the date and time that the message was sent, in UTC, expressed in ISO-8601 format.",
             "type": "string"
-          }
-        ],
-        "responses": {
-          "200": {
-            "description": "The operation succeeded, there is no response."
           },
-          "204": {
-            "description": "The operation succeeded but no content was returned."
+          "localTimestamp": {
+            "format": "date-time",
+            "description": "Contains the local date and time of the message, expressed in ISO-8601 format.\r\nFor example, 2016-09-23T13:07:49.4714686-07:00.",
+            "type": "string"
           },
-          "default": {
-            "description": "The operation failed and the response is an error object describing the status code and failure.",
-            "schema": {
-              "$ref": "#/definitions/ErrorResponse"
-            }
-          }
-        }
-      }
-    },
-    "/v3/conversations/{conversationId}/activities/{activityId}/members": {
-      "get": {
-        "tags": [
-          "Conversations"
-        ],
-        "summary": "GetActivityMembers",
-        "description": "Enumerate the members of an activity. \r\n\r\nThis REST API takes a ConversationId and a ActivityId, returning an array of ChannelAccount objects representing the members of the particular activity in the conversation.",
-        "operationId": "Conversations_GetActivityMembers",
-        "consumes": [],
-        "produces": [
-          "application/json",
-          "text/json",
-          "application/xml",
-          "text/xml"
-        ],
-        "parameters": [
-          {
-            "name": "conversationId",
-            "in": "path",
-            "description": "Conversation ID",
-            "required": true,
+          "localTimezone": {
+            "description": "Contains the name of the local timezone of the message, expressed in IANA Time Zone database format.\r\nFor example, America/Los_Angeles.",
             "type": "string"
           },
-          {
-            "name": "activityId",
-            "in": "path",
-            "description": "Activity ID",
-            "required": true,
+          "callerId": {
+            "description": "A string containing an IRI identifying the caller of a bot. This field is not intended to be transmitted\r\nover the wire, but is instead populated by bots and clients based on cryptographically verifiable data\r\nthat asserts the identity of the callers (e.g. tokens).",
             "type": "string"
-          }
-        ],
-        "responses": {
-          "200": {
-            "description": "An array of ChannelAccount objects",
-            "schema": {
-              "type": "array",
-              "items": {
-                "$ref": "#/definitions/ChannelAccount"
-              }
-            }
           },
-          "default": {
-            "description": "The operation failed and the response is an error object describing the status code and failure.",
-            "schema": {
-              "$ref": "#/definitions/ErrorResponse"
-            }
-          }
-        }
-      }
-    },
-    "/v3/conversations/{conversationId}/attachments": {
-      "post": {
-        "tags": [
-          "Conversations"
-        ],
-        "summary": "UploadAttachment",
-        "description": "Upload an attachment directly into a channel's blob storage.\r\n\r\nThis is useful because it allows you to store data in a compliant store when dealing with enterprises.\r\n\r\nThe response is a ResourceResponse which contains an AttachmentId which is suitable for using with the attachments API.",
-        "operationId": "Conversations_UploadAttachment",
-        "consumes": [
-          "application/json",
-          "text/json",
-          "application/xml",
-          "text/xml",
-          "application/x-www-form-urlencoded"
-        ],
-        "produces": [
-          "application/json",
-          "text/json"
-        ],
-        "parameters": [
-          {
-            "name": "conversationId",
-            "in": "path",
-            "description": "Conversation ID",
-            "required": true,
+          "serviceUrl": {
+            "description": "Contains the URL that specifies the channel's service endpoint. Set by the channel.",
+            "type": "string"
+          },
+          "channelId": {
+            "description": "Contains an ID that uniquely identifies the channel. Set by the channel.",
             "type": "string"
           },
-          {
-            "name": "attachmentUpload",
-            "in": "body",
-            "description": "Attachment data",
-            "required": true,
-            "schema": {
-              "$ref": "#/definitions/AttachmentData"
+          "from": {
+            "$ref": "#/definitions/ChannelAccount",
+            "description": "Identifies the sender of the message."
+          },
+          "conversation": {
+            "$ref": "#/definitions/ConversationAccount",
+            "description": "Identifies the conversation to which the activity belongs."
+          },
+          "recipient": {
+            "$ref": "#/definitions/ChannelAccount",
+            "description": "Identifies the recipient of the message."
+          },
+          "textFormat": {
+            "$ref": "#/definitions/TextFormatTypes",
+            "description": "Format of text fields Default:markdown"
+          },
+          "attachmentLayout": {
+            "$ref": "#/definitions/AttachmentLayoutTypes",
+            "description": "The layout hint for multiple attachments. Default: list."
+          },
+          "membersAdded": {
+            "description": "The collection of members added to the conversation.",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/ChannelAccount"
             }
-          }
-        ],
-        "responses": {
-          "200": {
-            "description": "An object will be returned containing the ID for the resource.",
-            "schema": {
-              "$ref": "#/definitions/ResourceResponse"
+          },
+          "membersRemoved": {
+            "description": "The collection of members removed from the conversation.",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/ChannelAccount"
             }
           },
-          "201": {
-            "description": "A ResourceResponse object will be returned containing the ID for the resource.",
-            "schema": {
-              "$ref": "#/definitions/ResourceResponse"
+          "reactionsAdded": {
+            "description": "The collection of reactions added to the conversation.",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/MessageReaction"
             }
           },
-          "202": {
-            "description": "An object will be returned containing the ID for the resource.",
-            "schema": {
-              "$ref": "#/definitions/ResourceResponse"
+          "reactionsRemoved": {
+            "description": "The collection of reactions removed from the conversation.",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/MessageReaction"
             }
           },
-          "default": {
-            "description": "The operation failed and the response is an error object describing the status code and failure.",
-            "schema": {
-              "$ref": "#/definitions/ErrorResponse"
+          "topicName": {
+            "description": "The updated topic name of the conversation.",
+            "type": "string"
+          },
+          "historyDisclosed": {
+            "description": "Indicates whether the prior history of the channel is disclosed.",
+            "type": "boolean"
+          },
+          "locale": {
+            "description": "A locale name for the contents of the text field.\r\nThe locale name is a combination of an ISO 639 two- or three-letter culture code associated with a language\r\nand an ISO 3166 two-letter subculture code associated with a country or region.\r\nThe locale name can also correspond to a valid BCP-47 language tag.",
+            "type": "string"
+          },
+          "text": {
+            "description": "The text content of the message.",
+            "type": "string"
+          },
+          "speak": {
+            "description": "The text to speak.",
+            "type": "string"
+          },
+          "inputHint": {
+            "$ref": "#/definitions/InputHints",
+            "description": "Indicates whether your bot is accepting,\r\nexpecting, or ignoring user input after the message is delivered to the client."
+          },
+          "summary": {
+            "description": "The text to display if the channel cannot render cards.",
+            "type": "string"
+          },
+          "suggestedActions": {
+            "$ref": "#/definitions/SuggestedActions",
+            "description": "The suggested actions for the activity."
+          },
+          "attachments": {
+            "description": "Attachments",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/Attachment"
             }
+          },
+          "entities": {
+            "description": "Represents the entities that were mentioned in the message.",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/Entity"
+            }
+          },
+          "channelData": {
+            "description": "Contains channel-specific content.",
+            "type": "object"
+          },
+          "action": {
+            "description": "Indicates whether the recipient of a contactRelationUpdate was added or removed from the sender's contact list.",
+            "type": "string"
+          },
+          "replyToId": {
+            "description": "Contains the ID of the message to which this message is a reply.",
+            "type": "string"
+          },
+          "label": {
+            "description": "A descriptive label for the activity.",
+            "type": "string"
+          },
+          "valueType": {
+            "description": "The type of the activity's value object.",
+            "type": "string"
+          },
+          "value": {
+            "description": "A value that is associated with the activity.",
+            "type": "object"
+          },
+          "name": {
+            "description": "The name of the operation associated with an invoke or event activity.",
+            "type": "string"
+          },
+          "relatesTo": {
+            "$ref": "#/definitions/ConversationReference",
+            "description": "A reference to another conversation or activity."
+          },
+          "code": {
+            "$ref": "#/definitions/EndOfConversationCodes",
+            "description": "The a code for endOfConversation activities that indicates why the conversation ended."
+          },
+          "expiration": {
+            "format": "date-time",
+            "description": "The time at which the activity should be considered to be \"expired\" and should not be presented to the recipient.",
+            "type": "string"
+          },
+          "importance": {
+            "$ref": "#/definitions/ActivityImportance",
+            "description": "The importance of the activity."
+          },
+          "deliveryMode": {
+            "$ref": "#/definitions/DeliveryModes",
+            "description": "A delivery hint to signal to the recipient alternate delivery paths for the activity.\r\nThe default delivery mode is \"default\"."
+          },
+          "listenFor": {
+            "description": "List of phrases and references that speech and language priming systems should listen for",
+            "type": "array",
+            "items": {
+              "type": "string"
+            }
+          },
+          "textHighlights": {
+            "description": "The collection of text fragments to highlight when the activity contains a ReplyToId value.",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/TextHighlight"
+            }
+          },
+          "semanticAction": {
+            "$ref": "#/definitions/SemanticAction",
+            "description": "An optional programmatic action accompanying this request"
           }
         }
-      }
-    }
-  },
-  "definitions": {
-    "AttachmentInfo": {
-      "description": "Metadata for an attachment",
-      "type": "object",
-      "properties": {
-        "name": {
-          "description": "Name of the attachment",
-          "type": "string"
-        },
-        "type": {
-          "description": "ContentType of the attachment",
-          "type": "string"
-        },
-        "views": {
-          "description": "attachment views",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/AttachmentView"
-          }
-        }
-      }
-    },
-    "AttachmentView": {
-      "description": "Attachment View name and size",
-      "type": "object",
-      "properties": {
-        "viewId": {
-          "description": "Id of the attachment",
-          "type": "string"
-        },
-        "size": {
-          "format": "int32",
-          "description": "Size of the attachment",
-          "type": "integer"
-        }
-      }
-    },
-    "ErrorResponse": {
-      "description": "An HTTP API response",
-      "type": "object",
-      "properties": {
-        "error": {
-          "$ref": "#/definitions/Error",
-          "description": "Error message"
-        }
-      }
-    },
-    "Error": {
-      "description": "Object representing error information",
-      "type": "object",
-      "properties": {
-        "code": {
-          "description": "Error code",
-          "type": "string"
-        },
-        "message": {
-          "description": "Error message",
-          "type": "string"
-        },
-        "innerHttpError": {
-          "$ref": "#/definitions/InnerHttpError",
-          "description": "Error from inner http call"
-        }
-      }
-    },
-    "InnerHttpError": {
-      "description": "Object representing inner http error",
-      "type": "object",
-      "properties": {
-        "statusCode": {
-          "format": "int32",
-          "description": "HttpStatusCode from failed request",
-          "type": "integer"
-        },
-        "body": {
-          "description": "Body from failed request",
-          "type": "object"
-        }
-      }
-    },
-    "ConversationParameters": {
-      "description": "Parameters for creating a new conversation",
-      "type": "object",
-      "properties": {
-        "isGroup": {
-          "description": "IsGroup",
-          "type": "boolean"
-        },
-        "bot": {
-          "$ref": "#/definitions/ChannelAccount",
-          "description": "The bot address for this conversation"
-        },
-        "members": {
-          "description": "Members to add to the conversation",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/ChannelAccount"
+      },
+      "ConversationAccount": {
+        "description": "Conversation account represents the identity of the conversation within a channel",
+        "type": "object",
+        "properties": {
+          "isGroup": {
+            "description": "Indicates whether the conversation contains more than two participants at the time the activity was generated",
+            "type": "boolean"
+          },
+          "conversationType": {
+            "description": "Indicates the type of the conversation in channels that distinguish between conversation types",
+            "type": "string"
+          },
+          "tenantId": {
+            "description": "This conversation's tenant ID",
+            "type": "string"
+          },
+          "id": {
+            "description": "Channel id for the user or bot on this channel (Example: joe@smith.com, or @joesmith or 123456)",
+            "type": "string"
+          },
+          "name": {
+            "description": "Display friendly name",
+            "type": "string"
+          },
+          "aadObjectId": {
+            "description": "This account's object ID within Azure Active Directory (AAD)",
+            "type": "string"
+          },
+          "role": {
+            "$ref": "#/definitions/RoleTypes",
+            "description": "Role of the entity behind the account (Example: User, Bot, Skill, etc.)"
           }
-        },
-        "topicName": {
-          "description": "(Optional) Topic of the conversation (if supported by the channel)",
-          "type": "string"
-        },
-        "tenantId": {
-          "description": "(Optional) The tenant ID in which the conversation should be created",
-          "type": "string"
-        },
-        "activity": {
-          "$ref": "#/definitions/Activity",
-          "description": "(Optional) When creating a new conversation, use this activity as the initial message to the conversation"
-        },
-        "channelData": {
-          "description": "Channel specific payload for creating the conversation",
-          "type": "object"
         }
-      }
-    },
-    "ChannelAccount": {
-      "description": "Channel account information needed to route a message",
-      "type": "object",
-      "properties": {
-        "id": {
-          "description": "Channel id for the user or bot on this channel (Example: joe@smith.com, or @joesmith or 123456)",
-          "type": "string"
-        },
-        "name": {
-          "description": "Display friendly name",
-          "type": "string"
-        },
-        "aadObjectId": {
-          "description": "This account's object ID within Azure Active Directory (AAD)",
-          "type": "string"
-        },
-        "role": {
-          "$ref": "#/definitions/RoleTypes",
-          "description": "Role of the entity behind the account (Example: User, Bot, etc.)"
-        }
-      }
-    },
-    "Activity": {
-      "description": "An Activity is the basic communication type for the Bot Framework 3.0 protocol.",
-      "type": "object",
-      "properties": {
-        "type": {
-          "$ref": "#/definitions/ActivityTypes",
-          "description": "Contains the activity type."
-        },
-        "id": {
-          "description": "Contains an ID that uniquely identifies the activity on the channel.",
-          "type": "string"
-        },
-        "timestamp": {
-          "format": "date-time",
-          "description": "Contains the date and time that the message was sent, in UTC, expressed in ISO-8601 format.",
-          "type": "string"
-        },
-        "localTimestamp": {
-          "format": "date-time",
-          "description": "Contains the local date and time of the message, expressed in ISO-8601 format.\r\nFor example, 2016-09-23T13:07:49.4714686-07:00.",
-          "type": "string"
-        },
-        "localTimezone": {
-          "description": "Contains the name of the local timezone of the message, expressed in IANA Time Zone database format.\r\nFor example, America/Los_Angeles.",
-          "type": "string"
-        },
-        "callerId": {
-          "description": "A string containing an IRI identifying the caller of a bot. This field is not intended to be transmitted\r\nover the wire, but is instead populated by bots and clients based on cryptographically verifiable data\r\nthat asserts the identity of the callers (e.g. tokens).",
-          "type": "string"
-        },
-        "serviceUrl": {
-          "description": "Contains the URL that specifies the channel's service endpoint. Set by the channel.",
-          "type": "string"
-        },
-        "channelId": {
-          "description": "Contains an ID that uniquely identifies the channel. Set by the channel.",
-          "type": "string"
-        },
-        "from": {
-          "$ref": "#/definitions/ChannelAccount",
-          "description": "Identifies the sender of the message."
-        },
-        "conversation": {
-          "$ref": "#/definitions/ConversationAccount",
-          "description": "Identifies the conversation to which the activity belongs."
-        },
-        "recipient": {
-          "$ref": "#/definitions/ChannelAccount",
-          "description": "Identifies the recipient of the message."
-        },
-        "textFormat": {
-          "$ref": "#/definitions/TextFormatTypes",
-          "description": "Format of text fields Default:markdown"
-        },
-        "attachmentLayout": {
-          "$ref": "#/definitions/AttachmentLayoutTypes",
-          "description": "The layout hint for multiple attachments. Default: list."
-        },
-        "membersAdded": {
-          "description": "The collection of members added to the conversation.",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/ChannelAccount"
-          }
-        },
-        "membersRemoved": {
-          "description": "The collection of members removed from the conversation.",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/ChannelAccount"
-          }
-        },
-        "reactionsAdded": {
-          "description": "The collection of reactions added to the conversation.",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/MessageReaction"
-          }
-        },
-        "reactionsRemoved": {
-          "description": "The collection of reactions removed from the conversation.",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/MessageReaction"
-          }
-        },
-        "topicName": {
-          "description": "The updated topic name of the conversation.",
-          "type": "string"
-        },
-        "historyDisclosed": {
-          "description": "Indicates whether the prior history of the channel is disclosed.",
-          "type": "boolean"
-        },
-        "locale": {
-          "description": "A locale name for the contents of the text field.\r\nThe locale name is a combination of an ISO 639 two- or three-letter culture code associated with a language\r\nand an ISO 3166 two-letter subculture code associated with a country or region.\r\nThe locale name can also correspond to a valid BCP-47 language tag.",
-          "type": "string"
-        },
-        "text": {
-          "description": "The text content of the message.",
-          "type": "string"
-        },
-        "speak": {
-          "description": "The text to speak.",
-          "type": "string"
-        },
-        "inputHint": {
-          "$ref": "#/definitions/InputHints",
-          "description": "Indicates whether your bot is accepting,\r\nexpecting, or ignoring user input after the message is delivered to the client."
-        },
-        "summary": {
-          "description": "The text to display if the channel cannot render cards.",
-          "type": "string"
-        },
-        "suggestedActions": {
-          "$ref": "#/definitions/SuggestedActions",
-          "description": "The suggested actions for the activity."
-        },
-        "attachments": {
-          "description": "Attachments",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/Attachment"
+      },
+      "MessageReaction": {
+        "description": "Message reaction object",
+        "type": "object",
+        "properties": {
+          "type": {
+            "$ref": "#/definitions/MessageReactionTypes",
+            "description": "Message reaction type"
           }
-        },
-        "entities": {
-          "description": "Represents the entities that were mentioned in the message.",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/Entity"
+        }
+      },
+      "SuggestedActions": {
+        "description": "SuggestedActions that can be performed",
+        "type": "object",
+        "properties": {
+          "to": {
+            "description": "Ids of the recipients that the actions should be shown to.  These Ids are relative to the channelId and a subset of all recipients of the activity",
+            "type": "array",
+            "items": {
+              "type": "string"
+            }
+          },
+          "actions": {
+            "description": "Actions that can be shown to the user",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/CardAction"
+            }
           }
-        },
-        "channelData": {
-          "description": "Contains channel-specific content.",
-          "type": "object"
-        },
-        "action": {
-          "description": "Indicates whether the recipient of a contactRelationUpdate was added or removed from the sender's contact list.",
-          "type": "string"
-        },
-        "replyToId": {
-          "description": "Contains the ID of the message to which this message is a reply.",
-          "type": "string"
-        },
-        "label": {
-          "description": "A descriptive label for the activity.",
-          "type": "string"
-        },
-        "valueType": {
-          "description": "The type of the activity's value object.",
-          "type": "string"
-        },
-        "value": {
-          "description": "A value that is associated with the activity.",
-          "type": "object"
-        },
-        "name": {
-          "description": "The name of the operation associated with an invoke or event activity.",
-          "type": "string"
-        },
-        "relatesTo": {
-          "$ref": "#/definitions/ConversationReference",
-          "description": "A reference to another conversation or activity."
-        },
-        "code": {
-          "$ref": "#/definitions/EndOfConversationCodes",
-          "description": "The a code for endOfConversation activities that indicates why the conversation ended."
-        },
-        "expiration": {
-          "format": "date-time",
-          "description": "The time at which the activity should be considered to be \"expired\" and should not be presented to the recipient.",
-          "type": "string"
-        },
-        "importance": {
-          "$ref": "#/definitions/ActivityImportance",
-          "description": "The importance of the activity."
-        },
-        "deliveryMode": {
-          "$ref": "#/definitions/DeliveryModes",
-          "description": "A delivery hint to signal to the recipient alternate delivery paths for the activity.\r\nThe default delivery mode is \"default\"."
-        },
-        "listenFor": {
-          "description": "List of phrases and references that speech and language priming systems should listen for",
-          "type": "array",
-          "items": {
+        }
+      },
+      "Attachment": {
+        "description": "An attachment within an activity",
+        "type": "object",
+        "properties": {
+          "contentType": {
+            "description": "mimetype/Contenttype for the file",
+            "type": "string"
+          },
+          "contentUrl": {
+            "description": "Content Url",
+            "type": "string"
+          },
+          "content": {
+            "description": "Embedded content",
+            "type": "object"
+          },
+          "name": {
+            "description": "(OPTIONAL) The name of the attachment",
+            "type": "string"
+          },
+          "thumbnailUrl": {
+            "description": "(OPTIONAL) Thumbnail associated with attachment",
             "type": "string"
           }
-        },
-        "textHighlights": {
-          "description": "The collection of text fragments to highlight when the activity contains a ReplyToId value.",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/TextHighlight"
-          }
-        },
-        "semanticAction": {
-          "$ref": "#/definitions/SemanticAction",
-          "description": "An optional programmatic action accompanying this request"
-        }
-      }
-    },
-    "ConversationAccount": {
-      "description": "Conversation account represents the identity of the conversation within a channel",
-      "type": "object",
-      "properties": {
-        "isGroup": {
-          "description": "Indicates whether the conversation contains more than two participants at the time the activity was generated",
-          "type": "boolean"
-        },
-        "conversationType": {
-          "description": "Indicates the type of the conversation in channels that distinguish between conversation types",
-          "type": "string"
-        },
-        "tenantId": {
-          "description": "This conversation's tenant ID",
-          "type": "string"
-        },
-        "id": {
-          "description": "Channel id for the user or bot on this channel (Example: joe@smith.com, or @joesmith or 123456)",
-          "type": "string"
-        },
-        "name": {
-          "description": "Display friendly name",
-          "type": "string"
-        },
-        "aadObjectId": {
-          "description": "This account's object ID within Azure Active Directory (AAD)",
-          "type": "string"
-        },
-        "role": {
-          "$ref": "#/definitions/RoleTypes",
-          "description": "Role of the entity behind the account (Example: User, Bot, etc.)"
         }
-      }
-    },
-    "MessageReaction": {
-      "description": "Message reaction object",
-      "type": "object",
-      "properties": {
-        "type": {
-          "$ref": "#/definitions/MessageReactionTypes",
-          "description": "Message reaction type"
-        }
-      }
-    },
-    "SuggestedActions": {
-      "description": "SuggestedActions that can be performed",
-      "type": "object",
-      "properties": {
-        "to": {
-          "description": "Ids of the recipients that the actions should be shown to.  These Ids are relative to the channelId and a subset of all recipients of the activity",
-          "type": "array",
-          "items": {
+      },
+      "Entity": {
+        "description": "Metadata object pertaining to an activity",
+        "type": "object",
+        "properties": {
+          "type": {
+            "description": "Type of this entity (RFC 3987 IRI)",
             "type": "string"
           }
-        },
-        "actions": {
-          "description": "Actions that can be shown to the user",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/CardAction"
-          }
-        }
-      }
-    },
-    "Attachment": {
-      "description": "An attachment within an activity",
-      "type": "object",
-      "properties": {
-        "contentType": {
-          "description": "mimetype/Contenttype for the file",
-          "type": "string"
-        },
-        "contentUrl": {
-          "description": "Content Url",
-          "type": "string"
-        },
-        "content": {
-          "description": "Embedded content",
-          "type": "object"
-        },
-        "name": {
-          "description": "(OPTIONAL) The name of the attachment",
-          "type": "string"
-        },
-        "thumbnailUrl": {
-          "description": "(OPTIONAL) Thumbnail associated with attachment",
-          "type": "string"
-        }
-      }
-    },
-    "Entity": {
-      "description": "Metadata object pertaining to an activity",
-      "type": "object",
-      "properties": {
-        "type": {
-          "description": "Type of this entity (RFC 3987 IRI)",
-          "type": "string"
         }
-      }
-    },
-    "ConversationReference": {
-      "description": "An object relating to a particular point in a conversation",
-      "type": "object",
-      "properties": {
-        "activityId": {
-          "description": "(Optional) ID of the activity to refer to",
-          "type": "string"
-        },
-        "user": {
-          "$ref": "#/definitions/ChannelAccount",
-          "description": "(Optional) User participating in this conversation"
-        },
-        "bot": {
-          "$ref": "#/definitions/ChannelAccount",
-          "description": "Bot participating in this conversation"
-        },
-        "conversation": {
-          "$ref": "#/definitions/ConversationAccount",
-          "description": "Conversation reference"
-        },
-        "channelId": {
-          "description": "Channel ID",
-          "type": "string"
-        },
-        "serviceUrl": {
-          "description": "Service endpoint where operations concerning the referenced conversation may be performed",
-          "type": "string"
-        }
-      }
-    },
-    "TextHighlight": {
-      "description": "Refers to a substring of content within another field",
-      "type": "object",
-      "properties": {
-        "text": {
-          "description": "Defines the snippet of text to highlight",
-          "type": "string"
-        },
-        "occurrence": {
-          "format": "int32",
-          "description": "Occurrence of the text field within the referenced text, if multiple exist.",
-          "type": "integer"
-        }
-      }
-    },
-    "SemanticAction": {
-      "description": "Represents a reference to a programmatic action",
-      "type": "object",
-      "properties": {
-        "state": {
-          "$ref": "#/definitions/SemanticActionStates",
-          "description": "State of this action. Allowed values: `start`, `continue`, `done`"
-        },
-        "id": {
-          "description": "ID of this action",
-          "type": "string"
-        },
-        "entities": {
-          "description": "Entities associated with this action",
-          "type": "object",
-          "additionalProperties": {
-            "$ref": "#/definitions/Entity"
+      },
+      "ConversationReference": {
+        "description": "An object relating to a particular point in a conversation",
+        "type": "object",
+        "properties": {
+          "activityId": {
+            "description": "(Optional) ID of the activity to refer to",
+            "type": "string"
+          },
+          "user": {
+            "$ref": "#/definitions/ChannelAccount",
+            "description": "(Optional) User participating in this conversation"
+          },
+          "bot": {
+            "$ref": "#/definitions/ChannelAccount",
+            "description": "Bot participating in this conversation"
+          },
+          "conversation": {
+            "$ref": "#/definitions/ConversationAccount",
+            "description": "Conversation reference"
+          },
+          "channelId": {
+            "description": "Channel ID",
+            "type": "string"
+          },
+          "serviceUrl": {
+            "description": "Service endpoint where operations concerning the referenced conversation may be performed",
+            "type": "string"
           }
         }
-      }
-    },
-    "CardAction": {
-      "description": "A clickable action",
-      "type": "object",
-      "properties": {
-        "type": {
-          "$ref": "#/definitions/ActionTypes",
-          "description": "The type of action implemented by this button"
-        },
-        "title": {
-          "description": "Text description which appears on the button",
-          "type": "string"
-        },
-        "image": {
-          "description": "Image URL which will appear on the button, next to text label",
-          "type": "string"
-        },
-        "text": {
-          "description": "Text for this action",
-          "type": "string"
-        },
-        "displayText": {
-          "description": "(Optional) text to display in the chat feed if the button is clicked",
-          "type": "string"
-        },
-        "value": {
-          "description": "Supplementary parameter for action. Content of this property depends on the ActionType",
-          "type": "object"
-        },
-        "channelData": {
-          "description": "Channel-specific data associated with this action",
-          "type": "object"
+      },
+      "TextHighlight": {
+        "description": "Refers to a substring of content within another field",
+        "type": "object",
+        "properties": {
+          "text": {
+            "description": "Defines the snippet of text to highlight",
+            "type": "string"
+          },
+          "occurrence": {
+            "format": "int32",
+            "description": "Occurrence of the text field within the referenced text, if multiple exist.",
+            "type": "integer"
+          }
         }
-      }
-    },
-    "ConversationResourceResponse": {
-      "description": "A response containing a resource",
-      "type": "object",
-      "properties": {
-        "activityId": {
-          "description": "ID of the Activity (if sent)",
-          "type": "string"
-        },
-        "serviceUrl": {
-          "description": "Service endpoint where operations concerning the conversation may be performed",
-          "type": "string"
-        },
-        "id": {
-          "description": "Id of the resource",
-          "type": "string"
+      },
+      "SemanticAction": {
+        "description": "Represents a reference to a programmatic action",
+        "type": "object",
+        "properties": {
+          "state": {
+            "$ref": "#/definitions/SemanticActionStates",
+            "description": "State of this action. Allowed values: `start`, `continue`, `done`"
+          },
+          "id": {
+            "description": "ID of this action",
+            "type": "string"
+          },
+          "entities": {
+            "description": "Entities associated with this action",
+            "type": "object",
+            "additionalProperties": {
+              "$ref": "#/definitions/Entity"
+            }
+          }
         }
-      }
-    },
-    "ConversationsResult": {
-      "description": "Conversations result",
-      "type": "object",
-      "properties": {
-        "continuationToken": {
-          "description": "Paging token",
-          "type": "string"
-        },
-        "conversations": {
-          "description": "List of conversations",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/ConversationMembers"
+      },
+      "CardAction": {
+        "description": "A clickable action",
+        "type": "object",
+        "properties": {
+          "type": {
+            "$ref": "#/definitions/ActionTypes",
+            "description": "The type of action implemented by this button"
+          },
+          "title": {
+            "description": "Text description which appears on the button",
+            "type": "string"
+          },
+          "image": {
+            "description": "Image URL which will appear on the button, next to text label",
+            "type": "string"
+          },
+          "text": {
+            "description": "Text for this action",
+            "type": "string"
+          },
+          "displayText": {
+            "description": "(Optional) text to display in the chat feed if the button is clicked",
+            "type": "string"
+          },
+          "value": {
+            "description": "Supplementary parameter for action. Content of this property depends on the ActionType",
+            "type": "object"
+          },
+          "channelData": {
+            "description": "Channel-specific data associated with this action",
+            "type": "object"
           }
         }
-      }
-    },
-    "ConversationMembers": {
-      "description": "Conversation and its members",
-      "type": "object",
-      "properties": {
-        "id": {
-          "description": "Conversation ID",
-          "type": "string"
-        },
-        "members": {
-          "description": "List of members in this conversation",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/ChannelAccount"
+      },
+      "ConversationResourceResponse": {
+        "description": "A response containing a resource",
+        "type": "object",
+        "properties": {
+          "activityId": {
+            "description": "ID of the Activity (if sent)",
+            "type": "string"
+          },
+          "serviceUrl": {
+            "description": "Service endpoint where operations concerning the conversation may be performed",
+            "type": "string"
+          },
+          "id": {
+            "description": "Id of the resource",
+            "type": "string"
           }
         }
-      }
-    },
-    "ResourceResponse": {
-      "description": "A response containing a resource ID",
-      "type": "object",
-      "properties": {
-        "id": {
-          "description": "Id of the resource",
-          "type": "string"
+      },
+      "ConversationsResult": {
+        "description": "Conversations result",
+        "type": "object",
+        "properties": {
+          "continuationToken": {
+            "description": "Paging token",
+            "type": "string"
+          },
+          "conversations": {
+            "description": "List of conversations",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/ConversationMembers"
+            }
+          }
         }
-      }
-    },
-    "Transcript": {
-      "description": "Transcript",
-      "type": "object",
-      "properties": {
-        "activities": {
-          "description": "A collection of Activities that conforms to the Transcript schema.",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/Activity"
+      },
+      "ConversationMembers": {
+        "description": "Conversation and its members",
+        "type": "object",
+        "properties": {
+          "id": {
+            "description": "Conversation ID",
+            "type": "string"
+          },
+          "members": {
+            "description": "List of members in this conversation",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/ChannelAccount"
+            }
           }
         }
-      }
-    },
-    "PagedMembersResult": {
-      "description": "Page of members.",
-      "type": "object",
-      "properties": {
-        "continuationToken": {
-          "description": "Paging token",
-          "type": "string"
-        },
-        "members": {
-          "description": "The Channel Accounts.",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/ChannelAccount"
+      },
+      "ResourceResponse": {
+        "description": "A response containing a resource ID",
+        "type": "object",
+        "properties": {
+          "id": {
+            "description": "Id of the resource",
+            "type": "string"
           }
         }
-      }
-    },
-    "AttachmentData": {
-      "description": "Attachment data",
-      "type": "object",
-      "properties": {
-        "type": {
-          "description": "Content-Type of the attachment",
-          "type": "string"
-        },
-        "name": {
-          "description": "Name of the attachment",
-          "type": "string"
-        },
-        "originalBase64": {
-          "format": "byte",
-          "description": "Attachment content",
-          "type": "string"
-        },
-        "thumbnailBase64": {
-          "format": "byte",
-          "description": "Attachment thumbnail",
-          "type": "string"
+      },
+      "Transcript": {
+        "description": "Transcript",
+        "type": "object",
+        "properties": {
+          "activities": {
+            "description": "A collection of Activities that conforms to the Transcript schema.",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/Activity"
+            }
+          }
         }
-      }
-    },
-    "HeroCard": {
-      "description": "A Hero card (card with a single, large image)",
-      "type": "object",
-      "properties": {
-        "title": {
-          "description": "Title of the card",
-          "type": "string"
-        },
-        "subtitle": {
-          "description": "Subtitle of the card",
-          "type": "string"
-        },
-        "text": {
-          "description": "Text for the card",
-          "type": "string"
-        },
-        "images": {
-          "description": "Array of images for the card",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/CardImage"
+      },
+      "PagedMembersResult": {
+        "description": "Page of members.",
+        "type": "object",
+        "properties": {
+          "continuationToken": {
+            "description": "Paging token",
+            "type": "string"
+          },
+          "members": {
+            "description": "The Channel Accounts.",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/ChannelAccount"
+            }
           }
-        },
-        "buttons": {
-          "description": "Set of actions applicable to the current card",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/CardAction"
+        }
+      },
+      "AttachmentData": {
+        "description": "Attachment data",
+        "type": "object",
+        "properties": {
+          "type": {
+            "description": "Content-Type of the attachment",
+            "type": "string"
+          },
+          "name": {
+            "description": "Name of the attachment",
+            "type": "string"
+          },
+          "originalBase64": {
+            "format": "byte",
+            "description": "Attachment content",
+            "type": "string"
+          },
+          "thumbnailBase64": {
+            "format": "byte",
+            "description": "Attachment thumbnail",
+            "type": "string"
           }
-        },
-        "tap": {
-          "$ref": "#/definitions/CardAction",
-          "description": "This action will be activated when user taps on the card itself"
         }
-      }
-    },
-    "CardImage": {
-      "description": "An image on a card",
-      "type": "object",
-      "properties": {
-        "url": {
-          "description": "URL thumbnail image for major content property",
-          "type": "string"
-        },
-        "alt": {
-          "description": "Image description intended for screen readers",
-          "type": "string"
-        },
-        "tap": {
-          "$ref": "#/definitions/CardAction",
-          "description": "Action assigned to specific Attachment"
+      },
+      "HeroCard": {
+        "description": "A Hero card (card with a single, large image)",
+        "type": "object",
+        "properties": {
+          "title": {
+            "description": "Title of the card",
+            "type": "string"
+          },
+          "subtitle": {
+            "description": "Subtitle of the card",
+            "type": "string"
+          },
+          "text": {
+            "description": "Text for the card",
+            "type": "string"
+          },
+          "images": {
+            "description": "Array of images for the card",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/CardImage"
+            }
+          },
+          "buttons": {
+            "description": "Set of actions applicable to the current card",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/CardAction"
+            }
+          },
+          "tap": {
+            "$ref": "#/definitions/CardAction",
+            "description": "This action will be activated when user taps on the card itself"
+          }
         }
-      }
-    },
-    "AnimationCard": {
-      "description": "An animation card (Ex: gif or short video clip)",
-      "type": "object",
-      "properties": {
-        "title": {
-          "description": "Title of this card",
-          "type": "string"
-        },
-        "subtitle": {
-          "description": "Subtitle of this card",
-          "type": "string"
-        },
-        "text": {
-          "description": "Text of this card",
-          "type": "string"
-        },
-        "image": {
-          "$ref": "#/definitions/ThumbnailUrl",
-          "description": "Thumbnail placeholder"
-        },
-        "media": {
-          "description": "Media URLs for this card. When this field contains more than one URL, each URL is an alternative format of the same content.",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/MediaUrl"
+      },
+      "CardImage": {
+        "description": "An image on a card",
+        "type": "object",
+        "properties": {
+          "url": {
+            "description": "URL thumbnail image for major content property",
+            "type": "string"
+          },
+          "alt": {
+            "description": "Image description intended for screen readers",
+            "type": "string"
+          },
+          "tap": {
+            "$ref": "#/definitions/CardAction",
+            "description": "Action assigned to specific Attachment"
           }
-        },
-        "buttons": {
-          "description": "Actions on this card",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/CardAction"
+        }
+      },
+      "AnimationCard": {
+        "description": "An animation card (Ex: gif or short video clip)",
+        "type": "object",
+        "properties": {
+          "title": {
+            "description": "Title of this card",
+            "type": "string"
+          },
+          "subtitle": {
+            "description": "Subtitle of this card",
+            "type": "string"
+          },
+          "text": {
+            "description": "Text of this card",
+            "type": "string"
+          },
+          "image": {
+            "$ref": "#/definitions/ThumbnailUrl",
+            "description": "Thumbnail placeholder"
+          },
+          "media": {
+            "description": "Media URLs for this card. When this field contains more than one URL, each URL is an alternative format of the same content.",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/MediaUrl"
+            }
+          },
+          "buttons": {
+            "description": "Actions on this card",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/CardAction"
+            }
+          },
+          "shareable": {
+            "description": "This content may be shared with others (default:true)",
+            "type": "boolean"
+          },
+          "autoloop": {
+            "description": "Should the client loop playback at end of content (default:true)",
+            "type": "boolean"
+          },
+          "autostart": {
+            "description": "Should the client automatically start playback of media in this card (default:true)",
+            "type": "boolean"
+          },
+          "aspect": {
+            "description": "Aspect ratio of thumbnail/media placeholder. Allowed values are \"16:9\" and \"4:3\"",
+            "type": "string"
+          },
+          "duration": {
+            "description": "Describes the length of the media content without requiring a receiver to open the content. Formatted as an ISO 8601 Duration field.",
+            "type": "string"
+          },
+          "value": {
+            "description": "Supplementary parameter for this card",
+            "type": "object"
           }
-        },
-        "shareable": {
-          "description": "This content may be shared with others (default:true)",
-          "type": "boolean"
-        },
-        "autoloop": {
-          "description": "Should the client loop playback at end of content (default:true)",
-          "type": "boolean"
-        },
-        "autostart": {
-          "description": "Should the client automatically start playback of media in this card (default:true)",
-          "type": "boolean"
-        },
-        "aspect": {
-          "description": "Aspect ratio of thumbnail/media placeholder. Allowed values are \"16:9\" and \"4:3\"",
-          "type": "string"
-        },
-        "duration": {
-          "description": "Describes the length of the media content without requiring a receiver to open the content. Formatted as an ISO 8601 Duration field.",
-          "type": "string"
-        },
-        "value": {
-          "description": "Supplementary parameter for this card",
-          "type": "object"
         }
-      }
-    },
-    "ThumbnailUrl": {
-      "description": "Thumbnail URL",
-      "type": "object",
-      "properties": {
-        "url": {
-          "description": "URL pointing to the thumbnail to use for media content",
-          "type": "string"
-        },
-        "alt": {
-          "description": "HTML alt text to include on this thumbnail image",
-          "type": "string"
+      },
+      "ThumbnailUrl": {
+        "description": "Thumbnail URL",
+        "type": "object",
+        "properties": {
+          "url": {
+            "description": "URL pointing to the thumbnail to use for media content",
+            "type": "string"
+          },
+          "alt": {
+            "description": "HTML alt text to include on this thumbnail image",
+            "type": "string"
+          }
         }
-      }
-    },
-    "MediaUrl": {
-      "description": "Media URL",
-      "type": "object",
-      "properties": {
-        "url": {
-          "description": "Url for the media",
-          "type": "string"
-        },
-        "profile": {
-          "description": "Optional profile hint to the client to differentiate multiple MediaUrl objects from each other",
-          "type": "string"
+      },
+      "MediaUrl": {
+        "description": "Media URL",
+        "type": "object",
+        "properties": {
+          "url": {
+            "description": "Url for the media",
+            "type": "string"
+          },
+          "profile": {
+            "description": "Optional profile hint to the client to differentiate multiple MediaUrl objects from each other",
+            "type": "string"
+          }
         }
-      }
-    },
-    "AudioCard": {
-      "description": "Audio card",
-      "type": "object",
-      "properties": {
-        "title": {
-          "description": "Title of this card",
-          "type": "string"
-        },
-        "subtitle": {
-          "description": "Subtitle of this card",
-          "type": "string"
-        },
-        "text": {
-          "description": "Text of this card",
-          "type": "string"
-        },
-        "image": {
-          "$ref": "#/definitions/ThumbnailUrl",
-          "description": "Thumbnail placeholder"
-        },
-        "media": {
-          "description": "Media URLs for this card. When this field contains more than one URL, each URL is an alternative format of the same content.",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/MediaUrl"
+      },
+      "AudioCard": {
+        "description": "Audio card",
+        "type": "object",
+        "properties": {
+          "title": {
+            "description": "Title of this card",
+            "type": "string"
+          },
+          "subtitle": {
+            "description": "Subtitle of this card",
+            "type": "string"
+          },
+          "text": {
+            "description": "Text of this card",
+            "type": "string"
+          },
+          "image": {
+            "$ref": "#/definitions/ThumbnailUrl",
+            "description": "Thumbnail placeholder"
+          },
+          "media": {
+            "description": "Media URLs for this card. When this field contains more than one URL, each URL is an alternative format of the same content.",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/MediaUrl"
+            }
+          },
+          "buttons": {
+            "description": "Actions on this card",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/CardAction"
+            }
+          },
+          "shareable": {
+            "description": "This content may be shared with others (default:true)",
+            "type": "boolean"
+          },
+          "autoloop": {
+            "description": "Should the client loop playback at end of content (default:true)",
+            "type": "boolean"
+          },
+          "autostart": {
+            "description": "Should the client automatically start playback of media in this card (default:true)",
+            "type": "boolean"
+          },
+          "aspect": {
+            "description": "Aspect ratio of thumbnail/media placeholder. Allowed values are \"16:9\" and \"4:3\"",
+            "type": "string"
+          },
+          "duration": {
+            "description": "Describes the length of the media content without requiring a receiver to open the content. Formatted as an ISO 8601 Duration field.",
+            "type": "string"
+          },
+          "value": {
+            "description": "Supplementary parameter for this card",
+            "type": "object"
           }
-        },
-        "buttons": {
-          "description": "Actions on this card",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/CardAction"
+        }
+      },
+      "BasicCard": {
+        "description": "A basic card",
+        "type": "object",
+        "properties": {
+          "title": {
+            "description": "Title of the card",
+            "type": "string"
+          },
+          "subtitle": {
+            "description": "Subtitle of the card",
+            "type": "string"
+          },
+          "text": {
+            "description": "Text for the card",
+            "type": "string"
+          },
+          "images": {
+            "description": "Array of images for the card",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/CardImage"
+            }
+          },
+          "buttons": {
+            "description": "Set of actions applicable to the current card",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/CardAction"
+            }
+          },
+          "tap": {
+            "$ref": "#/definitions/CardAction",
+            "description": "This action will be activated when user taps on the card itself"
           }
-        },
-        "shareable": {
-          "description": "This content may be shared with others (default:true)",
-          "type": "boolean"
-        },
-        "autoloop": {
-          "description": "Should the client loop playback at end of content (default:true)",
-          "type": "boolean"
-        },
-        "autostart": {
-          "description": "Should the client automatically start playback of media in this card (default:true)",
-          "type": "boolean"
-        },
-        "aspect": {
-          "description": "Aspect ratio of thumbnail/media placeholder. Allowed values are \"16:9\" and \"4:3\"",
-          "type": "string"
-        },
-        "duration": {
-          "description": "Describes the length of the media content without requiring a receiver to open the content. Formatted as an ISO 8601 Duration field.",
-          "type": "string"
-        },
-        "value": {
-          "description": "Supplementary parameter for this card",
-          "type": "object"
         }
-      }
-    },
-    "BasicCard": {
-      "description": "A basic card",
-      "type": "object",
-      "properties": {
-        "title": {
-          "description": "Title of the card",
-          "type": "string"
-        },
-        "subtitle": {
-          "description": "Subtitle of the card",
-          "type": "string"
-        },
-        "text": {
-          "description": "Text for the card",
-          "type": "string"
-        },
-        "images": {
-          "description": "Array of images for the card",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/CardImage"
+      },
+      "MediaCard": {
+        "description": "Media card",
+        "type": "object",
+        "properties": {
+          "title": {
+            "description": "Title of this card",
+            "type": "string"
+          },
+          "subtitle": {
+            "description": "Subtitle of this card",
+            "type": "string"
+          },
+          "text": {
+            "description": "Text of this card",
+            "type": "string"
+          },
+          "image": {
+            "$ref": "#/definitions/ThumbnailUrl",
+            "description": "Thumbnail placeholder"
+          },
+          "media": {
+            "description": "Media URLs for this card. When this field contains more than one URL, each URL is an alternative format of the same content.",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/MediaUrl"
+            }
+          },
+          "buttons": {
+            "description": "Actions on this card",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/CardAction"
+            }
+          },
+          "shareable": {
+            "description": "This content may be shared with others (default:true)",
+            "type": "boolean"
+          },
+          "autoloop": {
+            "description": "Should the client loop playback at end of content (default:true)",
+            "type": "boolean"
+          },
+          "autostart": {
+            "description": "Should the client automatically start playback of media in this card (default:true)",
+            "type": "boolean"
+          },
+          "aspect": {
+            "description": "Aspect ratio of thumbnail/media placeholder. Allowed values are \"16:9\" and \"4:3\"",
+            "type": "string"
+          },
+          "duration": {
+            "description": "Describes the length of the media content without requiring a receiver to open the content. Formatted as an ISO 8601 Duration field.",
+            "type": "string"
+          },
+          "value": {
+            "description": "Supplementary parameter for this card",
+            "type": "object"
           }
-        },
-        "buttons": {
-          "description": "Set of actions applicable to the current card",
-          "type": "array",
+        }
+      },
+      "ReceiptCard": {
+        "description": "A receipt card",
+        "type": "object",
+        "properties": {
+          "title": {
+            "description": "Title of the card",
+            "type": "string"
+          },
+          "facts": {
+            "description": "Array of Fact objects",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/Fact"
+            }
+          },
           "items": {
-            "$ref": "#/definitions/CardAction"
+            "description": "Array of Receipt Items",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/ReceiptItem"
+            }
+          },
+          "tap": {
+            "$ref": "#/definitions/CardAction",
+            "description": "This action will be activated when user taps on the card"
+          },
+          "total": {
+            "description": "Total amount of money paid (or to be paid)",
+            "type": "string"
+          },
+          "tax": {
+            "description": "Total amount of tax paid (or to be paid)",
+            "type": "string"
+          },
+          "vat": {
+            "description": "Total amount of VAT paid (or to be paid)",
+            "type": "string"
+          },
+          "buttons": {
+            "description": "Set of actions applicable to the current card",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/CardAction"
+            }
           }
-        },
-        "tap": {
-          "$ref": "#/definitions/CardAction",
-          "description": "This action will be activated when user taps on the card itself"
         }
-      }
-    },
-    "MediaCard": {
-      "description": "Media card",
-      "type": "object",
-      "properties": {
-        "title": {
-          "description": "Title of this card",
-          "type": "string"
-        },
-        "subtitle": {
-          "description": "Subtitle of this card",
-          "type": "string"
-        },
-        "text": {
-          "description": "Text of this card",
-          "type": "string"
-        },
-        "image": {
-          "$ref": "#/definitions/ThumbnailUrl",
-          "description": "Thumbnail placeholder"
-        },
-        "media": {
-          "description": "Media URLs for this card. When this field contains more than one URL, each URL is an alternative format of the same content.",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/MediaUrl"
+      },
+      "Fact": {
+        "description": "Set of key-value pairs. Advantage of this section is that key and value properties will be \r\nrendered with default style information with some delimiter between them. So there is no need for developer to specify style information.",
+        "type": "object",
+        "properties": {
+          "key": {
+            "description": "The key for this Fact",
+            "type": "string"
+          },
+          "value": {
+            "description": "The value for this Fact",
+            "type": "string"
           }
-        },
-        "buttons": {
-          "description": "Actions on this card",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/CardAction"
+        }
+      },
+      "ReceiptItem": {
+        "description": "An item on a receipt card",
+        "type": "object",
+        "properties": {
+          "title": {
+            "description": "Title of the Card",
+            "type": "string"
+          },
+          "subtitle": {
+            "description": "Subtitle appears just below Title field, differs from Title in font styling only",
+            "type": "string"
+          },
+          "text": {
+            "description": "Text field appears just below subtitle, differs from Subtitle in font styling only",
+            "type": "string"
+          },
+          "image": {
+            "$ref": "#/definitions/CardImage",
+            "description": "Image"
+          },
+          "price": {
+            "description": "Amount with currency",
+            "type": "string"
+          },
+          "quantity": {
+            "description": "Number of items of given kind",
+            "type": "string"
+          },
+          "tap": {
+            "$ref": "#/definitions/CardAction",
+            "description": "This action will be activated when user taps on the Item bubble."
           }
-        },
-        "shareable": {
-          "description": "This content may be shared with others (default:true)",
-          "type": "boolean"
-        },
-        "autoloop": {
-          "description": "Should the client loop playback at end of content (default:true)",
-          "type": "boolean"
-        },
-        "autostart": {
-          "description": "Should the client automatically start playback of media in this card (default:true)",
-          "type": "boolean"
-        },
-        "aspect": {
-          "description": "Aspect ratio of thumbnail/media placeholder. Allowed values are \"16:9\" and \"4:3\"",
-          "type": "string"
-        },
-        "duration": {
-          "description": "Describes the length of the media content without requiring a receiver to open the content. Formatted as an ISO 8601 Duration field.",
-          "type": "string"
-        },
-        "value": {
-          "description": "Supplementary parameter for this card",
-          "type": "object"
         }
-      }
-    },
-    "ReceiptCard": {
-      "description": "A receipt card",
-      "type": "object",
-      "properties": {
-        "title": {
-          "description": "Title of the card",
-          "type": "string"
-        },
-        "facts": {
-          "description": "Array of Fact objects",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/Fact"
+      },
+      "SigninCard": {
+        "description": "A card representing a request to sign in",
+        "type": "object",
+        "properties": {
+          "text": {
+            "description": "Text for signin request",
+            "type": "string"
+          },
+          "buttons": {
+            "description": "Action to use to perform signin",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/CardAction"
+            }
           }
-        },
-        "items": {
-          "description": "Array of Receipt Items",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/ReceiptItem"
+        }
+      },
+      "OAuthCard": {
+        "description": "A card representing a request to perform a sign in via OAuth",
+        "type": "object",
+        "properties": {
+          "text": {
+            "description": "Text for signin request",
+            "type": "string"
+          },
+          "connectionName": {
+            "description": "The name of the registered connection",
+            "type": "string"
+          },
+          "buttons": {
+            "description": "Action to use to perform signin",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/CardAction"
+            }
           }
-        },
-        "tap": {
-          "$ref": "#/definitions/CardAction",
-          "description": "This action will be activated when user taps on the card"
-        },
-        "total": {
-          "description": "Total amount of money paid (or to be paid)",
-          "type": "string"
-        },
-        "tax": {
-          "description": "Total amount of tax paid (or to be paid)",
-          "type": "string"
-        },
-        "vat": {
-          "description": "Total amount of VAT paid (or to be paid)",
-          "type": "string"
-        },
-        "buttons": {
-          "description": "Set of actions applicable to the current card",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/CardAction"
+        }
+      },
+      "ThumbnailCard": {
+        "description": "A thumbnail card (card with a single, small thumbnail image)",
+        "type": "object",
+        "properties": {
+          "title": {
+            "description": "Title of the card",
+            "type": "string"
+          },
+          "subtitle": {
+            "description": "Subtitle of the card",
+            "type": "string"
+          },
+          "text": {
+            "description": "Text for the card",
+            "type": "string"
+          },
+          "images": {
+            "description": "Array of images for the card",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/CardImage"
+            }
+          },
+          "buttons": {
+            "description": "Set of actions applicable to the current card",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/CardAction"
+            }
+          },
+          "tap": {
+            "$ref": "#/definitions/CardAction",
+            "description": "This action will be activated when user taps on the card itself"
           }
         }
-      }
-    },
-    "Fact": {
-      "description": "Set of key-value pairs. Advantage of this section is that key and value properties will be \r\nrendered with default style information with some delimiter between them. So there is no need for developer to specify style information.",
-      "type": "object",
-      "properties": {
-        "key": {
-          "description": "The key for this Fact",
-          "type": "string"
-        },
-        "value": {
-          "description": "The value for this Fact",
-          "type": "string"
+      },
+      "VideoCard": {
+        "description": "Video card",
+        "type": "object",
+        "properties": {
+          "title": {
+            "description": "Title of this card",
+            "type": "string"
+          },
+          "subtitle": {
+            "description": "Subtitle of this card",
+            "type": "string"
+          },
+          "text": {
+            "description": "Text of this card",
+            "type": "string"
+          },
+          "image": {
+            "$ref": "#/definitions/ThumbnailUrl",
+            "description": "Thumbnail placeholder"
+          },
+          "media": {
+            "description": "Media URLs for this card. When this field contains more than one URL, each URL is an alternative format of the same content.",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/MediaUrl"
+            }
+          },
+          "buttons": {
+            "description": "Actions on this card",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/CardAction"
+            }
+          },
+          "shareable": {
+            "description": "This content may be shared with others (default:true)",
+            "type": "boolean"
+          },
+          "autoloop": {
+            "description": "Should the client loop playback at end of content (default:true)",
+            "type": "boolean"
+          },
+          "autostart": {
+            "description": "Should the client automatically start playback of media in this card (default:true)",
+            "type": "boolean"
+          },
+          "aspect": {
+            "description": "Aspect ratio of thumbnail/media placeholder. Allowed values are \"16:9\" and \"4:3\"",
+            "type": "string"
+          },
+          "duration": {
+            "description": "Describes the length of the media content without requiring a receiver to open the content. Formatted as an ISO 8601 Duration field.",
+            "type": "string"
+          },
+          "value": {
+            "description": "Supplementary parameter for this card",
+            "type": "object"
+          }
         }
-      }
-    },
-    "ReceiptItem": {
-      "description": "An item on a receipt card",
-      "type": "object",
-      "properties": {
-        "title": {
-          "description": "Title of the Card",
-          "type": "string"
-        },
-        "subtitle": {
-          "description": "Subtitle appears just below Title field, differs from Title in font styling only",
-          "type": "string"
-        },
-        "text": {
-          "description": "Text field appears just below subtitle, differs from Subtitle in font styling only",
-          "type": "string"
-        },
-        "image": {
-          "$ref": "#/definitions/CardImage",
-          "description": "Image"
-        },
-        "price": {
-          "description": "Amount with currency",
-          "type": "string"
-        },
-        "quantity": {
-          "description": "Number of items of given kind",
-          "type": "string"
-        },
-        "tap": {
-          "$ref": "#/definitions/CardAction",
-          "description": "This action will be activated when user taps on the Item bubble."
+      },
+      "GeoCoordinates": {
+        "description": "GeoCoordinates (entity type: \"https://schema.org/GeoCoordinates\")",
+        "type": "object",
+        "properties": {
+          "elevation": {
+            "format": "double",
+            "description": "Elevation of the location [WGS 84](https://en.wikipedia.org/wiki/World_Geodetic_System)",
+            "type": "number"
+          },
+          "latitude": {
+            "format": "double",
+            "description": "Latitude of the location [WGS 84](https://en.wikipedia.org/wiki/World_Geodetic_System)",
+            "type": "number"
+          },
+          "longitude": {
+            "format": "double",
+            "description": "Longitude of the location [WGS 84](https://en.wikipedia.org/wiki/World_Geodetic_System)",
+            "type": "number"
+          },
+          "type": {
+            "description": "The type of the thing",
+            "type": "string"
+          },
+          "name": {
+            "description": "The name of the thing",
+            "type": "string"
+          }
         }
-      }
-    },
-    "SigninCard": {
-      "description": "A card representing a request to sign in",
-      "type": "object",
-      "properties": {
-        "text": {
-          "description": "Text for signin request",
-          "type": "string"
-        },
-        "buttons": {
-          "description": "Action to use to perform signin",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/CardAction"
+      },
+      "Mention": {
+        "description": "Mention information (entity type: \"mention\")",
+        "type": "object",
+        "properties": {
+          "mentioned": {
+            "$ref": "#/definitions/ChannelAccount",
+            "description": "The mentioned user"
+          },
+          "text": {
+            "description": "Sub Text which represents the mention (can be null or empty)",
+            "type": "string"
+          },
+          "type": {
+            "description": "Type of this entity (RFC 3987 IRI)",
+            "type": "string"
           }
         }
-      }
-    },
-    "OAuthCard": {
-      "description": "A card representing a request to perform a sign in via OAuth",
-      "type": "object",
-      "properties": {
-        "text": {
-          "description": "Text for signin request",
-          "type": "string"
-        },
-        "connectionName": {
-          "description": "The name of the registered connection",
-          "type": "string"
-        },
-        "buttons": {
-          "description": "Action to use to perform signin",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/CardAction"
+      },
+      "Place": {
+        "description": "Place (entity type: \"https://schema.org/Place\")",
+        "type": "object",
+        "properties": {
+          "address": {
+            "description": "Address of the place (may be `string` or complex object of type `PostalAddress`)",
+            "type": "object"
+          },
+          "geo": {
+            "description": "Geo coordinates of the place (may be complex object of type `GeoCoordinates` or `GeoShape`)",
+            "type": "object"
+          },
+          "hasMap": {
+            "description": "Map to the place (may be `string` (URL) or complex object of type `Map`)",
+            "type": "object"
+          },
+          "type": {
+            "description": "The type of the thing",
+            "type": "string"
+          },
+          "name": {
+            "description": "The name of the thing",
+            "type": "string"
           }
         }
-      }
-    },
-    "ThumbnailCard": {
-      "description": "A thumbnail card (card with a single, small thumbnail image)",
-      "type": "object",
-      "properties": {
-        "title": {
-          "description": "Title of the card",
-          "type": "string"
-        },
-        "subtitle": {
-          "description": "Subtitle of the card",
-          "type": "string"
-        },
-        "text": {
-          "description": "Text for the card",
-          "type": "string"
-        },
-        "images": {
-          "description": "Array of images for the card",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/CardImage"
+      },
+      "Thing": {
+        "description": "Thing (entity type: \"https://schema.org/Thing\")",
+        "type": "object",
+        "properties": {
+          "type": {
+            "description": "The type of the thing",
+            "type": "string"
+          },
+          "name": {
+            "description": "The name of the thing",
+            "type": "string"
           }
-        },
-        "buttons": {
-          "description": "Set of actions applicable to the current card",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/CardAction"
+        }
+      },
+      "MediaEventValue": {
+        "description": "Supplementary parameter for media events",
+        "type": "object",
+        "properties": {
+          "cardValue": {
+            "description": "Callback parameter specified in the Value field of the MediaCard that originated this event",
+            "type": "object"
           }
-        },
-        "tap": {
-          "$ref": "#/definitions/CardAction",
-          "description": "This action will be activated when user taps on the card itself"
         }
-      }
-    },
-    "VideoCard": {
-      "description": "Video card",
-      "type": "object",
-      "properties": {
-        "title": {
-          "description": "Title of this card",
-          "type": "string"
-        },
-        "subtitle": {
-          "description": "Subtitle of this card",
-          "type": "string"
-        },
-        "text": {
-          "description": "Text of this card",
-          "type": "string"
-        },
-        "image": {
-          "$ref": "#/definitions/ThumbnailUrl",
-          "description": "Thumbnail placeholder"
-        },
-        "media": {
-          "description": "Media URLs for this card. When this field contains more than one URL, each URL is an alternative format of the same content.",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/MediaUrl"
+      },
+      "TokenRequest": {
+        "description": "A request to receive a user token",
+        "type": "object",
+        "properties": {
+          "provider": {
+            "description": "The provider to request a user token from",
+            "type": "string"
+          },
+          "settings": {
+            "description": "A collection of settings for the specific provider for this request",
+            "type": "object",
+            "additionalProperties": {
+              "type": "object"
+            }
           }
-        },
-        "buttons": {
-          "description": "Actions on this card",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/CardAction"
+        }
+      },
+      "TokenResponse": {
+        "description": "A response that includes a user token",
+        "type": "object",
+        "properties": {
+          "channelId": {
+            "description": "The channelId of the TokenResponse",
+            "type": "string"
+          },
+          "connectionName": {
+            "description": "The connection name",
+            "type": "string"
+          },
+          "token": {
+            "description": "The user token",
+            "type": "string"
+          },
+          "expiration": {
+            "description": "Expiration for the token, in ISO 8601 format (e.g. \"2007-04-05T14:30Z\")",
+            "type": "string"
           }
-        },
-        "shareable": {
-          "description": "This content may be shared with others (default:true)",
-          "type": "boolean"
-        },
-        "autoloop": {
-          "description": "Should the client loop playback at end of content (default:true)",
-          "type": "boolean"
-        },
-        "autostart": {
-          "description": "Should the client automatically start playback of media in this card (default:true)",
-          "type": "boolean"
-        },
-        "aspect": {
-          "description": "Aspect ratio of thumbnail/media placeholder. Allowed values are \"16:9\" and \"4:3\"",
-          "type": "string"
-        },
-        "duration": {
-          "description": "Describes the length of the media content without requiring a receiver to open the content. Formatted as an ISO 8601 Duration field.",
-          "type": "string"
-        },
-        "value": {
-          "description": "Supplementary parameter for this card",
-          "type": "object"
         }
-      }
-    },
-    "GeoCoordinates": {
-      "description": "GeoCoordinates (entity type: \"https://schema.org/GeoCoordinates\")",
-      "type": "object",
-      "properties": {
-        "elevation": {
-          "format": "double",
-          "description": "Elevation of the location [WGS 84](https://en.wikipedia.org/wiki/World_Geodetic_System)",
-          "type": "number"
-        },
-        "latitude": {
-          "format": "double",
-          "description": "Latitude of the location [WGS 84](https://en.wikipedia.org/wiki/World_Geodetic_System)",
-          "type": "number"
-        },
-        "longitude": {
-          "format": "double",
-          "description": "Longitude of the location [WGS 84](https://en.wikipedia.org/wiki/World_Geodetic_System)",
-          "type": "number"
-        },
-        "type": {
-          "description": "The type of the thing",
-          "type": "string"
-        },
-        "name": {
-          "description": "The name of the thing",
-          "type": "string"
+      },
+      "ActivityTypes": {
+        "description": "Types of Activities",
+        "enum": [
+          "message",
+          "contactRelationUpdate",
+          "conversationUpdate",
+          "typing",
+          "endOfConversation",
+          "event",
+          "invoke",
+          "deleteUserData",
+          "messageUpdate",
+          "messageDelete",
+          "installationUpdate",
+          "messageReaction",
+          "suggestion",
+          "trace",
+          "handoff"
+        ],
+        "type": "string",
+        "properties": {},
+        "x-ms-enum": {
+          "name": "ActivityTypes",
+          "modelAsString": true
         }
-      }
-    },
-    "Mention": {
-      "description": "Mention information (entity type: \"mention\")",
-      "type": "object",
-      "properties": {
-        "mentioned": {
-          "$ref": "#/definitions/ChannelAccount",
-          "description": "The mentioned user"
-        },
-        "text": {
-          "description": "Sub Text which represents the mention (can be null or empty)",
-          "type": "string"
-        },
-        "type": {
-          "description": "Type of this entity (RFC 3987 IRI)",
-          "type": "string"
+      },
+      "AttachmentLayoutTypes": {
+        "description": "Attachment layout types",
+        "enum": [
+          "list",
+          "carousel"
+        ],
+        "type": "string",
+        "properties": {},
+        "x-ms-enum": {
+          "name": "AttachmentLayoutTypes",
+          "modelAsString": true
+        }
+      },
+      "SemanticActionStates": {
+        "description": "Indicates whether the semantic action is starting, continuing, or done",
+        "enum": [
+          "start",
+          "continue",
+          "done"
+        ],
+        "type": "string",
+        "properties": {},
+        "x-ms-enum": {
+          "name": "SemanticActionStates",
+          "modelAsString": true
+        }
+      },
+      "ActionTypes": {
+        "description": "Defines action types for clickable buttons.",
+        "enum": [
+          "openUrl",
+          "imBack",
+          "postBack",
+          "playAudio",
+          "playVideo",
+          "showImage",
+          "downloadFile",
+          "signin",
+          "call",
+          "payment",
+          "messageBack"
+        ],
+        "type": "string",
+        "properties": {},
+        "x-ms-enum": {
+          "name": "ActionTypes",
+          "modelAsString": true
+        }
+      },
+      "ContactRelationUpdateActionTypes": {
+        "description": "Action types valid for ContactRelationUpdate activities",
+        "enum": [
+          "add",
+          "remove"
+        ],
+        "type": "string",
+        "properties": {},
+        "x-ms-enum": {
+          "name": "ContactRelationUpdateActionTypes",
+          "modelAsString": true
+        }
+      },
+      "InstallationUpdateActionTypes": {
+        "description": "Action types valid for InstallationUpdate activities",
+        "enum": [
+          "add",
+          "remove"
+        ],
+        "type": "string",
+        "properties": {},
+        "x-ms-enum": {
+          "name": "InstallationUpdateActionTypes",
+          "modelAsString": true
+        }
+      },
+      "MessageReactionTypes": {
+        "description": "Message reaction types",
+        "enum": [
+          "like",
+          "plusOne"
+        ],
+        "type": "string",
+        "properties": {},
+        "x-ms-enum": {
+          "name": "MessageReactionTypes",
+          "modelAsString": true
         }
-      }
-    },
-    "Place": {
-      "description": "Place (entity type: \"https://schema.org/Place\")",
-      "type": "object",
-      "properties": {
-        "address": {
-          "description": "Address of the place (may be `string` or complex object of type `PostalAddress`)",
-          "type": "object"
-        },
-        "geo": {
-          "description": "Geo coordinates of the place (may be complex object of type `GeoCoordinates` or `GeoShape`)",
-          "type": "object"
-        },
-        "hasMap": {
-          "description": "Map to the place (may be `string` (URL) or complex object of type `Map`)",
-          "type": "object"
-        },
-        "type": {
-          "description": "The type of the thing",
-          "type": "string"
-        },
-        "name": {
-          "description": "The name of the thing",
-          "type": "string"
+      },
+      "TextFormatTypes": {
+        "description": "Text format types",
+        "enum": [
+          "markdown",
+          "plain",
+          "xml"
+        ],
+        "type": "string",
+        "properties": {},
+        "x-ms-enum": {
+          "name": "TextFormatTypes",
+          "modelAsString": true
         }
-      }
-    },
-    "Thing": {
-      "description": "Thing (entity type: \"https://schema.org/Thing\")",
-      "type": "object",
-      "properties": {
-        "type": {
-          "description": "The type of the thing",
-          "type": "string"
-        },
-        "name": {
-          "description": "The name of the thing",
-          "type": "string"
+      },
+      "InputHints": {
+        "description": "Indicates whether the bot is accepting, expecting, or ignoring input",
+        "enum": [
+          "acceptingInput",
+          "ignoringInput",
+          "expectingInput"
+        ],
+        "type": "string",
+        "properties": {},
+        "x-ms-enum": {
+          "name": "InputHints",
+          "modelAsString": true
         }
-      }
-    },
-    "MediaEventValue": {
-      "description": "Supplementary parameter for media events",
-      "type": "object",
-      "properties": {
-        "cardValue": {
-          "description": "Callback parameter specified in the Value field of the MediaCard that originated this event",
-          "type": "object"
+      },
+      "EndOfConversationCodes": {
+        "description": "Codes indicating why a conversation has ended",
+        "enum": [
+          "unknown",
+          "completedSuccessfully",
+          "userCancelled",
+          "botTimedOut",
+          "botIssuedInvalidMessage",
+          "channelFailed"
+        ],
+        "type": "string",
+        "properties": {},
+        "x-ms-enum": {
+          "name": "EndOfConversationCodes",
+          "modelAsString": true
         }
-      }
-    },
-    "TokenRequest": {
-      "description": "A request to receive a user token",
-      "type": "object",
-      "properties": {
-        "provider": {
-          "description": "The provider to request a user token from",
-          "type": "string"
-        },
-        "settings": {
-          "description": "A collection of settings for the specific provider for this request",
-          "type": "object",
-          "additionalProperties": {
-            "type": "object"
-          }
+      },
+      "ActivityImportance": {
+        "description": "Defines the importance of an Activity",
+        "enum": [
+          "low",
+          "normal",
+          "high"
+        ],
+        "type": "string",
+        "properties": {},
+        "x-ms-enum": {
+          "name": "ActivityImportance",
+          "modelAsString": true
         }
-      }
-    },
-    "TokenResponse": {
-      "description": "A response that includes a user token",
-      "type": "object",
-      "properties": {
-        "channelId": {
-          "description": "The channelId of the TokenResponse",
-          "type": "string"
-        },
-        "connectionName": {
-          "description": "The connection name",
-          "type": "string"
-        },
-        "token": {
-          "description": "The user token",
-          "type": "string"
-        },
-        "expiration": {
-          "description": "Expiration for the token, in ISO 8601 format (e.g. \"2007-04-05T14:30Z\")",
-          "type": "string"
+      },
+      "RoleTypes": {
+        "description": "Role of the entity behind the account (Example: User, Bot, Skill, etc.)",
+        "enum": [
+          "user",
+          "bot",
+          "skill"
+        ],
+        "type": "string",
+        "properties": {},
+        "x-ms-enum": {
+          "name": "RoleTypes",
+          "modelAsString": true
         }
-      }
-    },
-    "ActivityTypes": {
-      "description": "Types of Activities",
-      "enum": [
-        "message",
-        "contactRelationUpdate",
-        "conversationUpdate",
-        "typing",
-        "endOfConversation",
-        "event",
-        "invoke",
-        "deleteUserData",
-        "messageUpdate",
-        "messageDelete",
-        "installationUpdate",
-        "messageReaction",
-        "suggestion",
-        "trace",
-        "handoff"
-      ],
-      "type": "string",
-      "properties": {},
-      "x-ms-enum": {
-        "name": "ActivityTypes",
-        "modelAsString": true
-      }
-    },
-    "AttachmentLayoutTypes": {
-      "description": "Attachment layout types",
-      "enum": [
-        "list",
-        "carousel"
-      ],
-      "type": "string",
-      "properties": {},
-      "x-ms-enum": {
-        "name": "AttachmentLayoutTypes",
-        "modelAsString": true
-      }
-    },
-    "SemanticActionStates": {
-      "description": "Indicates whether the semantic action is starting, continuing, or done",
-      "enum": [
-        "start",
-        "continue",
-        "done"
-      ],
-      "type": "string",
-      "properties": {},
-      "x-ms-enum": {
-        "name": "SemanticActionStates",
-        "modelAsString": true
-      }
-    },
-    "ActionTypes": {
-      "description": "Defines action types for clickable buttons.",
-      "enum": [
-        "openUrl",
-        "imBack",
-        "postBack",
-        "playAudio",
-        "playVideo",
-        "showImage",
-        "downloadFile",
-        "signin",
-        "call",
-        "payment",
-        "messageBack"
-      ],
-      "type": "string",
-      "properties": {},
-      "x-ms-enum": {
-        "name": "ActionTypes",
-        "modelAsString": true
-      }
-    },
-    "ContactRelationUpdateActionTypes": {
-      "description": "Action types valid for ContactRelationUpdate activities",
-      "enum": [
-        "add",
-        "remove"
-      ],
-      "type": "string",
-      "properties": {},
-      "x-ms-enum": {
-        "name": "ContactRelationUpdateActionTypes",
-        "modelAsString": true
-      }
-    },
-    "InstallationUpdateActionTypes": {
-      "description": "Action types valid for InstallationUpdate activities",
-      "enum": [
-        "add",
-        "remove"
-      ],
-      "type": "string",
-      "properties": {},
-      "x-ms-enum": {
-        "name": "InstallationUpdateActionTypes",
-        "modelAsString": true
-      }
-    },
-    "MessageReactionTypes": {
-      "description": "Message reaction types",
-      "enum": [
-        "like",
-        "plusOne"
-      ],
-      "type": "string",
-      "properties": {},
-      "x-ms-enum": {
-        "name": "MessageReactionTypes",
-        "modelAsString": true
-      }
-    },
-    "TextFormatTypes": {
-      "description": "Text format types",
-      "enum": [
-        "markdown",
-        "plain",
-        "xml"
-      ],
-      "type": "string",
-      "properties": {},
-      "x-ms-enum": {
-        "name": "TextFormatTypes",
-        "modelAsString": true
-      }
-    },
-    "InputHints": {
-      "description": "Indicates whether the bot is accepting, expecting, or ignoring input",
-      "enum": [
-        "acceptingInput",
-        "ignoringInput",
-        "expectingInput"
-      ],
-      "type": "string",
-      "properties": {},
-      "x-ms-enum": {
-        "name": "InputHints",
-        "modelAsString": true
-      }
-    },
-    "EndOfConversationCodes": {
-      "description": "Codes indicating why a conversation has ended",
-      "enum": [
-        "unknown",
-        "completedSuccessfully",
-        "userCancelled",
-        "botTimedOut",
-        "botIssuedInvalidMessage",
-        "channelFailed"
-      ],
-      "type": "string",
-      "properties": {},
-      "x-ms-enum": {
-        "name": "EndOfConversationCodes",
-        "modelAsString": true
-      }
-    },
-    "ActivityImportance": {
-      "description": "Defines the importance of an Activity",
-      "enum": [
-        "low",
-        "normal",
-        "high"
-      ],
-      "type": "string",
-      "properties": {},
-      "x-ms-enum": {
-        "name": "ActivityImportance",
-        "modelAsString": true
-      }
-    },
-    "RoleTypes": {
-      "description": "Role of the entity behind the account (Example: User, Bot, etc.)",
-      "enum": [
-        "user",
-        "bot"
-      ],
-      "type": "string",
-      "properties": {},
-      "x-ms-enum": {
-        "name": "RoleTypes",
-        "modelAsString": true
-      }
-    },
-    "DeliveryModes": {
-      "description": "Values for deliveryMode field",
-      "enum": [
-        "normal",
-        "notification"
-      ],
-      "type": "string",
-      "properties": {},
-      "x-ms-enum": {
-        "name": "DeliveryModes",
-        "modelAsString": true
-      }
-    },
-    "MicrosoftPayMethodData": {
-      "description": "W3C Payment Method Data for Microsoft Pay",
-      "type": "object",
-      "properties": {
-        "merchantId": {
-          "description": "Microsoft Pay Merchant ID",
-          "type": "string"
-        },
-        "supportedNetworks": {
-          "description": "Supported payment networks (e.g., \"visa\" and \"mastercard\")",
-          "type": "array",
-          "items": {
+      },
+      "DeliveryModes": {
+        "description": "Values for deliveryMode field",
+        "enum": [
+          "normal",
+          "notification",
+          "expectReplies"
+        ],
+        "type": "string",
+        "properties": {},
+        "x-ms-enum": {
+          "name": "DeliveryModes",
+          "modelAsString": true
+        }
+      },
+      "MicrosoftPayMethodData": {
+        "deprecated": true,
+        "description": "Deprecated. Bot Framework no longer supports payments.",
+        "type": "object",
+        "properties": {
+          "merchantId": {
+            "description": "Microsoft Pay Merchant ID",
             "type": "string"
+          },
+          "supportedNetworks": {
+              "deprecated": true,
+              "description": "Deprecated. Bot Framework no longer supports payments.",
+            "type": "array",
+            "items": {
+              "type": "string"
+            }
+          },
+          "supportedTypes": {
+              "deprecated": true,
+              "description": "Deprecated. Bot Framework no longer supports payments.",
+            "type": "array",
+            "items": {
+              "type": "string"
+            }
           }
-        },
-        "supportedTypes": {
-          "description": "Supported payment types (e.g., \"credit\")",
-          "type": "array",
-          "items": {
+        }
+      },
+      "PaymentAddress": {
+      "deprecated": true,
+      "description": "Deprecated. Bot Framework no longer supports payments.",
+        "type": "object",
+        "properties": {
+          "country": {
+            "description": "This is the CLDR (Common Locale Data Repository) region code. For example, US, GB, CN, or JP",
+            "type": "string"
+          },
+          "addressLine": {
+            "description": "This is the most specific part of the address. It can include, for example, a street name, a house number, apartment number, a rural delivery route, descriptive instructions, or a post office box number.",
+            "type": "array",
+            "items": {
+              "type": "string"
+            }
+          },
+          "region": {
+            "description": "This is the top level administrative subdivision of the country. For example, this can be a state, a province, an oblast, or a prefecture.",
+            "type": "string"
+          },
+          "city": {
+            "description": "This is the city/town portion of the address.",
+            "type": "string"
+          },
+          "dependentLocality": {
+            "description": "This is the dependent locality or sublocality within a city. For example, used for neighborhoods, boroughs, districts, or UK dependent localities.",
+            "type": "string"
+          },
+          "postalCode": {
+            "description": "This is the postal code or ZIP code, also known as PIN code in India.",
+            "type": "string"
+          },
+          "sortingCode": {
+            "description": "This is the sorting code as used in, for example, France.",
+            "type": "string"
+          },
+          "languageCode": {
+            "description": "This is the BCP-47 language code for the address. It's used to determine the field separators and the order of fields when formatting the address for display.",
+            "type": "string"
+          },
+          "organization": {
+            "description": "This is the organization, firm, company, or institution at this address.",
+            "type": "string"
+          },
+          "recipient": {
+            "description": "This is the name of the recipient or contact person.",
+            "type": "string"
+          },
+          "phone": {
+            "description": "This is the phone number of the recipient or contact person.",
             "type": "string"
           }
         }
-      }
-    },
-    "PaymentAddress": {
-      "description": "Address within a Payment Request",
-      "type": "object",
-      "properties": {
-        "country": {
-          "description": "This is the CLDR (Common Locale Data Repository) region code. For example, US, GB, CN, or JP",
-          "type": "string"
-        },
-        "addressLine": {
-          "description": "This is the most specific part of the address. It can include, for example, a street name, a house number, apartment number, a rural delivery route, descriptive instructions, or a post office box number.",
-          "type": "array",
-          "items": {
+      },
+      "PaymentCurrencyAmount": {
+      "deprecated": true,
+      "description": "Deprecated. Bot Framework no longer supports payments.",
+        "type": "object",
+        "properties": {
+          "currency": {
+            "description": "A currency identifier",
+            "type": "string"
+          },
+          "value": {
+            "description": "Decimal monetary value",
+            "type": "string"
+          },
+          "currencySystem": {
+            "description": "Currency system",
             "type": "string"
           }
-        },
-        "region": {
-          "description": "This is the top level administrative subdivision of the country. For example, this can be a state, a province, an oblast, or a prefecture.",
-          "type": "string"
-        },
-        "city": {
-          "description": "This is the city/town portion of the address.",
-          "type": "string"
-        },
-        "dependentLocality": {
-          "description": "This is the dependent locality or sublocality within a city. For example, used for neighborhoods, boroughs, districts, or UK dependent localities.",
-          "type": "string"
-        },
-        "postalCode": {
-          "description": "This is the postal code or ZIP code, also known as PIN code in India.",
-          "type": "string"
-        },
-        "sortingCode": {
-          "description": "This is the sorting code as used in, for example, France.",
-          "type": "string"
-        },
-        "languageCode": {
-          "description": "This is the BCP-47 language code for the address. It's used to determine the field separators and the order of fields when formatting the address for display.",
-          "type": "string"
-        },
-        "organization": {
-          "description": "This is the organization, firm, company, or institution at this address.",
-          "type": "string"
-        },
-        "recipient": {
-          "description": "This is the name of the recipient or contact person.",
-          "type": "string"
-        },
-        "phone": {
-          "description": "This is the phone number of the recipient or contact person.",
-          "type": "string"
-        }
-      }
-    },
-    "PaymentCurrencyAmount": {
-      "description": "Supplies monetary amounts",
-      "type": "object",
-      "properties": {
-        "currency": {
-          "description": "A currency identifier",
-          "type": "string"
-        },
-        "value": {
-          "description": "Decimal monetary value",
-          "type": "string"
-        },
-        "currencySystem": {
-          "description": "Currency system",
-          "type": "string"
         }
-      }
-    },
-    "PaymentDetails": {
-      "description": "Provides information about the requested transaction",
-      "type": "object",
-      "properties": {
-        "total": {
-          "$ref": "#/definitions/PaymentItem",
-          "description": "Contains the total amount of the payment request"
-        },
-        "displayItems": {
-          "description": "Contains line items for the payment request that the user agent may display",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/PaymentItem"
+      },
+      "PaymentDetails": {
+      "deprecated": true,
+      "description": "Deprecated. Bot Framework no longer supports payments.",
+        "type": "object",
+        "properties": {
+          "total": {
+            "$ref": "#/definitions/PaymentItem",
+            "description": "Contains the total amount of the payment request"
+          },
+          "displayItems": {
+            "description": "Contains line items for the payment request that the user agent may display",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/PaymentItem"
+            }
+          },
+          "shippingOptions": {
+            "description": "A sequence containing the different shipping options for the user to choose from",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/PaymentShippingOption"
+            }
+          },
+          "modifiers": {
+            "description": "Contains modifiers for particular payment method identifiers",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/PaymentDetailsModifier"
+            }
+          },
+          "error": {
+            "description": "Error description",
+            "type": "string"
           }
-        },
-        "shippingOptions": {
-          "description": "A sequence containing the different shipping options for the user to choose from",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/PaymentShippingOption"
+        }
+      },
+      "PaymentItem": {
+      "deprecated": true,
+      "description": "Deprecated. Bot Framework no longer supports payments.",
+        "type": "object",
+        "properties": {
+          "label": {
+            "description": "Human-readable description of the item",
+            "type": "string"
+          },
+          "amount": {
+            "$ref": "#/definitions/PaymentCurrencyAmount",
+            "description": "Monetary amount for the item"
+          },
+          "pending": {
+            "description": "When set to true this flag means that the amount field is not final.",
+            "type": "boolean"
           }
-        },
-        "modifiers": {
-          "description": "Contains modifiers for particular payment method identifiers",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/PaymentDetailsModifier"
+        }
+      },
+      "PaymentShippingOption": {
+      "deprecated": true,
+      "description": "Deprecated. Bot Framework no longer supports payments.",
+        "type": "object",
+        "properties": {
+          "id": {
+            "description": "String identifier used to reference this PaymentShippingOption",
+            "type": "string"
+          },
+          "label": {
+            "description": "Human-readable description of the item",
+            "type": "string"
+          },
+          "amount": {
+            "$ref": "#/definitions/PaymentCurrencyAmount",
+            "description": "Contains the monetary amount for the item"
+          },
+          "selected": {
+            "description": "Indicates whether this is the default selected PaymentShippingOption",
+            "type": "boolean"
           }
-        },
-        "error": {
-          "description": "Error description",
-          "type": "string"
         }
-      }
-    },
-    "PaymentItem": {
-      "description": "Indicates what the payment request is for and the value asked for",
-      "type": "object",
-      "properties": {
-        "label": {
-          "description": "Human-readable description of the item",
-          "type": "string"
-        },
-        "amount": {
-          "$ref": "#/definitions/PaymentCurrencyAmount",
-          "description": "Monetary amount for the item"
-        },
-        "pending": {
-          "description": "When set to true this flag means that the amount field is not final.",
-          "type": "boolean"
+      },
+      "PaymentDetailsModifier": {
+      "deprecated": true,
+      "description": "Deprecated. Bot Framework no longer supports payments.",
+        "type": "object",
+        "properties": {
+          "supportedMethods": {
+            "description": "Contains a sequence of payment method identifiers",
+            "type": "array",
+            "items": {
+              "type": "string"
+            }
+          },
+          "total": {
+            "$ref": "#/definitions/PaymentItem",
+            "description": "This value overrides the total field in the PaymentDetails dictionary for the payment method identifiers in the supportedMethods field"
+          },
+          "additionalDisplayItems": {
+            "description": "Provides additional display items that are appended to the displayItems field in the PaymentDetails dictionary for the payment method identifiers in the supportedMethods field",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/PaymentItem"
+            }
+          },
+          "data": {
+            "description": "A JSON-serializable object that provides optional information that might be needed by the supported payment methods",
+            "type": "object"
+          }
         }
-      }
-    },
-    "PaymentShippingOption": {
-      "description": "Describes a shipping option",
-      "type": "object",
-      "properties": {
-        "id": {
-          "description": "String identifier used to reference this PaymentShippingOption",
-          "type": "string"
-        },
-        "label": {
-          "description": "Human-readable description of the item",
-          "type": "string"
-        },
-        "amount": {
-          "$ref": "#/definitions/PaymentCurrencyAmount",
-          "description": "Contains the monetary amount for the item"
-        },
-        "selected": {
-          "description": "Indicates whether this is the default selected PaymentShippingOption",
-          "type": "boolean"
+      },
+      "PaymentMethodData": {
+      "deprecated": true,
+      "description": "Deprecated. Bot Framework no longer supports payments.",
+        "type": "object",
+        "properties": {
+          "supportedMethods": {
+            "description": "Required sequence of strings containing payment method identifiers for payment methods that the merchant web site accepts",
+            "type": "array",
+            "items": {
+              "type": "string"
+            }
+          },
+          "data": {
+            "description": "A JSON-serializable object that provides optional information that might be needed by the supported payment methods",
+            "type": "object"
+          }
         }
-      }
-    },
-    "PaymentDetailsModifier": {
-      "description": "Provides details that modify the PaymentDetails based on payment method identifier",
-      "type": "object",
-      "properties": {
-        "supportedMethods": {
-          "description": "Contains a sequence of payment method identifiers",
-          "type": "array",
-          "items": {
+      },
+      "PaymentOptions": {
+      "deprecated": true,
+      "description": "Deprecated. Bot Framework no longer supports payments.",
+        "type": "object",
+        "properties": {
+          "requestPayerName": {
+            "description": "Indicates whether the user agent should collect and return the payer's name as part of the payment request",
+            "type": "boolean"
+          },
+          "requestPayerEmail": {
+            "description": "Indicates whether the user agent should collect and return the payer's email address as part of the payment request",
+            "type": "boolean"
+          },
+          "requestPayerPhone": {
+            "description": "Indicates whether the user agent should collect and return the payer's phone number as part of the payment request",
+            "type": "boolean"
+          },
+          "requestShipping": {
+            "description": "Indicates whether the user agent should collect and return a shipping address as part of the payment request",
+            "type": "boolean"
+          },
+          "shippingType": {
+            "description": "If requestShipping is set to true, then the shippingType field may be used to influence the way the user agent presents the user interface for gathering the shipping address",
             "type": "string"
           }
-        },
-        "total": {
-          "$ref": "#/definitions/PaymentItem",
-          "description": "This value overrides the total field in the PaymentDetails dictionary for the payment method identifiers in the supportedMethods field"
-        },
-        "additionalDisplayItems": {
-          "description": "Provides additional display items that are appended to the displayItems field in the PaymentDetails dictionary for the payment method identifiers in the supportedMethods field",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/PaymentItem"
-          }
-        },
-        "data": {
-          "description": "A JSON-serializable object that provides optional information that might be needed by the supported payment methods",
-          "type": "object"
         }
-      }
-    },
-    "PaymentMethodData": {
-      "description": "Indicates a set of supported payment methods and any associated payment method specific data for those methods",
-      "type": "object",
-      "properties": {
-        "supportedMethods": {
-          "description": "Required sequence of strings containing payment method identifiers for payment methods that the merchant web site accepts",
-          "type": "array",
-          "items": {
+      },
+      "PaymentRequest": {
+      "deprecated": true,
+      "description": "Deprecated. Bot Framework no longer supports payments.",
+        "type": "object",
+        "properties": {
+          "id": {
+            "description": "ID of this payment request",
+            "type": "string"
+          },
+          "methodData": {
+            "description": "Allowed payment methods for this request",
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/PaymentMethodData"
+            }
+          },
+          "details": {
+            "$ref": "#/definitions/PaymentDetails",
+            "description": "Details for this request"
+          },
+          "options": {
+            "$ref": "#/definitions/PaymentOptions",
+            "description": "Provides information about the options desired for the payment request"
+          },
+          "expires": {
+            "description": "Expiration for this request, in ISO 8601 duration format (e.g., 'P1D')",
             "type": "string"
           }
-        },
-        "data": {
-          "description": "A JSON-serializable object that provides optional information that might be needed by the supported payment methods",
-          "type": "object"
-        }
-      }
-    },
-    "PaymentOptions": {
-      "description": "Provides information about the options desired for the payment request",
-      "type": "object",
-      "properties": {
-        "requestPayerName": {
-          "description": "Indicates whether the user agent should collect and return the payer's name as part of the payment request",
-          "type": "boolean"
-        },
-        "requestPayerEmail": {
-          "description": "Indicates whether the user agent should collect and return the payer's email address as part of the payment request",
-          "type": "boolean"
-        },
-        "requestPayerPhone": {
-          "description": "Indicates whether the user agent should collect and return the payer's phone number as part of the payment request",
-          "type": "boolean"
-        },
-        "requestShipping": {
-          "description": "Indicates whether the user agent should collect and return a shipping address as part of the payment request",
-          "type": "boolean"
-        },
-        "shippingType": {
-          "description": "If requestShipping is set to true, then the shippingType field may be used to influence the way the user agent presents the user interface for gathering the shipping address",
-          "type": "string"
         }
-      }
-    },
-    "PaymentRequest": {
-      "description": "A request to make a payment",
-      "type": "object",
-      "properties": {
-        "id": {
-          "description": "ID of this payment request",
-          "type": "string"
-        },
-        "methodData": {
-          "description": "Allowed payment methods for this request",
-          "type": "array",
-          "items": {
-            "$ref": "#/definitions/PaymentMethodData"
+      },
+      "PaymentRequestComplete": {
+      "deprecated": true,
+      "description": "Deprecated. Bot Framework no longer supports payments.",
+        "type": "object",
+        "properties": {
+          "id": {
+            "description": "Payment request ID",
+            "type": "string"
+          },
+          "paymentRequest": {
+            "$ref": "#/definitions/PaymentRequest",
+            "description": "Initial payment request"
+          },
+          "paymentResponse": {
+            "$ref": "#/definitions/PaymentResponse",
+            "description": "Corresponding payment response"
           }
-        },
-        "details": {
-          "$ref": "#/definitions/PaymentDetails",
-          "description": "Details for this request"
-        },
-        "options": {
-          "$ref": "#/definitions/PaymentOptions",
-          "description": "Provides information about the options desired for the payment request"
-        },
-        "expires": {
-          "description": "Expiration for this request, in ISO 8601 duration format (e.g., 'P1D')",
-          "type": "string"
         }
-      }
-    },
-    "PaymentRequestComplete": {
-      "description": "Payload delivered when completing a payment request",
-      "type": "object",
-      "properties": {
-        "id": {
-          "description": "Payment request ID",
-          "type": "string"
-        },
-        "paymentRequest": {
-          "$ref": "#/definitions/PaymentRequest",
-          "description": "Initial payment request"
-        },
-        "paymentResponse": {
-          "$ref": "#/definitions/PaymentResponse",
-          "description": "Corresponding payment response"
+      },
+      "PaymentResponse": {
+      "deprecated": true,
+      "description": "Deprecated. Bot Framework no longer supports payments.",
+        "type": "object",
+        "properties": {
+          "methodName": {
+            "description": "The payment method identifier for the payment method that the user selected to fulfil the transaction",
+            "type": "string"
+          },
+          "details": {
+            "description": "A JSON-serializable object that provides a payment method specific message used by the merchant to process the transaction and determine successful fund transfer",
+            "type": "object"
+          },
+          "shippingAddress": {
+            "$ref": "#/definitions/PaymentAddress",
+            "description": "If the requestShipping flag was set to true in the PaymentOptions passed to the PaymentRequest constructor, then shippingAddress will be the full and final shipping address chosen by the user"
+          },
+          "shippingOption": {
+            "description": "If the requestShipping flag was set to true in the PaymentOptions passed to the PaymentRequest constructor, then shippingOption will be the id attribute of the selected shipping option",
+            "type": "string"
+          },
+          "payerEmail": {
+            "description": "If the requestPayerEmail flag was set to true in the PaymentOptions passed to the PaymentRequest constructor, then payerEmail will be the email address chosen by the user",
+            "type": "string"
+          },
+          "payerPhone": {
+            "description": "If the requestPayerPhone flag was set to true in the PaymentOptions passed to the PaymentRequest constructor, then payerPhone will be the phone number chosen by the user",
+            "type": "string"
+          }
         }
-      }
-    },
-    "PaymentResponse": {
-      "description": "A PaymentResponse is returned when a user has selected a payment method and approved a payment request",
-      "type": "object",
-      "properties": {
-        "methodName": {
-          "description": "The payment method identifier for the payment method that the user selected to fulfil the transaction",
-          "type": "string"
-        },
-        "details": {
-          "description": "A JSON-serializable object that provides a payment method specific message used by the merchant to process the transaction and determine successful fund transfer",
-          "type": "object"
-        },
-        "shippingAddress": {
-          "$ref": "#/definitions/PaymentAddress",
-          "description": "If the requestShipping flag was set to true in the PaymentOptions passed to the PaymentRequest constructor, then shippingAddress will be the full and final shipping address chosen by the user"
-        },
-        "shippingOption": {
-          "description": "If the requestShipping flag was set to true in the PaymentOptions passed to the PaymentRequest constructor, then shippingOption will be the id attribute of the selected shipping option",
-          "type": "string"
-        },
-        "payerEmail": {
-          "description": "If the requestPayerEmail flag was set to true in the PaymentOptions passed to the PaymentRequest constructor, then payerEmail will be the email address chosen by the user",
-          "type": "string"
-        },
-        "payerPhone": {
-          "description": "If the requestPayerPhone flag was set to true in the PaymentOptions passed to the PaymentRequest constructor, then payerPhone will be the phone number chosen by the user",
-          "type": "string"
+      },
+      "PaymentRequestCompleteResult": {
+      "deprecated": true,
+      "description": "Deprecated. Bot Framework no longer supports payments.",
+        "type": "object",
+        "properties": {
+          "result": {
+            "description": "Result of the payment request completion",
+            "type": "string"
+          }
         }
-      }
-    },
-    "PaymentRequestCompleteResult": {
-      "description": "Result from a completed payment request",
-      "type": "object",
-      "properties": {
-        "result": {
-          "description": "Result of the payment request completion",
-          "type": "string"
+      },
+      "PaymentRequestUpdate": {
+      "deprecated": true,
+      "description": "Deprecated. Bot Framework no longer supports payments.",
+        "type": "object",
+        "properties": {
+          "id": {
+            "description": "ID for the payment request to update",
+            "type": "string"
+          },
+          "details": {
+            "$ref": "#/definitions/PaymentDetails",
+            "description": "Update payment details"
+          },
+          "shippingAddress": {
+            "$ref": "#/definitions/PaymentAddress",
+            "description": "Updated shipping address"
+          },
+          "shippingOption": {
+            "description": "Updated shipping options",
+            "type": "string"
+          }
         }
-      }
-    },
-    "PaymentRequestUpdate": {
-      "description": "An update to a payment request",
-      "type": "object",
-      "properties": {
-        "id": {
-          "description": "ID for the payment request to update",
-          "type": "string"
-        },
-        "details": {
-          "$ref": "#/definitions/PaymentDetails",
-          "description": "Update payment details"
-        },
-        "shippingAddress": {
-          "$ref": "#/definitions/PaymentAddress",
-          "description": "Updated shipping address"
-        },
-        "shippingOption": {
-          "description": "Updated shipping options",
-          "type": "string"
+      },
+      "PaymentRequestUpdateResult": {
+      "deprecated": true,
+      "description": "Deprecated. Bot Framework no longer supports payments.",
+        "type": "object",
+        "properties": {
+          "details": {
+            "$ref": "#/definitions/PaymentDetails",
+            "description": "Update payment details"
+          }
         }
       }
     },
-    "PaymentRequestUpdateResult": {
-      "description": "A result object from a Payment Request Update invoke operation",
-      "type": "object",
-      "properties": {
-        "details": {
-          "$ref": "#/definitions/PaymentDetails",
-          "description": "Update payment details"
-        }
+    "securityDefinitions": {
+      "bearer_auth": {
+        "type": "apiKey",
+        "description": "Access token to authenticate calls to the Bot Connector Service.",
+        "name": "Authorization",
+        "in": "header"
       }
     }
-  },
-  "securityDefinitions": {
-    "bearer_auth": {
-      "type": "apiKey",
-      "description": "Access token to authenticate calls to the Bot Connector Service.",
-      "name": "Authorization",
-      "in": "header"
-    }
-  }
-}
\ No newline at end of file
+  }
\ No newline at end of file
diff --git a/libraries/swagger/TokenAPI.json b/libraries/swagger/TokenAPI.json
index 76f1dc0bb..8e848793c 100644
--- a/libraries/swagger/TokenAPI.json
+++ b/libraries/swagger/TokenAPI.json
@@ -60,7 +60,7 @@
         ],
         "responses": {
           "200": {
-            "description": "",
+            "description": "The operation succeeded.",
             "schema": {
               "type": "string"
             }
@@ -68,6 +68,55 @@
         }
       }
     },
+    "/api/botsignin/GetSignInResource": {
+      "get": {
+        "tags": [
+          "BotSignIn"
+        ],
+        "operationId": "BotSignIn_GetSignInResource",
+        "consumes": [],
+        "produces": [
+          "application/json",
+          "text/json",
+          "application/xml",
+          "text/xml"
+        ],
+        "parameters": [
+          {
+            "name": "state",
+            "in": "query",
+            "required": true,
+            "type": "string"
+          },
+          {
+            "name": "code_challenge",
+            "in": "query",
+            "required": false,
+            "type": "string"
+          },
+          {
+            "name": "emulatorUrl",
+            "in": "query",
+            "required": false,
+            "type": "string"
+          },
+          {
+            "name": "finalRedirect",
+            "in": "query",
+            "required": false,
+            "type": "string"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "",
+            "schema": {
+              "$ref": "#/definitions/SignInUrlResponse"
+            }
+          }
+        }
+      }
+    },
     "/api/usertoken/GetToken": {
       "get": {
         "tags": [
@@ -293,9 +342,109 @@
           }
         }
       }
+    },
+    "/api/usertoken/exchange": {
+      "post": {
+        "tags": [
+          "UserToken"
+        ],
+        "operationId": "UserToken_ExchangeAsync",
+        "consumes": [
+          "application/json",
+          "text/json",
+          "application/xml",
+          "text/xml",
+          "application/x-www-form-urlencoded"
+        ],
+        "produces": [
+          "application/json",
+          "text/json",
+          "application/xml",
+          "text/xml"
+        ],
+        "parameters": [
+          {
+            "name": "userId",
+            "in": "query",
+            "required": true,
+            "type": "string"
+          },
+          {
+            "name": "connectionName",
+            "in": "query",
+            "required": true,
+            "type": "string"
+          },
+          {
+            "name": "channelId",
+            "in": "query",
+            "required": true,
+            "type": "string"
+          },
+          {
+            "name": "exchangeRequest",
+            "in": "body",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/TokenExchangeRequest"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "A Token Response object will be returned\r\n",
+            "schema": {
+              "$ref": "#/definitions/TokenResponse"
+            }
+          },
+          "400": {
+            "description": "",
+            "schema": {
+              "$ref": "#/definitions/ErrorResponse"
+            }
+          },
+          "404": {
+            "description": "Resource was not found\r\n",
+            "schema": {
+              "$ref": "#/definitions/TokenResponse"
+            }
+          },
+          "default": {
+            "description": "The operation failed and the response is an error object describing the status code and failure.",
+            "schema": {
+              "$ref": "#/definitions/ErrorResponse"
+            }
+          }
+        }
+      }
     }
   },
   "definitions": {
+    "SignInUrlResponse": {
+      "type": "object",
+      "properties": {
+        "signInLink": {
+          "type": "string"
+        },
+        "tokenExchangeResource": {
+          "$ref": "#/definitions/TokenExchangeResource"
+        }
+      }
+    },
+    "TokenExchangeResource": {
+      "type": "object",
+      "properties": {
+        "id": {
+          "type": "string"
+        },
+        "uri": {
+          "type": "string"
+        },
+        "providerId": {
+          "type": "string"
+        }
+      }
+    },
     "TokenResponse": {
       "type": "object",
       "properties": {
@@ -383,6 +532,17 @@
           "type": "string"
         }
       }
+    },
+    "TokenExchangeRequest": {
+      "type": "object",
+      "properties": {
+        "uri": {
+          "type": "string"
+        },
+        "token": {
+          "type": "string"
+        }
+      }
     }
   },
   "securityDefinitions": {
diff --git a/pipelines/botbuilder-python-ci-slack-test.yml b/pipelines/botbuilder-python-ci-slack-test.yml
new file mode 100644
index 000000000..7e540187b
--- /dev/null
+++ b/pipelines/botbuilder-python-ci-slack-test.yml
@@ -0,0 +1,105 @@
+#
+# Runs functional tests against the Slack channel.
+#
+
+# "name" here defines the build number format. Build number is accessed via $(Build.BuildNumber)
+name: $(Build.BuildId)
+
+pool:
+  vmImage: $[ coalesce( variables['VMImage'], 'windows-2019' ) ] # or 'windows-latest' or 'vs2017-win2016'
+
+trigger: # ci trigger
+  batch: true
+  branches:
+    include:
+    - main
+  paths:
+    include:
+    - '*'
+    exclude:
+    - doc/
+    - specs/
+    - LICENSE
+    - README.md
+    - UsingTestPyPI.md
+
+pr: # pr trigger
+  branches:
+    include:
+    - main
+  paths:
+    include:
+    - pipelines/botbuilder-python-ci-slack-test.yml
+
+variables:
+  AppId: $(SlackTestBotAppId)
+  AppSecret: $(SlackTestBotAppSecret)
+  BotGroup: $(SlackTestBotBotGroup)
+  BotName: $(SlackTestBotBotName)
+  SlackBotToken: $(SlackTestBotSlackBotToken)
+  SlackClientSigningSecret: $(SlackTestBotSlackClientSigningSecret)
+  SlackVerificationToken: $(SlackTestBotSlackVerificationToken)
+#  AzureSubscription: define this in Azure
+#  SlackTestBotAppId: define this in Azure
+#  SlackTestBotAppSecret: define this in Azure
+#  SlackTestBotBotGroup: define this in Azure
+#  SlackTestBotBotName: define this in Azure
+#  SlackTestBotSlackBotToken: define this in Azure
+#  SlackTestBotSlackChannel: define this in Azure
+#  SlackTestBotSlackClientSigningSecret: define this in Azure
+#  SlackTestBotSlackVerificationToken: define this in Azure
+#  DeleteResourceGroup: (optional) define in Azure
+
+steps:
+- powershell: 'gci env:* | sort-object name | Format-Table -AutoSize -Wrap'
+  displayName: 'Display env vars'
+
+- task: AzureCLI@2
+  displayName: 'Create Azure resources'
+  inputs:
+    azureSubscription: $(AzureSubscription)
+    scriptType: pscore
+    scriptLocation: inlineScript
+    inlineScript: |
+      Set-PSDebug -Trace 1;
+      # set up resource group, bot channels registration, app service, app service plan
+      az deployment sub create --name "$(BotName)" --template-file "$(System.DefaultWorkingDirectory)/libraries/functional-tests/slacktestbot/deploymentTemplates/template-with-new-rg.json" --location "westus" --parameters groupName="$(BotGroup)" appId="$(AppId)" appSecret="$(AppSecret)" botId="$(BotName)" botSku="F0" newAppServicePlanName="$(BotName)" newWebAppName="$(BotName)" slackVerificationToken="$(SlackVerificationToken)" slackBotToken="$(SlackBotToken)" slackClientSigningSecret="$(SlackClientSigningSecret)" groupLocation="westus" newAppServicePlanLocation="westus";
+      Set-PSDebug -Trace 0;
+
+- powershell: |
+    7z a -tzip "$(System.DefaultWorkingDirectory)/libraries/functional-tests/slacktestbot/bot.zip"  "$(System.DefaultWorkingDirectory)/libraries/functional-tests/slacktestbot/*" -aoa
+  displayName: 'Zip Bot'
+
+- task: AzureCLI@1
+  displayName: 'Deploy bot'
+  inputs:
+    azureSubscription: $(AzureSubscription)
+    scriptType: ps
+    scriptLocation: inlineScript
+    inlineScript: |
+      az webapp deployment source config-zip --resource-group "$(BotGroup)" --name "$(BotName)" --src "$(System.DefaultWorkingDirectory)/libraries/functional-tests/slacktestbot/bot.zip" --timeout 300
+
+- script: |
+    python -m pip install --upgrade pip
+    pip install -r ./libraries/functional-tests/requirements.txt
+    pip install pytest
+  displayName: 'Install test dependencies'
+
+- script: |
+    pytest test_slack_client.py
+  workingDirectory: '$(System.DefaultWorkingDirectory)/libraries/functional-tests/tests/'
+  displayName: Run test
+  env:
+    BotName: $(SlackTestBotBotName)
+    SlackBotToken: $(SlackTestBotSlackBotToken)
+    SlackChannel: $(SlackTestBotSlackChannel)
+    SlackClientSigningSecret: $(SlackTestBotSlackClientSigningSecret)
+    SlackVerificationToken: $(SlackTestBotSlackVerificationToken)
+
+- task: AzureCLI@1
+  displayName: 'Delete resources'
+  inputs:
+    azureSubscription: $(AzureSubscription)
+    scriptLocation: inlineScript
+    inlineScript: 'call az group delete -n "$(BotGroup)" --yes'
+  condition: and(always(), ne(variables['DeleteResourceGroup'], 'false'))
diff --git a/pipelines/botbuilder-python-ci.yml b/pipelines/botbuilder-python-ci.yml
new file mode 100644
index 000000000..89987ffc9
--- /dev/null
+++ b/pipelines/botbuilder-python-ci.yml
@@ -0,0 +1,111 @@
+variables:
+  # Container registry service connection established during pipeline creation
+  CI_PULL_REQUEST: $(System.PullRequest.PullRequestId)
+  COVERALLS_FLAG_NAME: Build \# $(Build.BuildNumber)
+  COVERALLS_GIT_BRANCH: $(Build.SourceBranchName)
+  COVERALLS_GIT_COMMIT: $(Build.SourceVersion)
+  COVERALLS_SERVICE_JOB_ID: $(Build.BuildId)
+  COVERALLS_SERVICE_NAME: python-ci
+  python.36: 3.6.x
+  python.37: 3.7.x
+  python.38: 3.8.x
+  # PythonCoverallsToken: get this from Azure
+
+jobs:
+# Build and publish container
+- job: Build
+#Multi-configuration and multi-agent job options are not exported to YAML. Configure these options using documentation guidance: https://docs.microsoft.com/vsts/pipelines/process/phases
+  pool:
+    name: Hosted Ubuntu 1604
+
+  strategy:
+      matrix:
+        Python36:
+          PYTHON_VERSION: '$(python.36)'
+        Python37:
+          PYTHON_VERSION: '$(python.37)'
+        Python38:
+          PYTHON_VERSION: '$(python.38)'
+      maxParallel: 3
+
+  steps:
+  - powershell: |
+      Get-ChildItem env:* | sort-object name | Format-Table -Autosize -Wrap | Out-String -Width 120
+    displayName: 'Get environment vars'
+
+  - task: UsePythonVersion@0
+    displayName: 'Use Python $(PYTHON_VERSION)'
+    inputs:
+      versionSpec: '$(PYTHON_VERSION)'
+
+  - script: 'sudo ln -s /opt/hostedtoolcache/Python/3.6.9/x64/lib/libpython3.6m.so.1.0 /usr/lib/libpython3.6m.so'
+    displayName: libpython3.6m
+
+  - script: |
+      python -m pip install --upgrade pip
+      pip install -e ./libraries/botbuilder-schema
+      pip install -e ./libraries/botframework-connector
+      pip install -e ./libraries/botbuilder-core
+      pip install -e ./libraries/botbuilder-ai
+      pip install -e ./libraries/botbuilder-applicationinsights
+      pip install -e ./libraries/botbuilder-dialogs
+      pip install -e ./libraries/botbuilder-azure
+      pip install -e ./libraries/botbuilder-testing
+      pip install -e ./libraries/botbuilder-integration-applicationinsights-aiohttp
+      pip install -e ./libraries/botbuilder-adapters-slack
+      pip install -e ./libraries/botbuilder-integration-aiohttp
+      pip install -r ./libraries/botframework-connector/tests/requirements.txt
+      pip install -r ./libraries/botbuilder-core/tests/requirements.txt
+      pip install -r ./libraries/botbuilder-ai/tests/requirements.txt
+      pip install coveralls
+      pip install pylint==2.4.4
+      pip install black==19.10b0
+    displayName: 'Install dependencies'
+
+  - script: |
+      pip install pytest
+      pip install pytest-cov
+      pip install coveralls
+      pytest --junitxml=junit/test-results.$(PYTHON_VERSION).xml --cov-config=.coveragerc --cov --cov-report=xml --cov-report=html --ignore=libraries/functional-tests/tests/test_slack_client.py
+    displayName: Pytest
+
+  - task: PublishCodeCoverageResults@1
+    displayName: 'Publish Test Coverage'
+    inputs:
+      codeCoverageTool: Cobertura
+      summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage.xml'
+      reportDirectory: '$(System.DefaultWorkingDirectory)/**/htmlcov'
+
+  - task: PublishTestResults@2
+    displayName: 'Publish Test Results **/test-results.$(PYTHON_VERSION).xml'
+    inputs:
+      testResultsFiles: '**/test-results.$(PYTHON_VERSION).xml'
+      testRunTitle: 'Python $(PYTHON_VERSION)'
+
+  - script: 'black --check libraries'
+    displayName: 'Check Black compliant'
+
+  - script: 'pylint --rcfile=.pylintrc libraries'
+    displayName: Pylint
+
+  - script: 'COVERALLS_REPO_TOKEN=$(PythonCoverallsToken) coveralls'
+    displayName: 'Push test results to coveralls https://coveralls.io/github/microsoft/botbuilder-python'
+    continueOnError: true
+    condition: and(succeeded(), eq(variables['System.PullRequest.IsFork'], 'false'))
+
+  - powershell: |
+      Set-Location ..
+      Get-ChildItem -Recurse -Force
+
+    displayName: 'Dir workspace'
+    condition: succeededOrFailed()
+
+  - powershell: |
+      # This task copies the code coverage file created by dotnet test into a well known location. In all
+      # checks I've done, dotnet test ALWAYS outputs the coverage file to the temp directory.
+      # My attempts to override this and have it go directly to the CodeCoverage directory have
+      # all failed, so I'm just doing the copy here.  (cmullins)
+
+      Get-ChildItem -Path "$(Build.SourcesDirectory)" -Include "*coverage*" | Copy-Item -Destination "$(Build.ArtifactStagingDirectory)/CodeCoverage"
+    displayName: 'Copy .coverage Files to CodeCoverage folder'
+    continueOnError: true
diff --git a/pipelines/botbuilder-python-functional-test-linux.yml b/pipelines/botbuilder-python-functional-test-linux.yml
new file mode 100644
index 000000000..e9b13f27a
--- /dev/null
+++ b/pipelines/botbuilder-python-functional-test-linux.yml
@@ -0,0 +1,57 @@
+#
+# Run functional test on bot deployed to a Docker Linux environment in Azure.
+#
+pool:
+  vmImage: 'Ubuntu-16.04'
+
+trigger: # ci trigger
+  branches:
+    include:
+     - master
+
+pr: none # no pr trigger
+
+variables:
+  # Container registry service connection established during pipeline creation
+  dockerRegistryServiceConnection: 'NightlyE2E-Acr'
+  azureRmServiceConnection: 'NightlyE2E-RM'
+  dockerFilePath: 'libraries/functional-tests/functionaltestbot/Dockerfile'
+  buildIdTag: $(Build.BuildNumber)
+  webAppName: 'e2epython'
+  containerRegistry: 'nightlye2etest.azurecr.io'
+  imageRepository: 'functionaltestpy'
+  # LinuxTestBotAppId: get this from azure
+  # LinuxTestBotAppSecret: get this from Azure
+
+jobs:
+- job: Build
+  displayName: Build and push bot image
+  continueOnError: false
+  steps:
+  - task: Docker@2
+    displayName: Build and push bot image
+    inputs:
+      command: buildAndPush
+      repository: $(imageRepository)
+      dockerfile: $(dockerFilePath)
+      containerRegistry: $(dockerRegistryServiceConnection)
+      tags: $(buildIdTag)
+
+- job: Deploy
+  displayName: Provision bot container
+  dependsOn:
+  - Build
+  steps:
+  - task: AzureRMWebAppDeployment@4
+    displayName: Python Functional E2E test.
+    inputs:
+      ConnectionType: AzureRM
+      ConnectedServiceName: $(azureRmServiceConnection)
+      appType: webAppContainer
+      WebAppName: $(webAppName)
+      DockerNamespace: $(containerRegistry)
+      DockerRepository: $(imageRepository)
+      DockerImageTag: $(buildIdTag)
+      AppSettings: '-MicrosoftAppId $(LinuxTestBotAppId) -MicrosoftAppPassword $(LinuxTestBotAppSecret) -FLASK_APP /functionaltestbot/app.py -FLASK_DEBUG 1'
+
+      #StartupCommand: 'flask run --host=0.0.0.0 --port=3978'
diff --git a/pipelines/experimental-create-azure-container-registry.yml b/pipelines/experimental-create-azure-container-registry.yml
new file mode 100644
index 000000000..9bf7d4f20
--- /dev/null
+++ b/pipelines/experimental-create-azure-container-registry.yml
@@ -0,0 +1,36 @@
+trigger: none # no ci trigger
+
+pr: none # no pr trigger
+
+pool:
+  vmImage: 'ubuntu-latest'
+
+steps:
+- task: AzurePowerShell@5
+  displayName: 'Create container'
+  inputs:
+    azureSubscription: 'FUSE Temporary (174c5021-8109-4087-a3e2-a1de20420569)'
+    ScriptType: 'InlineScript'
+    Inline: |
+      Set-PSDebug -Trace 1;
+      Write-Host 'blah';
+      Write-Host 'az group create --name NightlyPythonFunctionalTestContainerRegistryRG --location eastus'
+      az group create --name NightlyPythonFunctionalTestContainerRegistryRG --location eastus
+      Write-Host 'az acr create --resource-group NightlyPythonFunctionalTestContainerRegistryRG --name NightlyPythonFunctionalTestContainerRegistry --sku Basic'
+      az acr create --resource-group NightlyPythonFunctionalTestContainerRegistryRG --name NightlyPythonFunctionalTestContainerRegistry --sku Basic
+      az acr login --name NightlyPythonFunctionalTestContainerRegistry
+      docker pull hello-world
+      docker tag hello-world nightlypythonfunctionaltestcontainerregistry.azurecr.io/hello-world:v1
+      docker push nightlypythonfunctionaltestcontainerregistry.azurecr.io/hello-world:v1
+      docker rmi nightlypythonfunctionaltestcontainerregistry.azurecr.io/hello-world:v1
+      az acr repository list --name NightlyPythonFunctionalTestContainerRegistry --output table
+      az acr repository show-tags --name NightlyPythonFunctionalTestContainerRegistry --repository hello-world --output table
+    azurePowerShellVersion: 'LatestVersion'
+
+- script: echo Hello, world!
+  displayName: 'Run a one-line script'
+
+- script: |
+    echo Add other tasks to build, test, and deploy your project.
+    echo See https://aka.ms/yaml
+  displayName: 'Run a multi-line script'
diff --git a/samples/01.console-echo/adapter/console_adapter.py b/samples/01.console-echo/adapter/console_adapter.py
deleted file mode 100644
index 9ee38f065..000000000
--- a/samples/01.console-echo/adapter/console_adapter.py
+++ /dev/null
@@ -1,177 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import datetime
-import asyncio
-import warnings
-from typing import List, Callable
-
-from botbuilder.schema import (
-    Activity,
-    ActivityTypes,
-    ChannelAccount,
-    ConversationAccount,
-    ResourceResponse,
-    ConversationReference,
-)
-from botbuilder.core.turn_context import TurnContext
-from botbuilder.core.bot_adapter import BotAdapter
-
-
-class ConsoleAdapter(BotAdapter):
-    """
-    Lets a user communicate with a bot from a console window.
-
-    :Example:
-    import asyncio
-    from botbuilder.core import ConsoleAdapter
-
-    async def logic(context):
-        await context.send_activity('Hello World!')
-
-    adapter = ConsoleAdapter()
-    loop = asyncio.get_event_loop()
-    if __name__ == "__main__":
-        try:
-            loop.run_until_complete(adapter.process_activity(logic))
-        except KeyboardInterrupt:
-            pass
-        finally:
-            loop.stop()
-            loop.close()
-    """
-
-    def __init__(self, reference: ConversationReference = None):
-        super(ConsoleAdapter, self).__init__()
-
-        self.reference = ConversationReference(
-            channel_id="console",
-            user=ChannelAccount(id="user", name="User1"),
-            bot=ChannelAccount(id="bot", name="Bot"),
-            conversation=ConversationAccount(id="convo1", name="", is_group=False),
-            service_url="",
-        )
-
-        # Warn users to pass in an instance of a ConversationReference, otherwise the parameter will be ignored.
-        if reference is not None and not isinstance(reference, ConversationReference):
-            warnings.warn(
-                "ConsoleAdapter: `reference` argument is not an instance of ConversationReference and will "
-                "be ignored."
-            )
-        else:
-            self.reference.channel_id = getattr(
-                reference, "channel_id", self.reference.channel_id
-            )
-            self.reference.user = getattr(reference, "user", self.reference.user)
-            self.reference.bot = getattr(reference, "bot", self.reference.bot)
-            self.reference.conversation = getattr(
-                reference, "conversation", self.reference.conversation
-            )
-            self.reference.service_url = getattr(
-                reference, "service_url", self.reference.service_url
-            )
-            # The only attribute on self.reference without an initial value is activity_id, so if reference does not
-            # have a value for activity_id, default self.reference.activity_id to None
-            self.reference.activity_id = getattr(reference, "activity_id", None)
-
-        self._next_id = 0
-
-    async def process_activity(self, logic: Callable):
-        """
-        Begins listening to console input.
-        :param logic:
-        :return:
-        """
-        while True:
-            msg = input()
-            if msg is None:
-                pass
-            else:
-                self._next_id += 1
-                activity = Activity(
-                    text=msg,
-                    channel_id="console",
-                    from_property=ChannelAccount(id="user", name="User1"),
-                    recipient=ChannelAccount(id="bot", name="Bot"),
-                    conversation=ConversationAccount(id="Convo1"),
-                    type=ActivityTypes.message,
-                    timestamp=datetime.datetime.now(),
-                    id=str(self._next_id),
-                )
-
-                activity = TurnContext.apply_conversation_reference(
-                    activity, self.reference, True
-                )
-                context = TurnContext(self, activity)
-                await self.run_pipeline(context, logic)
-
-    async def send_activities(self, context: TurnContext, activities: List[Activity]):
-        """
-        Logs a series of activities to the console.
-        :param context:
-        :param activities:
-        :return:
-        """
-        if context is None:
-            raise TypeError(
-                "ConsoleAdapter.send_activities(): `context` argument cannot be None."
-            )
-        if type(activities) != list:
-            raise TypeError(
-                "ConsoleAdapter.send_activities(): `activities` argument must be a list."
-            )
-        if len(activities) == 0:
-            raise ValueError(
-                "ConsoleAdapter.send_activities(): `activities` argument cannot have a length of 0."
-            )
-
-        async def next_activity(i: int):
-            responses = []
-
-            if i < len(activities):
-                responses.append(ResourceResponse())
-                a = activities[i]
-
-                if a.type == "delay":
-                    await asyncio.sleep(a.delay)
-                    await next_activity(i + 1)
-                elif a.type == ActivityTypes.message:
-                    if a.attachments is not None and len(a.attachments) > 0:
-                        append = (
-                            "(1 attachment)"
-                            if len(a.attachments) == 1
-                            else f"({len(a.attachments)} attachments)"
-                        )
-                        print(f"{a.text} {append}")
-                    else:
-                        print(a.text)
-                    await next_activity(i + 1)
-                else:
-                    print(f"[{a.type}]")
-                    await next_activity(i + 1)
-            else:
-                return responses
-
-        await next_activity(0)
-
-    async def delete_activity(
-        self, context: TurnContext, reference: ConversationReference
-    ):
-        """
-        Not supported for the ConsoleAdapter. Calling this method or `TurnContext.delete_activity()`
-        will result an error being returned.
-        :param context:
-        :param reference:
-        :return:
-        """
-        raise NotImplementedError("ConsoleAdapter.delete_activity(): not supported.")
-
-    async def update_activity(self, context: TurnContext, activity: Activity):
-        """
-        Not supported for the ConsoleAdapter. Calling this method or `TurnContext.update_activity()`
-        will result an error being returned.
-        :param context:
-        :param activity:
-        :return:
-        """
-        raise NotImplementedError("ConsoleAdapter.update_activity(): not supported.")
diff --git a/samples/01.console-echo/bot.py b/samples/01.console-echo/bot.py
deleted file mode 100644
index 226f0d963..000000000
--- a/samples/01.console-echo/bot.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from sys import exit
-
-
-class EchoBot:
-    async def on_turn(self, context):
-        # Check to see if this activity is an incoming message.
-        # (It could theoretically be another type of activity.)
-        if context.activity.type == "message" and context.activity.text:
-            # Check to see if the user sent a simple "quit" message.
-            if context.activity.text.lower() == "quit":
-                # Send a reply.
-                await context.send_activity("Bye!")
-                exit(0)
-            else:
-                # Echo the message text back to the user.
-                await context.send_activity(f"I heard you say {context.activity.text}")
diff --git a/samples/01.console-echo/main.py b/samples/01.console-echo/main.py
deleted file mode 100644
index 351ff1879..000000000
--- a/samples/01.console-echo/main.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import asyncio
-from botbuilder.core import TurnContext, ConversationState, UserState, MemoryStorage
-from botbuilder.schema import ActivityTypes
-
-from adapter import ConsoleAdapter
-from bot import EchoBot
-
-# Create adapter
-adapter = ConsoleAdapter()
-bot = EchoBot()
-
-loop = asyncio.get_event_loop()
-
-if __name__ == "__main__":
-    try:
-        # Greet user
-        print("Hi... I'm an echobot. Whatever you say I'll echo back.")
-
-        loop.run_until_complete(adapter.process_activity(bot.on_turn))
-    except KeyboardInterrupt:
-        pass
-    finally:
-        loop.stop()
-        loop.close()
diff --git a/samples/01.console-echo/requirements.txt b/samples/01.console-echo/requirements.txt
deleted file mode 100644
index 7e1c1616d..000000000
--- a/samples/01.console-echo/requirements.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-asyncio>=3.4.3
-botbuilder-core>=4.4.0.b1
-botbuilder-schema>=4.4.0.b1
-botframework-connector>=4.4.0.b1
\ No newline at end of file
diff --git a/samples/06.using-cards/README.md b/samples/06.using-cards/README.md
deleted file mode 100644
index 7a0b31b06..000000000
--- a/samples/06.using-cards/README.md
+++ /dev/null
@@ -1,50 +0,0 @@
-# Using Cards Bot
-
-Bot Framework v4 using cards bot sample
-
-This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a bot that uses rich cards to enhance your bot design.
-
-## PREREQUISITES
-- Python 3.7 or above
-
-## Running the sample
-- Clone the repository
-```bash
-git clone https://github.com/Microsoft/botbuilder-python.git
-```
-- Run `pip install -r requirements.txt` to install all dependencies
-- Run `python app.py`
-- Alternatively to the last command, you can set the file in an environment variable with `set FLASK_APP=app.py` in windows (`export FLASK_APP=app.py` in mac/linux) and then run `flask run --host=127.0.0.1 --port=3978`
-
-### Visual studio code
-- Activate your desired virtual environment
-- Open `botbuilder-python\samples\06.using-cards` folder
-- Bring up a terminal, navigate to `botbuilder-python\samples\06.using-cards` folder
-- In the terminal, type `pip install -r requirements.txt`
-- In the terminal, type `python app.py`
-
-
-## Testing the bot using Bot Framework Emulator
-[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel.
-
-- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases)
-
-### Connect to bot using Bot Framework Emulator
-- Launch Bot Framework Emulator
-- Paste this URL in the emulator window - http://localhost:3978/api/messages
-
-# Adding media to messages
-A message exchange between user and bot can contain media attachments, such as cards, images, video, audio, and files.
-
-There are several different card types supported by Bot Framework including:
-- [Adaptive card](http://adaptivecards.io)
-- [Hero card](https://docs.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-api-reference?view=azure-bot-service-4.0#herocard-object)
-- [Thumbnail card](https://docs.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-api-reference?view=azure-bot-service-4.0#thumbnailcard-object)
-- [More...](https://docs.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-add-rich-cards?view=azure-bot-service-4.0)
-
-# Further reading
-
-- [Azure Bot Service Introduction](https://docs.microsoft.com/en-us/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0)
-- [Bot State](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-storage-concept?view=azure-bot-service-4.0)
-- [Add media to messages](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-add-media-attachments?view=azure-bot-service-4.0&tabs=csharp)
-- [Rich card types](https://docs.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-connector-add-rich-cards?view=azure-bot-service-4.0)
diff --git a/samples/06.using-cards/app.py b/samples/06.using-cards/app.py
deleted file mode 100644
index 98d5bc583..000000000
--- a/samples/06.using-cards/app.py
+++ /dev/null
@@ -1,89 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-"""
-This sample shows how to use different types of rich cards.
-"""
-import asyncio
-import sys
-
-from flask import Flask, request, Response
-from botbuilder.core import (
-    BotFrameworkAdapter,
-    BotFrameworkAdapterSettings,
-    ConversationState,
-    MemoryStorage,
-    TurnContext,
-    UserState,
-)
-from botbuilder.schema import Activity
-
-from dialogs import MainDialog
-from bots import RichCardsBot
-
-LOOP = asyncio.get_event_loop()
-APP = Flask(__name__, instance_relative_config=True)
-APP.config.from_object("config.DefaultConfig")
-
-SETTINGS = BotFrameworkAdapterSettings(
-    APP.config["APP_ID"], APP.config["APP_PASSWORD"]
-)
-ADAPTER = BotFrameworkAdapter(SETTINGS)
-
-# Catch-all for errors.
-async def on_error(context: TurnContext, error: Exception):
-    # This check writes out errors to console log
-    # NOTE: In production environment, you should consider logging this to Azure
-    #       application insights.
-    print(f"\n [on_turn_error]: { error }", file=sys.stderr)
-    # Send a message to the user
-    await context.send_activity("Oops. Something went wrong!")
-    # Clear out state
-    await CONVERSATION_STATE.delete(context)
-
-
-ADAPTER.on_turn_error = on_error
-
-# Create MemoryStorage, UserState and ConversationState
-MEMORY = MemoryStorage()
-
-# Commented out user_state because it's not being used.
-USER_STATE = UserState(MEMORY)
-CONVERSATION_STATE = ConversationState(MEMORY)
-
-
-DIALOG = MainDialog()
-BOT = RichCardsBot(CONVERSATION_STATE, USER_STATE, DIALOG)
-
-
-@APP.route("/api/messages", methods=["POST"])
-def messages():
-    """Main bot message handler."""
-    if "application/json" in request.headers["Content-Type"]:
-        body = request.json
-    else:
-        return Response(status=415)
-
-    activity = Activity().deserialize(body)
-    auth_header = (
-        request.headers["Authorization"] if "Authorization" in request.headers else ""
-    )
-
-    async def aux_func(turn_context):
-        await BOT.on_turn(turn_context)
-
-    try:
-        task = LOOP.create_task(
-            ADAPTER.process_activity(activity, auth_header, aux_func)
-        )
-        LOOP.run_until_complete(task)
-        return Response(status=201)
-    except Exception as exception:
-        raise exception
-
-
-if __name__ == "__main__":
-    try:
-        APP.run(debug=False, port=APP.config["PORT"])  # nosec debug
-    except Exception as exception:
-        raise exception
diff --git a/samples/06.using-cards/bots/dialog_bot.py b/samples/06.using-cards/bots/dialog_bot.py
deleted file mode 100644
index 2702db884..000000000
--- a/samples/06.using-cards/bots/dialog_bot.py
+++ /dev/null
@@ -1,44 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import asyncio
-
-from helpers.dialog_helper import DialogHelper
-from botbuilder.core import ActivityHandler, ConversationState, UserState, TurnContext
-from botbuilder.dialogs import Dialog
-
-
-class DialogBot(ActivityHandler):
-    def __init__(
-        self,
-        conversation_state: ConversationState,
-        user_state: UserState,
-        dialog: Dialog,
-    ):
-        if conversation_state is None:
-            raise Exception(
-                "[DialogBot]: Missing parameter. conversation_state is required"
-            )
-        if user_state is None:
-            raise Exception("[DialogBot]: Missing parameter. user_state is required")
-        if dialog is None:
-            raise Exception("[DialogBot]: Missing parameter. dialog is required")
-
-        self.conversation_state = conversation_state
-        self.user_state = user_state
-        self.dialog = dialog
-        self.dialog_state = self.conversation_state.create_property("DialogState")
-
-    async def on_turn(self, turn_context: TurnContext):
-        await super().on_turn(turn_context)
-
-        # Save any state changes that might have occured during the turn.
-        await self.conversation_state.save_changes(turn_context, False)
-        await self.user_state.save_changes(turn_context, False)
-
-    async def on_message_activity(self, turn_context: TurnContext):
-        await DialogHelper.run_dialog(
-            self.dialog,
-            turn_context,
-            self.conversation_state.create_property("DialogState"),
-        )
diff --git a/samples/06.using-cards/bots/rich_cards_bot.py b/samples/06.using-cards/bots/rich_cards_bot.py
deleted file mode 100644
index d307465a0..000000000
--- a/samples/06.using-cards/bots/rich_cards_bot.py
+++ /dev/null
@@ -1,28 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from botbuilder.core import MessageFactory, TurnContext
-from botbuilder.schema import ChannelAccount
-from .dialog_bot import DialogBot
-
-"""
- RichCardsBot prompts a user to select a Rich Card and then returns the card
- that matches the user's selection.
-"""
-
-
-class RichCardsBot(DialogBot):
-    def __init__(self, conversation_state, user_state, dialog):
-        super().__init__(conversation_state, user_state, dialog)
-
-    async def on_members_added_activity(
-        self, members_added: ChannelAccount, turn_context: TurnContext
-    ):
-        for member in members_added:
-            if member.id != turn_context.activity.recipient.id:
-                reply = MessageFactory.text(
-                    "Welcome to CardBot. "
-                    + "This bot will show you different types of Rich Cards. "
-                    + "Please type anything to get started."
-                )
-                await turn_context.send_activity(reply)
diff --git a/samples/06.using-cards/dialogs/main_dialog.py b/samples/06.using-cards/dialogs/main_dialog.py
deleted file mode 100644
index 1a574e44c..000000000
--- a/samples/06.using-cards/dialogs/main_dialog.py
+++ /dev/null
@@ -1,303 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from botbuilder.core import CardFactory, MessageFactory
-from botbuilder.dialogs import (
-    ComponentDialog,
-    DialogSet,
-    DialogTurnStatus,
-    WaterfallDialog,
-    WaterfallStepContext,
-)
-from botbuilder.dialogs.prompts import TextPrompt, PromptOptions
-from botbuilder.schema import (
-    ActionTypes,
-    Attachment,
-    AnimationCard,
-    AudioCard,
-    HeroCard,
-    VideoCard,
-    ReceiptCard,
-    SigninCard,
-    ThumbnailCard,
-    MediaUrl,
-    CardAction,
-    CardImage,
-    ThumbnailUrl,
-    Fact,
-    ReceiptItem,
-)
-
-from .resources.adaptive_card_example import ADAPTIVE_CARD_CONTENT
-from helpers.activity_helper import create_activity_reply
-
-MAIN_WATERFALL_DIALOG = "mainWaterfallDialog"
-
-
-class MainDialog(ComponentDialog):
-    def __init__(self):
-        super().__init__("MainDialog")
-
-        # Define the main dialog and its related components.
-        self.add_dialog(TextPrompt("TextPrompt"))
-        self.add_dialog(
-            WaterfallDialog(
-                MAIN_WATERFALL_DIALOG, [self.choice_card_step, self.show_card_step]
-            )
-        )
-
-        # The initial child Dialog to run.
-        self.initial_dialog_id = MAIN_WATERFALL_DIALOG
-
-    """
-     1. Prompts the user if the user is not in the middle of a dialog.
-     2. Re-prompts the user when an invalid input is received.
-     """
-
-    async def choice_card_step(self, step_context: WaterfallStepContext):
-        menu_text = (
-            "Which card would you like to see?\n"
-            "(1) Adaptive Card\n"
-            "(2) Animation Card\n"
-            "(3) Audio Card\n"
-            "(4) Hero Card\n"
-            "(5) Receipt Card\n"
-            "(6) Signin Card\n"
-            "(7) Thumbnail Card\n"
-            "(8) Video Card\n"
-            "(9) All Cards"
-        )
-
-        # Prompt the user with the configured PromptOptions.
-        return await step_context.prompt(
-            "TextPrompt", PromptOptions(prompt=MessageFactory.text(menu_text))
-        )
-
-    """
-     Send a Rich Card response to the user based on their choice.
-     self method is only called when a valid prompt response is parsed from the user's response to the ChoicePrompt.
-    """
-
-    async def show_card_step(self, step_context: WaterfallStepContext):
-        response = step_context.result.lower().strip()
-        choice_dict = {
-            "1": [self.create_adaptive_card],
-            "adaptive card": [self.create_adaptive_card],
-            "2": [self.create_animation_card],
-            "animation card": [self.create_animation_card],
-            "3": [self.create_audio_card],
-            "audio card": [self.create_audio_card],
-            "4": [self.create_hero_card],
-            "hero card": [self.create_hero_card],
-            "5": [self.create_receipt_card],
-            "receipt card": [self.create_receipt_card],
-            "6": [self.create_signin_card],
-            "signin card": [self.create_signin_card],
-            "7": [self.create_thumbnail_card],
-            "thumbnail card": [self.create_thumbnail_card],
-            "8": [self.create_video_card],
-            "video card": [self.create_video_card],
-            "9": [
-                self.create_adaptive_card,
-                self.create_animation_card,
-                self.create_audio_card,
-                self.create_hero_card,
-                self.create_receipt_card,
-                self.create_signin_card,
-                self.create_thumbnail_card,
-                self.create_video_card,
-            ],
-            "all cards": [
-                self.create_adaptive_card,
-                self.create_animation_card,
-                self.create_audio_card,
-                self.create_hero_card,
-                self.create_receipt_card,
-                self.create_signin_card,
-                self.create_thumbnail_card,
-                self.create_video_card,
-            ],
-        }
-
-        # Get the functions that will generate the card(s) for our response
-        # If the stripped response from the user is not found in our choice_dict, default to None
-        choice = choice_dict.get(response, None)
-        # If the user's choice was not found, respond saying the bot didn't understand the user's response.
-        if not choice:
-            not_found = create_activity_reply(
-                step_context.context.activity, "Sorry, I didn't understand that. :("
-            )
-            await step_context.context.send_activity(not_found)
-        else:
-            for func in choice:
-                card = func()
-                response = create_activity_reply(
-                    step_context.context.activity, "", "", [card]
-                )
-                await step_context.context.send_activity(response)
-
-        # Give the user instructions about what to do next
-        await step_context.context.send_activity("Type anything to see another card.")
-
-        return await step_context.end_dialog()
-
-    """
-    ======================================
-     Helper functions used to create cards.
-    ======================================
-    """
-
-    # Methods to generate cards
-    def create_adaptive_card(self) -> Attachment:
-        return CardFactory.adaptive_card(ADAPTIVE_CARD_CONTENT)
-
-    def create_animation_card(self) -> Attachment:
-        card = AnimationCard(
-            media=[MediaUrl(url="http://i.giphy.com/Ki55RUbOV5njy.gif")],
-            title="Microsoft Bot Framework",
-            subtitle="Animation Card",
-        )
-        return CardFactory.animation_card(card)
-
-    def create_audio_card(self) -> Attachment:
-        card = AudioCard(
-            media=[MediaUrl(url="http://www.wavlist.com/movies/004/father.wav")],
-            title="I am your father",
-            subtitle="Star Wars: Episode V - The Empire Strikes Back",
-            text="The Empire Strikes Back (also known as Star Wars: Episode V – The Empire Strikes "
-            "Back) is a 1980 American epic space opera film directed by Irvin Kershner. Leigh "
-            "Brackett and Lawrence Kasdan wrote the screenplay, with George Lucas writing the "
-            "film's story and serving as executive producer. The second installment in the "
-            "original Star Wars trilogy, it was produced by Gary Kurtz for Lucasfilm Ltd. and "
-            "stars Mark Hamill, Harrison Ford, Carrie Fisher, Billy Dee Williams, Anthony "
-            "Daniels, David Prowse, Kenny Baker, Peter Mayhew and Frank Oz.",
-            image=ThumbnailUrl(
-                url="https://upload.wikimedia.org/wikipedia/en/3/3c/SW_-_Empire_Strikes_Back.jpg"
-            ),
-            buttons=[
-                CardAction(
-                    type=ActionTypes.open_url,
-                    title="Read more",
-                    value="https://en.wikipedia.org/wiki/The_Empire_Strikes_Back",
-                )
-            ],
-        )
-        return CardFactory.audio_card(card)
-
-    def create_hero_card(self) -> Attachment:
-        card = HeroCard(
-            title="",
-            images=[
-                CardImage(
-                    url="https://sec.ch9.ms/ch9/7ff5/e07cfef0-aa3b-40bb-9baa-7c9ef8ff7ff5/buildreactionbotframework_960.jpg"
-                )
-            ],
-            buttons=[
-                CardAction(
-                    type=ActionTypes.open_url,
-                    title="Get Started",
-                    value="https://docs.microsoft.com/en-us/azure/bot-service/",
-                )
-            ],
-        )
-        return CardFactory.hero_card(card)
-
-    def create_video_card(self) -> Attachment:
-        card = VideoCard(
-            title="Big Buck Bunny",
-            subtitle="by the Blender Institute",
-            text="Big Buck Bunny (code-named Peach) is a short computer-animated comedy film by the Blender "
-            "Institute, part of the Blender Foundation. Like the foundation's previous film Elephants "
-            "Dream, the film was made using Blender, a free software application for animation made by "
-            "the same foundation. It was released as an open-source film under Creative Commons License "
-            "Attribution 3.0.",
-            media=[
-                MediaUrl(
-                    url="http://download.blender.org/peach/bigbuckbunny_movies/"
-                    "BigBuckBunny_320x180.mp4"
-                )
-            ],
-            buttons=[
-                CardAction(
-                    type=ActionTypes.open_url,
-                    title="Learn More",
-                    value="https://peach.blender.org/",
-                )
-            ],
-        )
-        return CardFactory.video_card(card)
-
-    def create_receipt_card(self) -> Attachment:
-        card = ReceiptCard(
-            title="John Doe",
-            facts=[
-                Fact(key="Order Number", value="1234"),
-                Fact(key="Payment Method", value="VISA 5555-****"),
-            ],
-            items=[
-                ReceiptItem(
-                    title="Data Transfer",
-                    price="$38.45",
-                    quantity="368",
-                    image=CardImage(
-                        url="https://github.com/amido/azure-vector-icons/raw/master/"
-                        "renders/traffic-manager.png"
-                    ),
-                ),
-                ReceiptItem(
-                    title="App Service",
-                    price="$45.00",
-                    quantity="720",
-                    image=CardImage(
-                        url="https://github.com/amido/azure-vector-icons/raw/master/"
-                        "renders/cloud-service.png"
-                    ),
-                ),
-            ],
-            tax="$7.50",
-            total="90.95",
-            buttons=[
-                CardAction(
-                    type=ActionTypes.open_url,
-                    title="More Information",
-                    value="https://azure.microsoft.com/en-us/pricing/details/bot-service/",
-                )
-            ],
-        )
-        return CardFactory.receipt_card(card)
-
-    def create_signin_card(self) -> Attachment:
-        card = SigninCard(
-            text="BotFramework Sign-in Card",
-            buttons=[
-                CardAction(
-                    type=ActionTypes.signin,
-                    title="Sign-in",
-                    value="https://login.microsoftonline.com",
-                )
-            ],
-        )
-        return CardFactory.signin_card(card)
-
-    def create_thumbnail_card(self) -> Attachment:
-        card = ThumbnailCard(
-            title="BotFramework Thumbnail Card",
-            subtitle="Your bots — wherever your users are talking",
-            text="Build and connect intelligent bots to interact with your users naturally wherever"
-            " they are, from text/sms to Skype, Slack, Office 365 mail and other popular services.",
-            images=[
-                CardImage(
-                    url="https://sec.ch9.ms/ch9/7ff5/"
-                    "e07cfef0-aa3b-40bb-9baa-7c9ef8ff7ff5/"
-                    "buildreactionbotframework_960.jpg"
-                )
-            ],
-            buttons=[
-                CardAction(
-                    type=ActionTypes.open_url,
-                    title="Get Started",
-                    value="https://docs.microsoft.com/en-us/azure/bot-service/",
-                )
-            ],
-        )
-        return CardFactory.thumbnail_card(card)
diff --git a/samples/06.using-cards/dialogs/resources/adaptive_card_example.py b/samples/06.using-cards/dialogs/resources/adaptive_card_example.py
deleted file mode 100644
index 49cf269b8..000000000
--- a/samples/06.using-cards/dialogs/resources/adaptive_card_example.py
+++ /dev/null
@@ -1,186 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""Example content for an AdaptiveCard."""
-
-ADAPTIVE_CARD_CONTENT = {
-    "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
-    "version": "1.0",
-    "type": "AdaptiveCard",
-    "speak": "Your flight is confirmed for you and 3 other passengers from San Francisco to Amsterdam on Friday, October 10 8:30 AM",
-    "body": [
-        {
-            "type": "TextBlock",
-            "text": "Passengers",
-            "weight": "bolder",
-            "isSubtle": False,
-        },
-        {"type": "TextBlock", "text": "Sarah Hum", "separator": True},
-        {"type": "TextBlock", "text": "Jeremy Goldberg", "spacing": "none"},
-        {"type": "TextBlock", "text": "Evan Litvak", "spacing": "none"},
-        {
-            "type": "TextBlock",
-            "text": "2 Stops",
-            "weight": "bolder",
-            "spacing": "medium",
-        },
-        {
-            "type": "TextBlock",
-            "text": "Fri, October 10 8:30 AM",
-            "weight": "bolder",
-            "spacing": "none",
-        },
-        {
-            "type": "ColumnSet",
-            "separator": True,
-            "columns": [
-                {
-                    "type": "Column",
-                    "width": 1,
-                    "items": [
-                        {
-                            "type": "TextBlock",
-                            "text": "San Francisco",
-                            "isSubtle": True,
-                        },
-                        {
-                            "type": "TextBlock",
-                            "size": "extraLarge",
-                            "color": "accent",
-                            "text": "SFO",
-                            "spacing": "none",
-                        },
-                    ],
-                },
-                {
-                    "type": "Column",
-                    "width": "auto",
-                    "items": [
-                        {"type": "TextBlock", "text": " "},
-                        {
-                            "type": "Image",
-                            "url": "http://messagecardplayground.azurewebsites.net/assets/airplane.png",
-                            "size": "small",
-                            "spacing": "none",
-                        },
-                    ],
-                },
-                {
-                    "type": "Column",
-                    "width": 1,
-                    "items": [
-                        {
-                            "type": "TextBlock",
-                            "horizontalAlignment": "right",
-                            "text": "Amsterdam",
-                            "isSubtle": True,
-                        },
-                        {
-                            "type": "TextBlock",
-                            "horizontalAlignment": "right",
-                            "size": "extraLarge",
-                            "color": "accent",
-                            "text": "AMS",
-                            "spacing": "none",
-                        },
-                    ],
-                },
-            ],
-        },
-        {
-            "type": "TextBlock",
-            "text": "Non-Stop",
-            "weight": "bolder",
-            "spacing": "medium",
-        },
-        {
-            "type": "TextBlock",
-            "text": "Fri, October 18 9:50 PM",
-            "weight": "bolder",
-            "spacing": "none",
-        },
-        {
-            "type": "ColumnSet",
-            "separator": True,
-            "columns": [
-                {
-                    "type": "Column",
-                    "width": 1,
-                    "items": [
-                        {"type": "TextBlock", "text": "Amsterdam", "isSubtle": True},
-                        {
-                            "type": "TextBlock",
-                            "size": "extraLarge",
-                            "color": "accent",
-                            "text": "AMS",
-                            "spacing": "none",
-                        },
-                    ],
-                },
-                {
-                    "type": "Column",
-                    "width": "auto",
-                    "items": [
-                        {"type": "TextBlock", "text": " "},
-                        {
-                            "type": "Image",
-                            "url": "http://messagecardplayground.azurewebsites.net/assets/airplane.png",
-                            "size": "small",
-                            "spacing": "none",
-                        },
-                    ],
-                },
-                {
-                    "type": "Column",
-                    "width": 1,
-                    "items": [
-                        {
-                            "type": "TextBlock",
-                            "horizontalAlignment": "right",
-                            "text": "San Francisco",
-                            "isSubtle": True,
-                        },
-                        {
-                            "type": "TextBlock",
-                            "horizontalAlignment": "right",
-                            "size": "extraLarge",
-                            "color": "accent",
-                            "text": "SFO",
-                            "spacing": "none",
-                        },
-                    ],
-                },
-            ],
-        },
-        {
-            "type": "ColumnSet",
-            "spacing": "medium",
-            "columns": [
-                {
-                    "type": "Column",
-                    "width": "1",
-                    "items": [
-                        {
-                            "type": "TextBlock",
-                            "text": "Total",
-                            "size": "medium",
-                            "isSubtle": True,
-                        }
-                    ],
-                },
-                {
-                    "type": "Column",
-                    "width": 1,
-                    "items": [
-                        {
-                            "type": "TextBlock",
-                            "horizontalAlignment": "right",
-                            "text": "$4,032.54",
-                            "size": "medium",
-                            "weight": "bolder",
-                        }
-                    ],
-                },
-            ],
-        },
-    ],
-}
diff --git a/samples/06.using-cards/helpers/__init__.py b/samples/06.using-cards/helpers/__init__.py
deleted file mode 100644
index 135279f61..000000000
--- a/samples/06.using-cards/helpers/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from . import activity_helper, dialog_helper
-
-__all__ = ["activity_helper", "dialog_helper"]
diff --git a/samples/06.using-cards/helpers/activity_helper.py b/samples/06.using-cards/helpers/activity_helper.py
deleted file mode 100644
index 16188a3ad..000000000
--- a/samples/06.using-cards/helpers/activity_helper.py
+++ /dev/null
@@ -1,44 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from datetime import datetime
-from botbuilder.schema import (
-    Activity,
-    ActivityTypes,
-    Attachment,
-    ChannelAccount,
-    ConversationAccount,
-)
-
-
-def create_activity_reply(
-    activity: Activity,
-    text: str = None,
-    locale: str = None,
-    attachments: [Attachment] = [],
-):
-    attachments_aux = [attachment for attachment in attachments]
-
-    return Activity(
-        type=ActivityTypes.message,
-        timestamp=datetime.utcnow(),
-        from_property=ChannelAccount(
-            id=getattr(activity.recipient, "id", None),
-            name=getattr(activity.recipient, "name", None),
-        ),
-        recipient=ChannelAccount(
-            id=activity.from_property.id, name=activity.from_property.name
-        ),
-        reply_to_id=activity.id,
-        service_url=activity.service_url,
-        channel_id=activity.channel_id,
-        conversation=ConversationAccount(
-            is_group=activity.conversation.is_group,
-            id=activity.conversation.id,
-            name=activity.conversation.name,
-        ),
-        text=text or "",
-        locale=locale or "",
-        attachments=attachments_aux,
-        entities=[],
-    )
diff --git a/samples/06.using-cards/requirements.txt b/samples/06.using-cards/requirements.txt
deleted file mode 100644
index 2d41bcf0e..000000000
--- a/samples/06.using-cards/requirements.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-botbuilder-core>=4.4.0b1
-botbuilder-dialogs>=4.4.0b1
\ No newline at end of file
diff --git a/samples/13.core-bot/README-LUIS.md b/samples/13.core-bot/README-LUIS.md
deleted file mode 100644
index b6b9b925f..000000000
--- a/samples/13.core-bot/README-LUIS.md
+++ /dev/null
@@ -1,216 +0,0 @@
-# Setting up LUIS via CLI:
-
-This README contains information on how to create and deploy a LUIS application. When the bot is ready to be deployed to production, we recommend creating a LUIS Endpoint Resource for usage with your LUIS App.
-
-> _For instructions on how to create a LUIS Application via the LUIS portal, see these Quickstart steps:_
-> 1. _[Quickstart: Create a new app in the LUIS portal][Quickstart-create]_
-> 2. _[Quickstart: Deploy an app in the LUIS portal][Quickstart-deploy]_
-
-  [Quickstart-create]: https://docs.microsoft.com/azure/cognitive-services/luis/get-started-portal-build-app
-  [Quickstart-deploy]:https://docs.microsoft.com/azure/cognitive-services/luis/get-started-portal-deploy-app
-
-## Table of Contents:
-
-- [Prerequisites](#Prerequisites)
-- [Import a new LUIS Application using a local LUIS application](#Import-a-new-LUIS-Application-using-a-local-LUIS-application)
-- [How to create a LUIS Endpoint resource in Azure and pair it with a LUIS Application](#How-to-create-a-LUIS-Endpoint-resource-in-Azure-and-pair-it-with-a-LUIS-Application)
-
-___
-
-## [Prerequisites](#Table-of-Contents):
-
-#### Install Azure CLI >=2.0.61:
-
-Visit the following page to find the correct installer for your OS:
-- https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest
-
-#### Install LUIS CLI >=2.4.0:
-
-Open a CLI of your choice and type the following:
-
-```bash
-npm i -g luis-apis@^2.4.0
-```
-
-#### LUIS portal account:
-
-You should already have a LUIS account with either https://luis.ai, https://eu.luis.ai, or https://au.luis.ai. To determine where to create a LUIS account, consider where you will deploy your LUIS applications, and then place them in [the corresponding region][LUIS-Authoring-Regions].
-
-After you've created your account, you need your [Authoring Key][LUIS-AKey] and a LUIS application ID.
-
-  [LUIS-Authoring-Regions]: https://docs.microsoft.com/azure/cognitive-services/luis/luis-reference-regions#luis-authoring-regions]
-  [LUIS-AKey]: https://docs.microsoft.com/azure/cognitive-services/luis/luis-concept-keys#authoring-key
-
-___
-
-## [Import a new LUIS Application using a local LUIS application](#Table-of-Contents)
-
-### 1. Import the local LUIS application to luis.ai
-
-```bash
-luis import application --region "LuisAppAuthoringRegion" --authoringKey "LuisAuthoringKey" --appName "FlightBooking" --in "./cognitiveModels/FlightBooking.json"
-```
-
-Outputs the following JSON:
-
-```json
-{
-    "id": "########-####-####-####-############",
-    "name": "FlightBooking",
-    "description": "A LUIS model that uses intent and entities.",
-    "culture": "en-us",
-    "usageScenario": "",
-    "domain": "",
-    "versionsCount": 1,
-    "createdDateTime": "2019-03-29T18:32:02Z",
-    "endpoints": {},
-    "endpointHitsCount": 0,
-    "activeVersion": "0.1",
-    "ownerEmail": "bot@contoso.com",
-    "tokenizerVersion": "1.0.0"
-}
-```
-
-For the next step, you'll need the `"id"` value for `--appId` and the `"activeVersion"` value for `--versionId`.
-
-### 2. Train the LUIS Application
-
-```bash
-luis train version --region "LuisAppAuthoringRegion" --authoringKey "LuisAuthoringKey" --appId "LuisAppId" --versionId "LuisAppversion" --wait
-```
-
-### 3. Publish the LUIS Application
-
-```bash
-luis publish version --region "LuisAppAuthoringRegion" --authoringKey "LuisAuthoringKey" --appId "LuisAppId" --versionId "LuisAppversion" --publishRegion "LuisAppPublishRegion"
-```
-
-> `--region` corresponds to the region you _author_ your application in. The regions available for this are "westus", "westeurope" and "australiaeast". 
-> These regions correspond to the three available portals, https://luis.ai, https://eu.luis.ai, or https://au.luis.ai. 
-> `--publishRegion` corresponds to the region of the endpoint you're publishing to, (e.g. "westus", "southeastasia", "westeurope", "brazilsouth"). 
-> See the [reference docs][Endpoint-API] for a list of available publish/endpoint regions.
-
-  [Endpoint-API]: https://westus.dev.cognitive.microsoft.com/docs/services/5819c76f40a6350ce09de1ac/operations/5819c77140a63516d81aee78
-
-Outputs the following:
-
-```json
- {
-    "versionId": "0.1",
-    "isStaging": false,
-    "endpointUrl": "https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/########-####-####-####-############",
-    "region": "westus",
-    "assignedEndpointKey": null,
-    "endpointRegion": "westus",
-    "failedRegions": "",
-    "publishedDateTime": "2019-03-29T18:40:32Z",
-    "directVersionPublish": false
-}
-```
-
-To see how to create an LUIS Cognitive Service Resource in Azure, please see [the next README][README-LUIS]. This Resource should be used when you want to move your bot to production. The instructions will show you how to create and pair the resource with a LUIS Application.
-
-  [README-LUIS]: ./README-LUIS.md
-
-___
-
-## [How to create a LUIS Endpoint resource in Azure and pair it with a LUIS Application](#Table-of-Contents)
-
-### 1. Create a new LUIS Cognitive Services resource on Azure via Azure CLI
-
-> _Note:_ 
-> _If you don't have a Resource Group in your Azure subscription, you can create one through the Azure portal or through using:_
-> ```bash
-> az group create --subscription "AzureSubscriptionGuid" --location "westus" --name "ResourceGroupName"
-> ```
-> _To see a list of valid locations, use `az account list-locations`_
-
-
-```bash
-# Use Azure CLI to create the LUIS Key resource on Azure
-az cognitiveservices account create --kind "luis" --name "NewLuisResourceName" --sku "S0" --location "westus" --subscription "AzureSubscriptionGuid" -g "ResourceGroupName"
-```
-
-The command will output a response similar to the JSON below:
-
-```json
-{
-  "endpoint": "https://westus.api.cognitive.microsoft.com/luis/v2.0",
-  "etag": "\"########-####-####-####-############\"",
-  "id": "/subscriptions/########-####-####-####-############/resourceGroups/ResourceGroupName/providers/Microsoft.CognitiveServices/accounts/NewLuisResourceName",
-  "internalId": "################################",
-  "kind": "luis",
-  "location": "westus",
-  "name": "NewLuisResourceName",
-  "provisioningState": "Succeeded",
-  "resourceGroup": "ResourceGroupName",
-  "sku": {
-    "name": "S0",
-    "tier": null
-  },
-  "tags": null,
-  "type": "Microsoft.CognitiveServices/accounts"
-}
-```
-
-
-
-Take the output from the previous command and create a JSON file in the following format:
-
-```json
-{
-    "azureSubscriptionId": "00000000-0000-0000-0000-000000000000",
-    "resourceGroup": "ResourceGroupName",
-    "accountName": "NewLuisResourceName"
-}
-```
-
-### 2. Retrieve ARM access token via Azure CLI
-
-```bash
-az account get-access-token --subscription "AzureSubscriptionGuid"
-```
-
-This will return an object that looks like this:
-
-```json
-{
-  "accessToken": "eyJ0eXAiOiJKVtokentokentokentokentokeng1dCI6Ik4tbEMwbi05REFMcXdodUhZbkhRNjNHZUNYYyIsItokenI6Ik4tbEMwbi05REFMcXdodUhZbkhRNjNHZUNYYyJ9.eyJhdWQiOiJodHRwczovL21hbmFnZW1lbnQuY29yZS53aW5kb3dzLm5ldC8iLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDcvIiwiaWF0IjoxNTUzODc3MTUwLCJuYmYiOjE1NTM4NzcxNTAsImV4cCI6MTU1Mzg4MTA1MCwiX2NsYWltX25hbWVzIjp7Imdyb3VwcyI6InNyYzEifSwiX2NsYWltX3NvdXJjZXMiOnsic3JjMSI6eyJlbmRwb2ludCI6Imh0dHBzOi8vZ3JhcGgud2luZG93cy5uZXQvNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3L3VzZXJzL2ZmZTQyM2RkLWJhM2YtNDg0Ny04NjgyLWExNTI5MDA4MjM4Ny9nZXRNZW1iZXJPYmplY3RzIn19LCJhY3IiOiIxIiwiYWlvIjoiQVZRQXEvOEtBQUFBeGVUc201NDlhVHg4RE1mMFlRVnhGZmxxOE9RSC9PODR3QktuSmRqV1FqTkkwbmxLYzB0bHJEZzMyMFZ5bWZGaVVBSFBvNUFFUTNHL0FZNDRjdk01T3M0SEt0OVJkcE5JZW9WU0dzd0kvSkk9IiwiYW1yIjpbIndpYSIsIm1mYSJdLCJhcHBpZCI6IjA0YjA3Nzk1LThkZGItNDYxYS1iYmVlLTAyZjllMWJmN2I0NiIsImFwcGlkYWNyIjoiMCIsImRldmljZWlkIjoiNDhmNDVjNjEtMTg3Zi00MjUxLTlmZWItMTllZGFkZmMwMmE3IiwiZmFtaWx5X25hbWUiOiJHdW0iLCJnaXZlbl9uYW1lIjoiU3RldmVuIiwiaXBhZGRyIjoiMTY3LjIyMC4yLjU1IiwibmFtZSI6IlN0ZXZlbiBHdW0iLCJvaWQiOiJmZmU0MjNkZC1iYTNmLTQ4NDctODY4Mi1hMTUyOTAwODIzODciLCJvbnByZW1fc2lkIjoiUy0xLTUtMjEtMjEyNzUyMTE4NC0xNjA0MDEyOTIwLTE4ODc5Mjc1MjctMjYwOTgyODUiLCJwdWlkIjoiMTAwMzdGRkVBMDQ4NjlBNyIsInJoIjoiSSIsInNjcCI6InVzZXJfaW1wZXJzb25hdGlvbiIsInN1YiI6Ik1rMGRNMWszN0U5ckJyMjhieUhZYjZLSU85LXVFQVVkZFVhNWpkSUd1Nk0iLCJ0aWQiOiI3MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDciLCJ1bmlxdWVfbmFtZSI6InN0Z3VtQG1pY3Jvc29mdC5jb20iLCJ1cG4iOiJzdGd1bUBtaWNyb3NvZnQuY29tIiwidXRpIjoiT2w2NGN0TXY4RVNEQzZZQWRqRUFtokenInZlciI6IjEuMCJ9.kFAsEilE0mlS1pcpqxf4rEnRKeYsehyk-gz-zJHUrE__oad3QjgDSBDPrR_ikLdweynxbj86pgG4QFaHURNCeE6SzrbaIrNKw-n9jrEtokenlosOxg_0l2g1LeEUOi5Q4gQREAU_zvSbl-RY6sAadpOgNHtGvz3Rc6FZRITfkckSLmsKAOFoh-aWC6tFKG8P52rtB0qVVRz9tovBeNqkMYL49s9ypduygbXNVwSQhm5JszeWDgrFuVFHBUP_iENCQYGQpEZf_KvjmX1Ur1F9Eh9nb4yI2gFlKncKNsQl-tokenK7-tokentokentokentokentokentokenatoken",
-  "expiresOn": "2200-12-31 23:59:59.999999",
-  "subscription": "AzureSubscriptionGuid",
-  "tenant": "tenant-guid",
-  "tokenType": "Bearer"
-}
-```
-
-The value needed for the next step is the `"accessToken"`.
-
-### 3. Use `luis add appazureaccount` to pair your LUIS resource with a LUIS Application
-
-```bash
-luis add appazureaccount --in "path/to/created/requestBody.json" --appId "LuisAppId" --authoringKey "LuisAuthoringKey" --armToken "accessToken"
-```
-
-If successful, it should yield a response like this:
-
-```json
-{
-  "code": "Success",
-  "message": "Operation Successful"
-}
-```
-
-### 4. See the LUIS Cognitive Services' keys
-
-```bash
-az cognitiveservices account keys list --name "NewLuisResourceName" --subscription "AzureSubscriptionGuid" -g "ResourceGroupName"
-```
-
-This will return an object that looks like this:
-
-```json
-{
-  "key1": "9a69####dc8f####8eb4####399f####",
-  "key2": "####f99e####4b1a####fb3b####6b9f"
-}
-```
\ No newline at end of file
diff --git a/samples/13.core-bot/README.md b/samples/13.core-bot/README.md
deleted file mode 100644
index 01bfd900c..000000000
--- a/samples/13.core-bot/README.md
+++ /dev/null
@@ -1,61 +0,0 @@
-# CoreBot
-
-Bot Framework v4 core bot sample.
-
-This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to:
-
-- Use [LUIS](https://www.luis.ai) to implement core AI capabilities
-- Implement a multi-turn conversation using Dialogs
-- Handle user interruptions for such things as `Help` or `Cancel`
-- Prompt for and validate requests for information from the user
-
-## Prerequisites
-
-This sample **requires** prerequisites in order to run.
-
-### Overview
-
-This bot uses [LUIS](https://www.luis.ai), an AI based cognitive service, to implement language understanding.
-
-### Install Python 3.7
-
-
-### Create a LUIS Application to enable language understanding
-
-LUIS language model setup, training, and application configuration steps can be found [here](https://docs.microsoft.com/azure/bot-service/bot-builder-howto-v4-luis?view=azure-bot-service-4.0&tabs=cs).
-
-If you wish to create a LUIS application via the CLI, these steps can be found in the [README-LUIS.md](README-LUIS.md).
-
-## Running the sample
-- Run `pip install -r requirements.txt` to install all dependencies
-- Update LuisAppId, LuisAPIKey and LuisAPIHostName in `config.py` with the information retrieved from the [LUIS portal](https://www.luis.ai)
-- Run `python app.py`
-- Alternatively to the last command, you can set the file in an environment variable with `set FLASK_APP=app.py` in windows (`export FLASK_APP=app.py` in mac/linux) and then run `flask run --host=127.0.0.1 --port=3978`
-
-
-## Testing the bot using Bot Framework Emulator
-
-[Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel.
-
-- Install the Bot Framework Emulator version 4.3.0 or greater from [here](https://github.com/Microsoft/BotFramework-Emulator/releases)
-
-### Connect to the bot using Bot Framework Emulator
-
-- Launch Bot Framework Emulator
-- Enter a Bot URL of `http://localhost:3978/api/messages`
-
-
-## Further reading
-
-- [Bot Framework Documentation](https://docs.botframework.com)
-- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0)
-- [Dialogs](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-dialog?view=azure-bot-service-4.0)
-- [Gathering Input Using Prompts](https://docs.microsoft.com/azure/bot-service/bot-builder-prompts?view=azure-bot-service-4.0&tabs=csharp)
-- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0)
-- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0)
-- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0)
-- [.NET Core CLI tools](https://docs.microsoft.com/dotnet/core/tools/?tabs=netcore2x)
-- [Azure CLI](https://docs.microsoft.com/cli/azure/?view=azure-cli-latest)
-- [Azure Portal](https://portal.azure.com)
-- [Language Understanding using LUIS](https://docs.microsoft.com/azure/cognitive-services/luis/)
-- [Channels and Bot Connector Service](https://docs.microsoft.com/azure/bot-service/bot-concepts?view=azure-bot-service-4.0)
\ No newline at end of file
diff --git a/samples/13.core-bot/adapter_with_error_handler.py b/samples/13.core-bot/adapter_with_error_handler.py
deleted file mode 100644
index 10aaa238f..000000000
--- a/samples/13.core-bot/adapter_with_error_handler.py
+++ /dev/null
@@ -1,40 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-import sys
-from botbuilder.core import (
-    BotFrameworkAdapter,
-    BotFrameworkAdapterSettings,
-    ConversationState,
-    MessageFactory,
-    TurnContext,
-)
-from botbuilder.schema import InputHints
-
-
-class AdapterWithErrorHandler(BotFrameworkAdapter):
-    def __init__(
-        self,
-        settings: BotFrameworkAdapterSettings,
-        conversation_state: ConversationState,
-    ):
-        super().__init__(settings)
-        self._conversation_state = conversation_state
-
-        # Catch-all for errors.
-        async def on_error(context: TurnContext, error: Exception):
-            # This check writes out errors to console log
-            # NOTE: In production environment, you should consider logging this to Azure
-            #       application insights.
-            print(f"\n [on_turn_error]: {error}", file=sys.stderr)
-
-            # Send a message to the user
-            error_message_text = "Sorry, it looks like something went wrong."
-            error_message = MessageFactory.text(
-                error_message_text, error_message_text, InputHints.expecting_input
-            )
-            await context.send_activity(error_message)
-            # Clear out state
-            nonlocal self
-            await self._conversation_state.delete(context)
-
-        self.on_turn_error = on_error
diff --git a/samples/13.core-bot/app.py b/samples/13.core-bot/app.py
deleted file mode 100644
index bee45cd18..000000000
--- a/samples/13.core-bot/app.py
+++ /dev/null
@@ -1,76 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-"""
-This sample shows how to create a bot that demonstrates the following:
-- Use [LUIS](https://www.luis.ai) to implement core AI capabilities.
-- Implement a multi-turn conversation using Dialogs.
-- Handle user interruptions for such things as `Help` or `Cancel`.
-- Prompt for and validate requests for information from the user.
-"""
-
-import asyncio
-
-from flask import Flask, request, Response
-from botbuilder.core import (
-    BotFrameworkAdapterSettings,
-    ConversationState,
-    MemoryStorage,
-    UserState,
-)
-from botbuilder.schema import Activity
-from dialogs import MainDialog, BookingDialog
-from bots import DialogAndWelcomeBot
-
-from adapter_with_error_handler import AdapterWithErrorHandler
-from flight_booking_recognizer import FlightBookingRecognizer
-
-LOOP = asyncio.get_event_loop()
-APP = Flask(__name__, instance_relative_config=True)
-APP.config.from_object("config.DefaultConfig")
-
-SETTINGS = BotFrameworkAdapterSettings(APP.config["APP_ID"], APP.config["APP_PASSWORD"])
-
-# Create MemoryStorage, UserState and ConversationState
-MEMORY = MemoryStorage()
-USER_STATE = UserState(MEMORY)
-CONVERSATION_STATE = ConversationState(MEMORY)
-ADAPTER = AdapterWithErrorHandler(SETTINGS, CONVERSATION_STATE)
-RECOGNIZER = FlightBookingRecognizer(APP.config)
-BOOKING_DIALOG = BookingDialog()
-
-DIALOG = MainDialog(RECOGNIZER, BOOKING_DIALOG)
-BOT = DialogAndWelcomeBot(CONVERSATION_STATE, USER_STATE, DIALOG)
-
-
-@APP.route("/api/messages", methods=["POST"])
-def messages():
-    """Main bot message handler."""
-    if "application/json" in request.headers["Content-Type"]:
-        body = request.json
-    else:
-        return Response(status=415)
-
-    activity = Activity().deserialize(body)
-    auth_header = (
-        request.headers["Authorization"] if "Authorization" in request.headers else ""
-    )
-
-    async def aux_func(turn_context):
-        await BOT.on_turn(turn_context)
-
-    try:
-        task = LOOP.create_task(
-            ADAPTER.process_activity(activity, auth_header, aux_func)
-        )
-        LOOP.run_until_complete(task)
-        return Response(status=201)
-    except Exception as exception:
-        raise exception
-
-
-if __name__ == "__main__":
-    try:
-        APP.run(debug=False, port=APP.config["PORT"])  # nosec debug
-    except Exception as exception:
-        raise exception
diff --git a/samples/13.core-bot/booking_details.py b/samples/13.core-bot/booking_details.py
deleted file mode 100644
index 24c7a1df8..000000000
--- a/samples/13.core-bot/booking_details.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from typing import List
-
-
-class BookingDetails:
-    def __init__(
-        self,
-        destination: str = None,
-        origin: str = None,
-        travel_date: str = None,
-        unsupported_airports: List[str] = [],
-    ):
-        self.destination = destination
-        self.origin = origin
-        self.travel_date = travel_date
-        self.unsupported_airports = unsupported_airports
diff --git a/samples/13.core-bot/bots/dialog_and_welcome_bot.py b/samples/13.core-bot/bots/dialog_and_welcome_bot.py
deleted file mode 100644
index b392e2e1f..000000000
--- a/samples/13.core-bot/bots/dialog_and_welcome_bot.py
+++ /dev/null
@@ -1,59 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import json
-import os.path
-
-from typing import List
-from botbuilder.core import CardFactory
-from botbuilder.core import (
-    ActivityHandler,
-    ConversationState,
-    MessageFactory,
-    UserState,
-    TurnContext,
-)
-from botbuilder.dialogs import Dialog
-from botbuilder.schema import Activity, Attachment, ChannelAccount
-from helpers.dialog_helper import DialogHelper
-
-from .dialog_bot import DialogBot
-
-
-class DialogAndWelcomeBot(DialogBot):
-    def __init__(
-        self,
-        conversation_state: ConversationState,
-        user_state: UserState,
-        dialog: Dialog,
-    ):
-        super(DialogAndWelcomeBot, self).__init__(
-            conversation_state, user_state, dialog
-        )
-
-    async def on_members_added_activity(
-        self, members_added: List[ChannelAccount], turn_context: TurnContext
-    ):
-        for member in members_added:
-            # Greet anyone that was not the target (recipient) of this message.
-            # To learn more about Adaptive Cards, see https://aka.ms/msbot-adaptivecards for more details.
-            if member.id != turn_context.activity.recipient.id:
-                welcome_card = self.create_adaptive_card_attachment()
-                response = MessageFactory.attachment(welcome_card)
-                await turn_context.send_activity(response)
-                await DialogHelper.run_dialog(
-                    self.dialog,
-                    turn_context,
-                    self.conversation_state.create_property("DialogState"),
-                )
-
-    # Load attachment from file.
-    def create_adaptive_card_attachment(self):
-        relative_path = os.path.abspath(os.path.dirname(__file__))
-        path = os.path.join(relative_path, "../cards/welcomeCard.json")
-        with open(path) as f:
-            card = json.load(f)
-
-        return Attachment(
-            content_type="application/vnd.microsoft.card.adaptive", content=card
-        )
diff --git a/samples/13.core-bot/bots/dialog_bot.py b/samples/13.core-bot/bots/dialog_bot.py
deleted file mode 100644
index fc563d2ec..000000000
--- a/samples/13.core-bot/bots/dialog_bot.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import asyncio
-
-from botbuilder.core import ActivityHandler, ConversationState, UserState, TurnContext
-from botbuilder.dialogs import Dialog
-from helpers.dialog_helper import DialogHelper
-
-
-class DialogBot(ActivityHandler):
-    def __init__(
-        self,
-        conversation_state: ConversationState,
-        user_state: UserState,
-        dialog: Dialog,
-    ):
-        if conversation_state is None:
-            raise Exception(
-                "[DialogBot]: Missing parameter. conversation_state is required"
-            )
-        if user_state is None:
-            raise Exception("[DialogBot]: Missing parameter. user_state is required")
-        if dialog is None:
-            raise Exception("[DialogBot]: Missing parameter. dialog is required")
-
-        self.conversation_state = conversation_state
-        self.user_state = user_state
-        self.dialog = dialog
-
-    async def on_turn(self, turn_context: TurnContext):
-        await super().on_turn(turn_context)
-
-        # Save any state changes that might have occurred during the turn.
-        await self.conversation_state.save_changes(turn_context, False)
-        await self.user_state.save_changes(turn_context, False)
-
-    async def on_message_activity(self, turn_context: TurnContext):
-        await DialogHelper.run_dialog(
-            self.dialog,
-            turn_context,
-            self.conversation_state.create_property("DialogState"),
-        )
diff --git a/samples/13.core-bot/cards/welcomeCard.json b/samples/13.core-bot/cards/welcomeCard.json
deleted file mode 100644
index cc10cda9f..000000000
--- a/samples/13.core-bot/cards/welcomeCard.json
+++ /dev/null
@@ -1,46 +0,0 @@
-{
-  "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
-  "type": "AdaptiveCard",
-  "version": "1.0",
-  "body": [
-    {
-      "type": "Image",
-      "url": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU",
-      "size": "stretch"
-    },
-    {
-      "type": "TextBlock",
-      "spacing": "medium",
-      "size": "default",
-      "weight": "bolder",
-      "text": "Welcome to Bot Framework!",
-      "wrap": true,
-      "maxLines": 0
-    },
-    {
-      "type": "TextBlock",
-      "size": "default",
-      "isSubtle": "true",
-      "text": "Now that you have successfully run your bot, follow the links in this Adaptive Card to expand your knowledge of Bot Framework.",
-      "wrap": true,
-      "maxLines": 0
-    }
-  ],
-  "actions": [
-    {
-      "type": "Action.OpenUrl",
-      "title": "Get an overview",
-      "url": "https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0"
-    },
-    {
-      "type": "Action.OpenUrl",
-      "title": "Ask a question",
-      "url": "https://stackoverflow.com/questions/tagged/botframework"
-    },
-    {
-      "type": "Action.OpenUrl",
-      "title": "Learn how to deploy",
-      "url": "https://docs.microsoft.com/azure/bot-service/bot-builder-howto-deploy-azure?view=azure-bot-service-4.0"
-    }
-  ]
-}
\ No newline at end of file
diff --git a/samples/13.core-bot/cognitiveModels/FlightBooking.json b/samples/13.core-bot/cognitiveModels/FlightBooking.json
deleted file mode 100644
index f0e4b9770..000000000
--- a/samples/13.core-bot/cognitiveModels/FlightBooking.json
+++ /dev/null
@@ -1,339 +0,0 @@
-{
-  "luis_schema_version": "3.2.0",
-  "versionId": "0.1",
-  "name": "FlightBooking",
-  "desc": "Luis Model for CoreBot",
-  "culture": "en-us",
-  "tokenizerVersion": "1.0.0",
-  "intents": [
-    {
-      "name": "BookFlight"
-    },
-    {
-      "name": "Cancel"
-    },
-    {
-      "name": "GetWeather"
-    },
-    {
-      "name": "None"
-    }
-  ],
-  "entities": [],
-  "composites": [
-    {
-      "name": "From",
-      "children": [
-        "Airport"
-      ],
-      "roles": []
-    },
-    {
-      "name": "To",
-      "children": [
-        "Airport"
-      ],
-      "roles": []
-    }
-  ],
-  "closedLists": [
-    {
-      "name": "Airport",
-      "subLists": [
-        {
-          "canonicalForm": "Paris",
-          "list": [
-            "paris",
-            "cdg"
-          ]
-        },
-        {
-          "canonicalForm": "London",
-          "list": [
-            "london",
-            "lhr"
-          ]
-        },
-        {
-          "canonicalForm": "Berlin",
-          "list": [
-            "berlin",
-            "txl"
-          ]
-        },
-        {
-          "canonicalForm": "New York",
-          "list": [
-            "new york",
-            "jfk"
-          ]
-        },
-        {
-          "canonicalForm": "Seattle",
-          "list": [
-            "seattle",
-            "sea"
-          ]
-        }
-      ],
-      "roles": []
-    }
-  ],
-  "patternAnyEntities": [],
-  "regex_entities": [],
-  "prebuiltEntities": [
-    {
-      "name": "datetimeV2",
-      "roles": []
-    }
-  ],
-  "model_features": [],
-  "regex_features": [],
-  "patterns": [],
-  "utterances": [
-    {
-      "text": "book a flight",
-      "intent": "BookFlight",
-      "entities": []
-    },
-    {
-      "text": "book a flight from new york",
-      "intent": "BookFlight",
-      "entities": [
-        {
-          "entity": "From",
-          "startPos": 19,
-          "endPos": 26
-        }
-      ]
-    },
-    {
-      "text": "book a flight from seattle",
-      "intent": "BookFlight",
-      "entities": [
-        {
-          "entity": "From",
-          "startPos": 19,
-          "endPos": 25
-        }
-      ]
-    },
-    {
-      "text": "book a hotel in new york",
-      "intent": "None",
-      "entities": []
-    },
-    {
-      "text": "book a restaurant",
-      "intent": "None",
-      "entities": []
-    },
-    {
-      "text": "book flight from london to paris on feb 14th",
-      "intent": "BookFlight",
-      "entities": [
-        {
-          "entity": "From",
-          "startPos": 17,
-          "endPos": 22
-        },
-        {
-          "entity": "To",
-          "startPos": 27,
-          "endPos": 31
-        }
-      ]
-    },
-    {
-      "text": "book flight to berlin on feb 14th",
-      "intent": "BookFlight",
-      "entities": [
-        {
-          "entity": "To",
-          "startPos": 15,
-          "endPos": 20
-        }
-      ]
-    },
-    {
-      "text": "book me a flight from london to paris",
-      "intent": "BookFlight",
-      "entities": [
-        {
-          "entity": "From",
-          "startPos": 22,
-          "endPos": 27
-        },
-        {
-          "entity": "To",
-          "startPos": 32,
-          "endPos": 36
-        }
-      ]
-    },
-    {
-      "text": "bye",
-      "intent": "Cancel",
-      "entities": []
-    },
-    {
-      "text": "cancel booking",
-      "intent": "Cancel",
-      "entities": []
-    },
-    {
-      "text": "exit",
-      "intent": "Cancel",
-      "entities": []
-    },
-    {
-      "text": "find an airport near me",
-      "intent": "None",
-      "entities": []
-    },
-    {
-      "text": "flight to paris",
-      "intent": "BookFlight",
-      "entities": [
-        {
-          "entity": "To",
-          "startPos": 10,
-          "endPos": 14
-        }
-      ]
-    },
-    {
-      "text": "flight to paris from london on feb 14th",
-      "intent": "BookFlight",
-      "entities": [
-        {
-          "entity": "To",
-          "startPos": 10,
-          "endPos": 14
-        },
-        {
-          "entity": "From",
-          "startPos": 21,
-          "endPos": 26
-        }
-      ]
-    },
-    {
-      "text": "fly from berlin to paris on may 5th",
-      "intent": "BookFlight",
-      "entities": [
-        {
-          "entity": "From",
-          "startPos": 9,
-          "endPos": 14
-        },
-        {
-          "entity": "To",
-          "startPos": 19,
-          "endPos": 23
-        }
-      ]
-    },
-    {
-      "text": "go to paris",
-      "intent": "BookFlight",
-      "entities": [
-        {
-          "entity": "To",
-          "startPos": 6,
-          "endPos": 10
-        }
-      ]
-    },
-    {
-      "text": "going from paris to berlin",
-      "intent": "BookFlight",
-      "entities": [
-        {
-          "entity": "From",
-          "startPos": 11,
-          "endPos": 15
-        },
-        {
-          "entity": "To",
-          "startPos": 20,
-          "endPos": 25
-        }
-      ]
-    },
-    {
-      "text": "i'd like to rent a car",
-      "intent": "None",
-      "entities": []
-    },
-    {
-      "text": "ignore",
-      "intent": "Cancel",
-      "entities": []
-    },
-    {
-      "text": "travel from new york to paris",
-      "intent": "BookFlight",
-      "entities": [
-        {
-          "entity": "From",
-          "startPos": 12,
-          "endPos": 19
-        },
-        {
-          "entity": "To",
-          "startPos": 24,
-          "endPos": 28
-        }
-      ]
-    },
-    {
-      "text": "travel to new york",
-      "intent": "BookFlight",
-      "entities": [
-        {
-          "entity": "To",
-          "startPos": 10,
-          "endPos": 17
-        }
-      ]
-    },
-    {
-      "text": "travel to paris",
-      "intent": "BookFlight",
-      "entities": [
-        {
-          "entity": "To",
-          "startPos": 10,
-          "endPos": 14
-        }
-      ]
-    },
-    {
-      "text": "what's the forecast for this friday?",
-      "intent": "GetWeather",
-      "entities": []
-    },
-    {
-      "text": "what's the weather like for tomorrow",
-      "intent": "GetWeather",
-      "entities": []
-    },
-    {
-      "text": "what's the weather like in new york",
-      "intent": "GetWeather",
-      "entities": []
-    },
-    {
-      "text": "what's the weather like?",
-      "intent": "GetWeather",
-      "entities": []
-    },
-    {
-      "text": "winter is coming",
-      "intent": "None",
-      "entities": []
-    }
-  ],
-  "settings": []
-}
\ No newline at end of file
diff --git a/samples/13.core-bot/dialogs/__init__.py b/samples/13.core-bot/dialogs/__init__.py
deleted file mode 100644
index 567539f96..000000000
--- a/samples/13.core-bot/dialogs/__init__.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from .booking_dialog import BookingDialog
-from .cancel_and_help_dialog import CancelAndHelpDialog
-from .date_resolver_dialog import DateResolverDialog
-from .main_dialog import MainDialog
-
-__all__ = ["BookingDialog", "CancelAndHelpDialog", "DateResolverDialog", "MainDialog"]
diff --git a/samples/13.core-bot/dialogs/booking_dialog.py b/samples/13.core-bot/dialogs/booking_dialog.py
deleted file mode 100644
index 297dff07c..000000000
--- a/samples/13.core-bot/dialogs/booking_dialog.py
+++ /dev/null
@@ -1,142 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from botbuilder.dialogs import WaterfallDialog, WaterfallStepContext, DialogTurnResult
-from botbuilder.dialogs.prompts import ConfirmPrompt, TextPrompt, PromptOptions
-from botbuilder.core import MessageFactory
-from botbuilder.schema import InputHints
-from .cancel_and_help_dialog import CancelAndHelpDialog
-from .date_resolver_dialog import DateResolverDialog
-
-from datatypes_date_time.timex import Timex
-
-
-class BookingDialog(CancelAndHelpDialog):
-    def __init__(self, dialog_id: str = None):
-        super(BookingDialog, self).__init__(dialog_id or BookingDialog.__name__)
-
-        self.add_dialog(TextPrompt(TextPrompt.__name__))
-        self.add_dialog(ConfirmPrompt(ConfirmPrompt.__name__))
-        self.add_dialog(DateResolverDialog(DateResolverDialog.__name__))
-        self.add_dialog(
-            WaterfallDialog(
-                WaterfallDialog.__name__,
-                [
-                    self.destination_step,
-                    self.origin_step,
-                    self.travel_date_step,
-                    self.confirm_step,
-                    self.final_step,
-                ],
-            )
-        )
-
-        self.initial_dialog_id = WaterfallDialog.__name__
-
-    """
-    If a destination city has not been provided, prompt for one.
-    :param step_context:
-    :return DialogTurnResult:
-    """
-
-    async def destination_step(
-        self, step_context: WaterfallStepContext
-    ) -> DialogTurnResult:
-        booking_details = step_context.options
-
-        if booking_details.destination is None:
-            message_text = "Where would you like to travel to?"
-            prompt_message = MessageFactory.text(
-                message_text, message_text, InputHints.expecting_input
-            )
-            return await step_context.prompt(
-                TextPrompt.__name__, PromptOptions(prompt=prompt_message)
-            )
-        return await step_context.next(booking_details.destination)
-
-    """
-    If an origin city has not been provided, prompt for one.
-    :param step_context:
-    :return DialogTurnResult:
-    """
-
-    async def origin_step(self, step_context: WaterfallStepContext) -> DialogTurnResult:
-        booking_details = step_context.options
-
-        # Capture the response to the previous step's prompt
-        booking_details.destination = step_context.result
-        if booking_details.origin is None:
-            message_text = "From what city will you be travelling?"
-            prompt_message = MessageFactory.text(
-                message_text, message_text, InputHints.expecting_input
-            )
-            return await step_context.prompt(
-                TextPrompt.__name__, PromptOptions(prompt=prompt_message)
-            )
-        return await step_context.next(booking_details.origin)
-
-    """
-    If a travel date has not been provided, prompt for one.
-    This will use the DATE_RESOLVER_DIALOG.
-    :param step_context:
-    :return DialogTurnResult:
-    """
-
-    async def travel_date_step(
-        self, step_context: WaterfallStepContext
-    ) -> DialogTurnResult:
-        booking_details = step_context.options
-
-        # Capture the results of the previous step
-        booking_details.origin = step_context.result
-        if not booking_details.travel_date or self.is_ambiguous(
-            booking_details.travel_date
-        ):
-            return await step_context.begin_dialog(
-                DateResolverDialog.__name__, booking_details.travel_date
-            )
-        return await step_context.next(booking_details.travel_date)
-
-    """
-    Confirm the information the user has provided.
-    :param step_context:
-    :return DialogTurnResult:
-    """
-
-    async def confirm_step(
-        self, step_context: WaterfallStepContext
-    ) -> DialogTurnResult:
-        booking_details = step_context.options
-
-        # Capture the results of the previous step
-        booking_details.travel_date = step_context.result
-        message_text = (
-            f"Please confirm, I have you traveling to: { booking_details.destination } from: "
-            f"{ booking_details.origin } on: { booking_details.travel_date}."
-        )
-        prompt_message = MessageFactory.text(
-            message_text, message_text, InputHints.expecting_input
-        )
-
-        # Offer a YES/NO prompt.
-        return await step_context.prompt(
-            ConfirmPrompt.__name__, PromptOptions(prompt=prompt_message)
-        )
-
-    """
-    Complete the interaction and end the dialog.
-    :param step_context:
-    :return DialogTurnResult:
-    """
-
-    async def final_step(self, step_context: WaterfallStepContext) -> DialogTurnResult:
-
-        if step_context.result:
-            booking_details = step_context.options
-
-            return await step_context.end_dialog(booking_details)
-        return await step_context.end_dialog()
-
-    def is_ambiguous(self, timex: str) -> bool:
-        timex_property = Timex(timex)
-        return "definite" not in timex_property.types
diff --git a/samples/13.core-bot/dialogs/cancel_and_help_dialog.py b/samples/13.core-bot/dialogs/cancel_and_help_dialog.py
deleted file mode 100644
index 93c71d7df..000000000
--- a/samples/13.core-bot/dialogs/cancel_and_help_dialog.py
+++ /dev/null
@@ -1,47 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from botbuilder.dialogs import (
-    ComponentDialog,
-    DialogContext,
-    DialogTurnResult,
-    DialogTurnStatus,
-)
-from botbuilder.schema import ActivityTypes, InputHints
-from botbuilder.core import MessageFactory
-
-
-class CancelAndHelpDialog(ComponentDialog):
-    def __init__(self, dialog_id: str):
-        super(CancelAndHelpDialog, self).__init__(dialog_id)
-
-    async def on_continue_dialog(self, inner_dc: DialogContext) -> DialogTurnResult:
-        result = await self.interrupt(inner_dc)
-        if result is not None:
-            return result
-
-        return await super(CancelAndHelpDialog, self).on_continue_dialog(inner_dc)
-
-    async def interrupt(self, inner_dc: DialogContext) -> DialogTurnResult:
-        if inner_dc.context.activity.type == ActivityTypes.message:
-            text = inner_dc.context.activity.text.lower()
-
-            help_message_text = "Show Help..."
-            help_message = MessageFactory.text(
-                help_message_text, help_message_text, InputHints.expecting_input
-            )
-
-            if text == "help" or text == "?":
-                await inner_dc.context.send_activity(help_message)
-                return DialogTurnResult(DialogTurnStatus.Waiting)
-
-            cancel_message_text = "Cancelling"
-            cancel_message = MessageFactory.text(
-                cancel_message_text, cancel_message_text, InputHints.ignoring_input
-            )
-
-            if text == "cancel" or text == "quit":
-                await inner_dc.context.send_activity(cancel_message)
-                return await inner_dc.cancel_all_dialogs()
-
-        return None
diff --git a/samples/13.core-bot/dialogs/date_resolver_dialog.py b/samples/13.core-bot/dialogs/date_resolver_dialog.py
deleted file mode 100644
index a375b6fa4..000000000
--- a/samples/13.core-bot/dialogs/date_resolver_dialog.py
+++ /dev/null
@@ -1,79 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from botbuilder.core import MessageFactory
-from botbuilder.dialogs import WaterfallDialog, DialogTurnResult, WaterfallStepContext
-from botbuilder.dialogs.prompts import (
-    DateTimePrompt,
-    PromptValidatorContext,
-    PromptOptions,
-    DateTimeResolution,
-)
-from botbuilder.schema import InputHints
-from .cancel_and_help_dialog import CancelAndHelpDialog
-
-from datatypes_date_time.timex import Timex
-
-
-class DateResolverDialog(CancelAndHelpDialog):
-    def __init__(self, dialog_id: str = None):
-        super(DateResolverDialog, self).__init__(
-            dialog_id or DateResolverDialog.__name__
-        )
-
-        self.add_dialog(
-            DateTimePrompt(
-                DateTimePrompt.__name__, DateResolverDialog.datetime_prompt_validator
-            )
-        )
-        self.add_dialog(
-            WaterfallDialog(
-                WaterfallDialog.__name__ + "2", [self.initial_step, self.final_step]
-            )
-        )
-
-        self.initial_dialog_id = WaterfallDialog.__name__ + "2"
-
-    async def initial_step(
-        self, step_context: WaterfallStepContext
-    ) -> DialogTurnResult:
-        timex = step_context.options
-
-        prompt_msg_text = "On what date would you like to travel?"
-        prompt_msg = MessageFactory.text(
-            prompt_msg_text, prompt_msg_text, InputHints.expecting_input
-        )
-
-        reprompt_msg_text = "I'm sorry, for best results, please enter your travel date including the month, day and year."
-        reprompt_msg = MessageFactory.text(
-            reprompt_msg_text, reprompt_msg_text, InputHints.expecting_input
-        )
-
-        if timex is None:
-            # We were not given any date at all so prompt the user.
-            return await step_context.prompt(
-                DateTimePrompt.__name__,
-                PromptOptions(prompt=prompt_msg, retry_prompt=reprompt_msg),
-            )
-        # We have a Date we just need to check it is unambiguous.
-        if "definite" not in Timex(timex).types:
-            # This is essentially a "reprompt" of the data we were given up front.
-            return await step_context.prompt(
-                DateTimePrompt.__name__, PromptOptions(prompt=reprompt_msg)
-            )
-
-        return await step_context.next(DateTimeResolution(timex=timex))
-
-    async def final_step(self, step_context: WaterfallStepContext):
-        timex = step_context.result[0].timex
-        return await step_context.end_dialog(timex)
-
-    @staticmethod
-    async def datetime_prompt_validator(prompt_context: PromptValidatorContext) -> bool:
-        if prompt_context.recognized.succeeded:
-            timex = prompt_context.recognized.value[0].timex.split("T")[0]
-
-            # TODO: Needs TimexProperty
-            return "definite" in Timex(timex).types
-
-        return False
diff --git a/samples/13.core-bot/dialogs/main_dialog.py b/samples/13.core-bot/dialogs/main_dialog.py
deleted file mode 100644
index d85242375..000000000
--- a/samples/13.core-bot/dialogs/main_dialog.py
+++ /dev/null
@@ -1,132 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from botbuilder.dialogs import (
-    ComponentDialog,
-    WaterfallDialog,
-    WaterfallStepContext,
-    DialogTurnResult,
-)
-from botbuilder.dialogs.prompts import TextPrompt, PromptOptions
-from botbuilder.core import MessageFactory, TurnContext
-from botbuilder.schema import InputHints
-
-from .booking_dialog import BookingDialog
-from booking_details import BookingDetails
-from flight_booking_recognizer import FlightBookingRecognizer
-from helpers.luis_helper import LuisHelper, Intent
-
-
-class MainDialog(ComponentDialog):
-    def __init__(
-        self, luis_recognizer: FlightBookingRecognizer, booking_dialog: BookingDialog
-    ):
-        super(MainDialog, self).__init__(MainDialog.__name__)
-
-        self._luis_recognizer = luis_recognizer
-        self._booking_dialog_id = booking_dialog.id
-
-        self.add_dialog(TextPrompt(TextPrompt.__name__))
-        self.add_dialog(booking_dialog)
-        self.add_dialog(
-            WaterfallDialog(
-                "WFDialog", [self.intro_step, self.act_step, self.final_step]
-            )
-        )
-
-        self.initial_dialog_id = "WFDialog"
-
-    async def intro_step(self, step_context: WaterfallStepContext) -> DialogTurnResult:
-        if not self._luis_recognizer.is_configured:
-            await step_context.context.send_activity(
-                MessageFactory.text(
-                    "NOTE: LUIS is not configured. To enable all capabilities, add 'LuisAppId', 'LuisAPIKey' and "
-                    "'LuisAPIHostName' to the appsettings.json file.",
-                    input_hint=InputHints.ignoring_input,
-                )
-            )
-
-            return await step_context.next(None)
-        message_text = (
-            str(step_context.options)
-            if step_context.options
-            else "What can I help you with today?"
-        )
-        prompt_message = MessageFactory.text(
-            message_text, message_text, InputHints.expecting_input
-        )
-
-        return await step_context.prompt(
-            TextPrompt.__name__, PromptOptions(prompt=prompt_message)
-        )
-
-    async def act_step(self, step_context: WaterfallStepContext) -> DialogTurnResult:
-        if not self._luis_recognizer.is_configured:
-            # LUIS is not configured, we just run the BookingDialog path with an empty BookingDetailsInstance.
-            return await step_context.begin_dialog(
-                self._booking_dialog_id, BookingDetails()
-            )
-
-        # Call LUIS and gather any potential booking details. (Note the TurnContext has the response to the prompt.)
-        intent, luis_result = await LuisHelper.execute_luis_query(
-            self._luis_recognizer, step_context.context
-        )
-
-        if intent == Intent.BOOK_FLIGHT.value and luis_result:
-            # Show a warning for Origin and Destination if we can't resolve them.
-            await MainDialog._show_warning_for_unsupported_cities(
-                step_context.context, luis_result
-            )
-
-            # Run the BookingDialog giving it whatever details we have from the LUIS call.
-            return await step_context.begin_dialog(self._booking_dialog_id, luis_result)
-
-        elif intent == Intent.GET_WEATHER.value:
-            get_weather_text = "TODO: get weather flow here"
-            get_weather_message = MessageFactory.text(
-                get_weather_text, get_weather_text, InputHints.ignoring_input
-            )
-            await step_context.context.send_activity(get_weather_message)
-
-        else:
-            didnt_understand_text = (
-                "Sorry, I didn't get that. Please try asking in a different way"
-            )
-            didnt_understand_message = MessageFactory.text(
-                didnt_understand_text, didnt_understand_text, InputHints.ignoring_input
-            )
-            await step_context.context.send_activity(didnt_understand_message)
-
-        return await step_context.next(None)
-
-    async def final_step(self, step_context: WaterfallStepContext) -> DialogTurnResult:
-        # If the child dialog ("BookingDialog") was cancelled or the user failed to confirm,
-        # the Result here will be null.
-        if step_context.result is not None:
-            result = step_context.result
-
-            # Now we have all the booking details call the booking service.
-
-            # If the call to the booking service was successful tell the user.
-            # time_property = Timex(result.travel_date)
-            # travel_date_msg = time_property.to_natural_language(datetime.now())
-            msg_txt = f"I have you booked to {result.destination} from {result.origin} on {result.travel_date}"
-            message = MessageFactory.text(msg_txt, msg_txt, InputHints.ignoring_input)
-            await step_context.context.send_activity(message)
-
-        prompt_message = "What else can I do for you?"
-        return await step_context.replace_dialog(self.id, prompt_message)
-
-    @staticmethod
-    async def _show_warning_for_unsupported_cities(
-        context: TurnContext, luis_result: BookingDetails
-    ) -> None:
-        if luis_result.unsupported_airports:
-            message_text = (
-                f"Sorry but the following airports are not supported:"
-                f" {', '.join(luis_result.unsupported_airports)}"
-            )
-            message = MessageFactory.text(
-                message_text, message_text, InputHints.ignoring_input
-            )
-            await context.send_activity(message)
diff --git a/samples/13.core-bot/flight_booking_recognizer.py b/samples/13.core-bot/flight_booking_recognizer.py
deleted file mode 100644
index 7476103c7..000000000
--- a/samples/13.core-bot/flight_booking_recognizer.py
+++ /dev/null
@@ -1,32 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from botbuilder.ai.luis import LuisApplication, LuisRecognizer
-from botbuilder.core import Recognizer, RecognizerResult, TurnContext
-
-
-class FlightBookingRecognizer(Recognizer):
-    def __init__(self, configuration: dict):
-        self._recognizer = None
-
-        luis_is_configured = (
-            configuration["LUIS_APP_ID"]
-            and configuration["LUIS_API_KEY"]
-            and configuration["LUIS_API_HOST_NAME"]
-        )
-        if luis_is_configured:
-            luis_application = LuisApplication(
-                configuration["LUIS_APP_ID"],
-                configuration["LUIS_API_KEY"],
-                "https://" + configuration["LUIS_API_HOST_NAME"],
-            )
-
-            self._recognizer = LuisRecognizer(luis_application)
-
-    @property
-    def is_configured(self) -> bool:
-        # Returns true if luis is configured in the appsettings.json and initialized.
-        return self._recognizer is not None
-
-    async def recognize(self, turn_context: TurnContext) -> RecognizerResult:
-        return await self._recognizer.recognize(turn_context)
diff --git a/samples/13.core-bot/helpers/__init__.py b/samples/13.core-bot/helpers/__init__.py
deleted file mode 100644
index 699f8693c..000000000
--- a/samples/13.core-bot/helpers/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from . import activity_helper, luis_helper, dialog_helper
-
-__all__ = ["activity_helper", "dialog_helper", "luis_helper"]
diff --git a/samples/13.core-bot/helpers/activity_helper.py b/samples/13.core-bot/helpers/activity_helper.py
deleted file mode 100644
index 29a24823e..000000000
--- a/samples/13.core-bot/helpers/activity_helper.py
+++ /dev/null
@@ -1,37 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from datetime import datetime
-from botbuilder.schema import (
-    Activity,
-    ActivityTypes,
-    ChannelAccount,
-    ConversationAccount,
-)
-
-
-def create_activity_reply(activity: Activity, text: str = None, locale: str = None):
-
-    return Activity(
-        type=ActivityTypes.message,
-        timestamp=datetime.utcnow(),
-        from_property=ChannelAccount(
-            id=getattr(activity.recipient, "id", None),
-            name=getattr(activity.recipient, "name", None),
-        ),
-        recipient=ChannelAccount(
-            id=activity.from_property.id, name=activity.from_property.name
-        ),
-        reply_to_id=activity.id,
-        service_url=activity.service_url,
-        channel_id=activity.channel_id,
-        conversation=ConversationAccount(
-            is_group=activity.conversation.is_group,
-            id=activity.conversation.id,
-            name=activity.conversation.name,
-        ),
-        text=text or "",
-        locale=locale or "",
-        attachments=[],
-        entities=[],
-    )
diff --git a/samples/13.core-bot/helpers/luis_helper.py b/samples/13.core-bot/helpers/luis_helper.py
deleted file mode 100644
index fc59d8969..000000000
--- a/samples/13.core-bot/helpers/luis_helper.py
+++ /dev/null
@@ -1,101 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-from enum import Enum
-from typing import Dict
-from botbuilder.ai.luis import LuisRecognizer
-from botbuilder.core import IntentScore, TopIntent, TurnContext
-
-from booking_details import BookingDetails
-
-
-class Intent(Enum):
-    BOOK_FLIGHT = "BookFlight"
-    CANCEL = "Cancel"
-    GET_WEATHER = "GetWeather"
-    NONE_INTENT = "NoneIntent"
-
-
-def top_intent(intents: Dict[Intent, dict]) -> TopIntent:
-    max_intent = Intent.NONE_INTENT
-    max_value = 0.0
-
-    for intent, value in intents:
-        intent_score = IntentScore(value)
-        if intent_score.score > max_value:
-            max_intent, max_value = intent, intent_score.score
-
-    return TopIntent(max_intent, max_value)
-
-
-class LuisHelper:
-    @staticmethod
-    async def execute_luis_query(
-        luis_recognizer: LuisRecognizer, turn_context: TurnContext
-    ) -> (Intent, object):
-        """
-        Returns an object with preformatted LUIS results for the bot's dialogs to consume.
-        """
-        result = None
-        intent = None
-
-        try:
-            recognizer_result = await luis_recognizer.recognize(turn_context)
-
-            intent = (
-                sorted(
-                    recognizer_result.intents,
-                    key=recognizer_result.intents.get,
-                    reverse=True,
-                )[:1][0]
-                if recognizer_result.intents
-                else None
-            )
-
-            if intent == Intent.BOOK_FLIGHT.value:
-                result = BookingDetails()
-
-                # We need to get the result from the LUIS JSON which at every level returns an array.
-                to_entities = recognizer_result.entities.get("$instance", {}).get(
-                    "To", []
-                )
-                if len(to_entities) > 0:
-                    if recognizer_result.entities.get("To", [{"$instance": {}}])[0][
-                        "$instance"
-                    ]:
-                        result.destination = to_entities[0]["text"].capitalize()
-                    else:
-                        result.unsupported_airports.append(
-                            to_entities[0]["text"].capitalize()
-                        )
-
-                from_entities = recognizer_result.entities.get("$instance", {}).get(
-                    "From", []
-                )
-                if len(from_entities) > 0:
-                    if recognizer_result.entities.get("From", [{"$instance": {}}])[0][
-                        "$instance"
-                    ]:
-                        result.origin = from_entities[0]["text"].capitalize()
-                    else:
-                        result.unsupported_airports.append(
-                            from_entities[0]["text"].capitalize()
-                        )
-
-                # This value will be a TIMEX. And we are only interested in a Date so grab the first result and drop the Time part.
-                # TIMEX is a format that represents DateTime expressions that include some ambiguity. e.g. missing a Year.
-                date_entities = recognizer_result.entities.get("datetime", [])
-                if date_entities:
-                    timex = date_entities[0]["timex"]
-
-                    if timex:
-                        datetime = timex[0].split("T")[0]
-
-                        result.travel_date = datetime
-
-                else:
-                    result.travel_date = None
-
-        except Exception as e:
-            print(e)
-
-        return intent, result
diff --git a/samples/13.core-bot/requirements.txt b/samples/13.core-bot/requirements.txt
deleted file mode 100644
index c11eb2923..000000000
--- a/samples/13.core-bot/requirements.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-botbuilder-dialogs>=4.4.0.b1
-botbuilder-ai>=4.4.0.b1
-datatypes-date-time>=1.0.0.a2
-flask>=1.0.3
-
diff --git a/samples/21.corebot-app-insights/NOTICE.md b/samples/21.corebot-app-insights/NOTICE.md
deleted file mode 100644
index 056c7237f..000000000
--- a/samples/21.corebot-app-insights/NOTICE.md
+++ /dev/null
@@ -1,8 +0,0 @@
-## NOTICE
-
-Please note that while the 21.corebot-app-insights sample is licensed under the MIT license, the sample has dependencies that use other types of licenses.
-
-Since Microsoft does not modify nor distribute these dependencies, it is the sole responsibility of the user to determine the correct/compliant usage of these dependencies. Please refer to the 
-[bot requirements](./bot/requirements.txt), [model requirements](./model/setup.py) and [model runtime requirements](./model_runtime_svc/setup.py) for a list of the **direct** dependencies.
-
-Please also note that the sample depends on the `requests` package, which has a dependency `chardet` that uses LGPL license.
\ No newline at end of file
diff --git a/samples/21.corebot-app-insights/README-LUIS.md b/samples/21.corebot-app-insights/README-LUIS.md
deleted file mode 100644
index 61bde7719..000000000
--- a/samples/21.corebot-app-insights/README-LUIS.md
+++ /dev/null
@@ -1,216 +0,0 @@
-# Setting up LUIS via CLI:
-
-This README contains information on how to create and deploy a LUIS application. When the bot is ready to be deployed to production, we recommend creating a LUIS Endpoint Resource for usage with your LUIS App.
-
-> _For instructions on how to create a LUIS Application via the LUIS portal, see these Quickstart steps:_
-> 1. _[Quickstart: Create a new app in the LUIS portal][Quickstart-create]_
-> 2. _[Quickstart: Deploy an app in the LUIS portal][Quickstart-deploy]_
-
-  [Quickstart-create]: https://docs.microsoft.com/azure/cognitive-services/luis/get-started-portal-build-app
-  [Quickstart-deploy]:https://docs.microsoft.com/azure/cognitive-services/luis/get-started-portal-deploy-app
-
-## Table of Contents:
-
-- [Prerequisites](#Prerequisites)
-- [Import a new LUIS Application using a local LUIS application](#Import-a-new-LUIS-Application-using-a-local-LUIS-application)
-- [How to create a LUIS Endpoint resource in Azure and pair it with a LUIS Application](#How-to-create-a-LUIS-Endpoint-resource-in-Azure-and-pair-it-with-a-LUIS-Application)
-
-___
-
-## [Prerequisites](#Table-of-Contents):
-
-#### Install Azure CLI >=2.0.61:
-
-Visit the following page to find the correct installer for your OS:
-- https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest
-
-#### Install LUIS CLI >=2.4.0:
-
-Open a CLI of your choice and type the following:
-
-```bash
-npm i -g luis-apis@^2.4.0
-```
-
-#### LUIS portal account:
-
-You should already have a LUIS account with either https://luis.ai, https://eu.luis.ai, or https://au.luis.ai. To determine where to create a LUIS account, consider where you will deploy your LUIS applications, and then place them in [the corresponding region][LUIS-Authoring-Regions].
-
-After you've created your account, you need your [Authoring Key][LUIS-AKey] and a LUIS application ID.
-
-  [LUIS-Authoring-Regions]: https://docs.microsoft.com/azure/cognitive-services/luis/luis-reference-regions#luis-authoring-regions]
-  [LUIS-AKey]: https://docs.microsoft.com/azure/cognitive-services/luis/luis-concept-keys#authoring-key
-
-___
-
-## [Import a new LUIS Application using a local LUIS application](#Table-of-Contents)
-
-### 1. Import the local LUIS application to luis.ai
-
-```bash
-luis import application --region "LuisAppAuthoringRegion" --authoringKey "LuisAuthoringKey" --appName "FlightBooking" --in "./cognitiveModels/FlightBooking.json"
-```
-
-Outputs the following JSON:
-
-```json
-{
-    "id": "########-####-####-####-############",
-    "name": "FlightBooking",
-    "description": "A LUIS model that uses intent and entities.",
-    "culture": "en-us",
-    "usageScenario": "",
-    "domain": "",
-    "versionsCount": 1,
-    "createdDateTime": "2019-03-29T18:32:02Z",
-    "endpoints": {},
-    "endpointHitsCount": 0,
-    "activeVersion": "0.1",
-    "ownerEmail": "bot@contoso.com",
-    "tokenizerVersion": "1.0.0"
-}
-```
-
-For the next step, you'll need the `"id"` value for `--appId` and the `"activeVersion"` value for `--versionId`.
-
-### 2. Train the LUIS Application
-
-```bash
-luis train version --region "LuisAppAuthoringRegion" --authoringKey "LuisAuthoringKey" --appId "LuisAppId" --versionId "LuisAppversion" --wait
-```
-
-### 3. Publish the LUIS Application
-
-```bash
-luis publish version --region "LuisAppAuthoringRegion" --authoringKey "LuisAuthoringKey" --appId "LuisAppId" --versionId "LuisAppversion" --publishRegion "LuisAppPublishRegion"
-```
-
-> `--region` corresponds to the region you _author_ your application in. The regions available for this are "westus", "westeurope" and "australiaeast". 
-> These regions correspond to the three available portals, https://luis.ai, https://eu.luis.ai, or https://au.luis.ai. 
-> `--publishRegion` corresponds to the region of the endpoint you're publishing to, (e.g. "westus", "southeastasia", "westeurope", "brazilsouth"). 
-> See the [reference docs][Endpoint-API] for a list of available publish/endpoint regions.
-
-  [Endpoint-API]: https://westus.dev.cognitive.microsoft.com/docs/services/5819c76f40a6350ce09de1ac/operations/5819c77140a63516d81aee78
-
-Outputs the following:
-
-```json
- {
-    "versionId": "0.1",
-    "isStaging": false,
-    "endpointUrl": "https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/########-####-####-####-############",
-    "region": "westus",
-    "assignedEndpointKey": null,
-    "endpointRegion": "westus",
-    "failedRegions": "",
-    "publishedDateTime": "2019-03-29T18:40:32Z",
-    "directVersionPublish": false
-}
-```
-
-To see how to create an LUIS Cognitive Service Resource in Azure, please see [the next README][README-LUIS]. This Resource should be used when you want to move your bot to production. The instructions will show you how to create and pair the resource with a LUIS Application.
-
-  [README-LUIS]: ./README-LUIS.md
-
-___
-
-## [How to create a LUIS Endpoint resource in Azure and pair it with a LUIS Application](#Table-of-Contents)
-
-### 1. Create a new LUIS Cognitive Services resource on Azure via Azure CLI
-
-> _Note:_ 
-> _If you don't have a Resource Group in your Azure subscription, you can create one through the Azure portal or through using:_
-> ```bash
-> az group create --subscription "AzureSubscriptionGuid" --location "westus" --name "ResourceGroupName"
-> ```
-> _To see a list of valid locations, use `az account list-locations`_
-
-
-```bash
-# Use Azure CLI to create the LUIS Key resource on Azure
-az cognitiveservices account create --kind "luis" --name "NewLuisResourceName" --sku "S0" --location "westus" --subscription "AzureSubscriptionGuid" -g "ResourceGroupName"
-```
-
-The command will output a response similar to the JSON below:
-
-```json
-{
-  "endpoint": "https://westus.api.cognitive.microsoft.com/luis/v2.0",
-  "etag": "\"########-####-####-####-############\"",
-  "id": "/subscriptions/########-####-####-####-############/resourceGroups/ResourceGroupName/providers/Microsoft.CognitiveServices/accounts/NewLuisResourceName",
-  "internalId": "################################",
-  "kind": "luis",
-  "location": "westus",
-  "name": "NewLuisResourceName",
-  "provisioningState": "Succeeded",
-  "resourceGroup": "ResourceGroupName",
-  "sku": {
-    "name": "S0",
-    "tier": null
-  },
-  "tags": null,
-  "type": "Microsoft.CognitiveServices/accounts"
-}
-```
-
-
-
-Take the output from the previous command and create a JSON file in the following format:
-
-```json
-{
-    "azureSubscriptionId": "00000000-0000-0000-0000-000000000000",
-    "resourceGroup": "ResourceGroupName",
-    "accountName": "NewLuisResourceName"
-}
-```
-
-### 2. Retrieve ARM access token via Azure CLI
-
-```bash
-az account get-access-token --subscription "AzureSubscriptionGuid"
-```
-
-This will return an object that looks like this:
-
-```json
-{
-  "accessToken": "eyJ0eXAiOiJKVtokentokentokentokentokeng1dCI6Ik4tbEMwbi05REFMcXdodUhZbkhRNjNHZUNYYyIsItokenI6Ik4tbEMwbi05REFMcXdodUhZbkhRNjNHZUNYYyJ9.eyJhdWQiOiJodHRwczovL21hbmFnZW1lbnQuY29yZS53aW5kb3dzLm5ldC8iLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDcvIiwiaWF0IjoxNTUzODc3MTUwLCJuYmYiOjE1NTM4NzcxNTAsImV4cCI6MTU1Mzg4MTA1MCwiX2NsYWltX25hbWVzIjp7Imdyb3VwcyI6InNyYzEifSwiX2NsYWltX3NvdXJjZXMiOnsic3JjMSI6eyJlbmRwb2ludCI6Imh0dHBzOi8vZ3JhcGgud2luZG93cy5uZXQvNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3L3VzZXJzL2ZmZTQyM2RkLWJhM2YtNDg0Ny04NjgyLWExNTI5MDA4MjM4Ny9nZXRNZW1iZXJPYmplY3RzIn19LCJhY3IiOiIxIiwiYWlvIjoiQVZRQXEvOEtBQUFBeGVUc201NDlhVHg4RE1mMFlRVnhGZmxxOE9RSC9PODR3QktuSmRqV1FqTkkwbmxLYzB0bHJEZzMyMFZ5bWZGaVVBSFBvNUFFUTNHL0FZNDRjdk01T3M0SEt0OVJkcE5JZW9WU0dzd0kvSkk9IiwiYW1yIjpbIndpYSIsIm1mYSJdLCJhcHBpZCI6IjA0YjA3Nzk1LThkZGItNDYxYS1iYmVlLTAyZjllMWJmN2I0NiIsImFwcGlkYWNyIjoiMCIsImRldmljZWlkIjoiNDhmNDVjNjEtMTg3Zi00MjUxLTlmZWItMTllZGFkZmMwMmE3IiwiZmFtaWx5X25hbWUiOiJHdW0iLCJnaXZlbl9uYW1lIjoiU3RldmVuIiwiaXBhZGRyIjoiMTY3LjIyMC4yLjU1IiwibmFtZSI6IlN0ZXZlbiBHdW0iLCJvaWQiOiJmZmU0MjNkZC1iYTNmLTQ4NDctODY4Mi1hMTUyOTAwODIzODciLCJvbnByZW1fc2lkIjoiUy0xLTUtMjEtMjEyNzUyMTE4NC0xNjA0MDEyOTIwLTE4ODc5Mjc1MjctMjYwOTgyODUiLCJwdWlkIjoiMTAwMzdGRkVBMDQ4NjlBNyIsInJoIjoiSSIsInNjcCI6InVzZXJfaW1wZXJzb25hdGlvbiIsInN1YiI6Ik1rMGRNMWszN0U5ckJyMjhieUhZYjZLSU85LXVFQVVkZFVhNWpkSUd1Nk0iLCJ0aWQiOiI3MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDciLCJ1bmlxdWVfbmFtZSI6InN0Z3VtQG1pY3Jvc29mdC5jb20iLCJ1cG4iOiJzdGd1bUBtaWNyb3NvZnQuY29tIiwidXRpIjoiT2w2NGN0TXY4RVNEQzZZQWRqRUFtokenInZlciI6IjEuMCJ9.kFAsEilE0mlS1pcpqxf4rEnRKeYsehyk-gz-zJHUrE__oad3QjgDSBDPrR_ikLdweynxbj86pgG4QFaHURNCeE6SzrbaIrNKw-n9jrEtokenlosOxg_0l2g1LeEUOi5Q4gQREAU_zvSbl-RY6sAadpOgNHtGvz3Rc6FZRITfkckSLmsKAOFoh-aWC6tFKG8P52rtB0qVVRz9tovBeNqkMYL49s9ypduygbXNVwSQhm5JszeWDgrFuVFHBUP_iENCQYGQpEZf_KvjmX1Ur1F9Eh9nb4yI2gFlKncKNsQl-tokenK7-tokentokentokentokentokentokenatoken",
-  "expiresOn": "2200-12-31 23:59:59.999999",
-  "subscription": "AzureSubscriptionGuid",
-  "tenant": "tenant-guid",
-  "tokenType": "Bearer"
-}
-```
-
-The value needed for the next step is the `"accessToken"`.
-
-### 3. Use `luis add appazureaccount` to pair your LUIS resource with a LUIS Application
-
-```bash
-luis add appazureaccount --in "path/to/created/requestBody.json" --appId "LuisAppId" --authoringKey "LuisAuthoringKey" --armToken "accessToken"
-```
-
-If successful, it should yield a response like this:
-
-```json
-{
-  "code": "Success",
-  "message": "Operation Successful"
-}
-```
-
-### 4. See the LUIS Cognitive Services' keys
-
-```bash
-az cognitiveservices account keys list --name "NewLuisResourceName" --subscription "AzureSubscriptionGuid" -g "ResourceGroupName"
-```
-
-This will return an object that looks like this:
-
-```json
-{
-  "key1": "9a69####dc8f####8eb4####399f####",
-  "key2": "####f99e####4b1a####fb3b####6b9f"
-}
-```
\ No newline at end of file
diff --git a/samples/21.corebot-app-insights/README.md b/samples/21.corebot-app-insights/README.md
deleted file mode 100644
index 3e7d71599..000000000
--- a/samples/21.corebot-app-insights/README.md
+++ /dev/null
@@ -1,57 +0,0 @@
-# CoreBot
-
-Bot Framework v4 core bot sample.
-
-This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to:
-
-- Use [LUIS](https://www.luis.ai) to implement core AI capabilities
-- Implement a multi-turn conversation using Dialogs
-- Handle user interruptions for such things as `Help` or `Cancel`
-- Prompt for and validate requests for information from the user
-- Use [Application Insights](https://docs.microsoft.com/azure/azure-monitor/app/cloudservices) to monitor your bot
-
-## Prerequisites
-
-This sample **requires** prerequisites in order to run.
-
-### Overview
-
-This bot uses [LUIS](https://www.luis.ai), an AI based cognitive service, to implement language understanding.
-
-### Install Python 3.6
-
-
-### Create a LUIS Application to enable language understanding
-
-LUIS language model setup, training, and application configuration steps can be found [here](https://docs.microsoft.com/azure/bot-service/bot-builder-howto-v4-luis?view=azure-bot-service-4.0&tabs=cs).
-
-If you wish to create a LUIS application via the CLI, these steps can be found in the [README-LUIS.md](README-LUIS.md).
-
-
-## Testing the bot using Bot Framework Emulator
-
-[Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel.
-
-- Install the Bot Framework Emulator version 4.3.0 or greater from [here](https://github.com/Microsoft/BotFramework-Emulator/releases)
-
-### Connect to the bot using Bot Framework Emulator
-
-- Launch Bot Framework Emulator
-- File -> Open Bot
-- Enter a Bot URL of `http://localhost:3978/api/messages`
-
-
-## Further reading
-
-- [Bot Framework Documentation](https://docs.botframework.com)
-- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0)
-- [Dialogs](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-dialog?view=azure-bot-service-4.0)
-- [Gathering Input Using Prompts](https://docs.microsoft.com/azure/bot-service/bot-builder-prompts?view=azure-bot-service-4.0&tabs=csharp)
-- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0)
-- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0)
-- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0)
-- [.NET Core CLI tools](https://docs.microsoft.com/dotnet/core/tools/?tabs=netcore2x)
-- [Azure CLI](https://docs.microsoft.com/cli/azure/?view=azure-cli-latest)
-- [Azure Portal](https://portal.azure.com)
-- [Language Understanding using LUIS](https://docs.microsoft.com/azure/cognitive-services/luis/)
-- [Channels and Bot Connector Service](https://docs.microsoft.com/azure/bot-service/bot-concepts?view=azure-bot-service-4.0)
\ No newline at end of file
diff --git a/samples/21.corebot-app-insights/booking_details.py b/samples/21.corebot-app-insights/booking_details.py
deleted file mode 100644
index 81f420fa6..000000000
--- a/samples/21.corebot-app-insights/booking_details.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""Booking detail."""
-
-
-class BookingDetails:
-    """Booking detail implementation"""
-
-    def __init__(
-        self, destination: str = None, origin: str = None, travel_date: str = None
-    ):
-        self.destination = destination
-        self.origin = origin
-        self.travel_date = travel_date
diff --git a/samples/21.corebot-app-insights/bots/dialog_and_welcome_bot.py b/samples/21.corebot-app-insights/bots/dialog_and_welcome_bot.py
deleted file mode 100644
index 80f37ea71..000000000
--- a/samples/21.corebot-app-insights/bots/dialog_and_welcome_bot.py
+++ /dev/null
@@ -1,63 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""Main dialog to welcome users."""
-import json
-import os.path
-
-from typing import List
-from botbuilder.dialogs import Dialog
-from botbuilder.core import (
-    TurnContext,
-    ConversationState,
-    UserState,
-    BotTelemetryClient,
-)
-from botbuilder.schema import Activity, Attachment, ChannelAccount
-from helpers.activity_helper import create_activity_reply
-from .dialog_bot import DialogBot
-
-
-class DialogAndWelcomeBot(DialogBot):
-    """Main dialog to welcome users."""
-
-    def __init__(
-        self,
-        conversation_state: ConversationState,
-        user_state: UserState,
-        dialog: Dialog,
-        telemetry_client: BotTelemetryClient,
-    ):
-        super(DialogAndWelcomeBot, self).__init__(
-            conversation_state, user_state, dialog, telemetry_client
-        )
-        self.telemetry_client = telemetry_client
-
-    async def on_members_added_activity(
-        self, members_added: List[ChannelAccount], turn_context: TurnContext
-    ):
-        for member in members_added:
-            # Greet anyone that was not the target (recipient) of this message.
-            # To learn more about Adaptive Cards, see https://aka.ms/msbot-adaptivecards
-            # for more details.
-            if member.id != turn_context.activity.recipient.id:
-                welcome_card = self.create_adaptive_card_attachment()
-                response = self.create_response(turn_context.activity, welcome_card)
-                await turn_context.send_activity(response)
-
-    def create_response(self, activity: Activity, attachment: Attachment):
-        """Create an attachment message response."""
-        response = create_activity_reply(activity)
-        response.attachments = [attachment]
-        return response
-
-    # Load attachment from file.
-    def create_adaptive_card_attachment(self):
-        """Create an adaptive card."""
-        relative_path = os.path.abspath(os.path.dirname(__file__))
-        path = os.path.join(relative_path, "resources/welcomeCard.json")
-        with open(path) as card_file:
-            card = json.load(card_file)
-
-        return Attachment(
-            content_type="application/vnd.microsoft.card.adaptive", content=card
-        )
diff --git a/samples/21.corebot-app-insights/bots/dialog_bot.py b/samples/21.corebot-app-insights/bots/dialog_bot.py
deleted file mode 100644
index 3b55ba7a7..000000000
--- a/samples/21.corebot-app-insights/bots/dialog_bot.py
+++ /dev/null
@@ -1,75 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""Implements bot Activity handler."""
-
-from botbuilder.core import (
-    ActivityHandler,
-    ConversationState,
-    UserState,
-    TurnContext,
-    BotTelemetryClient,
-    NullTelemetryClient,
-)
-from botbuilder.dialogs import Dialog
-from helpers.dialog_helper import DialogHelper
-
-
-class DialogBot(ActivityHandler):
-    """Main activity handler for the bot."""
-
-    def __init__(
-        self,
-        conversation_state: ConversationState,
-        user_state: UserState,
-        dialog: Dialog,
-        telemetry_client: BotTelemetryClient,
-    ):
-        if conversation_state is None:
-            raise Exception(
-                "[DialogBot]: Missing parameter. conversation_state is required"
-            )
-        if user_state is None:
-            raise Exception("[DialogBot]: Missing parameter. user_state is required")
-        if dialog is None:
-            raise Exception("[DialogBot]: Missing parameter. dialog is required")
-
-        self.conversation_state = conversation_state
-        self.user_state = user_state
-        self.dialog = dialog
-        self.dialogState = self.conversation_state.create_property(
-            "DialogState"
-        )  # pylint:disable=invalid-name
-        self.telemetry_client = telemetry_client
-
-    async def on_turn(self, turn_context: TurnContext):
-        await super().on_turn(turn_context)
-
-        # Save any state changes that might have occured during the turn.
-        await self.conversation_state.save_changes(turn_context, False)
-        await self.user_state.save_changes(turn_context, False)
-
-    async def on_message_activity(self, turn_context: TurnContext):
-        # pylint:disable=invalid-name
-        await DialogHelper.run_dialog(
-            self.dialog,
-            turn_context,
-            self.conversation_state.create_property("DialogState"),
-        )
-
-    @property
-    def telemetry_client(self) -> BotTelemetryClient:
-        """
-        Gets the telemetry client for logging events.
-        """
-        return self._telemetry_client
-
-    # pylint:disable=attribute-defined-outside-init
-    @telemetry_client.setter
-    def telemetry_client(self, value: BotTelemetryClient) -> None:
-        """
-        Sets the telemetry client for logging events.
-        """
-        if value is None:
-            self._telemetry_client = NullTelemetryClient()
-        else:
-            self._telemetry_client = value
diff --git a/samples/21.corebot-app-insights/cognitiveModels/FlightBooking.json b/samples/21.corebot-app-insights/cognitiveModels/FlightBooking.json
deleted file mode 100644
index 5d1c9ec38..000000000
--- a/samples/21.corebot-app-insights/cognitiveModels/FlightBooking.json
+++ /dev/null
@@ -1,226 +0,0 @@
-{
-  "luis_schema_version": "3.2.0",
-  "versionId": "0.1",
-  "name": "Airline Reservation",
-  "desc": "A LUIS model that uses intent and entities.",
-  "culture": "en-us",
-  "tokenizerVersion": "1.0.0",
-  "intents": [
-    {
-      "name": "Book flight"
-    },
-    {
-      "name": "Cancel"
-    },
-    {
-      "name": "None"
-    }
-  ],
-  "entities": [],
-  "composites": [
-    {
-      "name": "From",
-      "children": [
-        "Airport"
-      ],
-      "roles": []
-    },
-    {
-      "name": "To",
-      "children": [
-        "Airport"
-      ],
-      "roles": []
-    }
-  ],
-  "closedLists": [
-    {
-      "name": "Airport",
-      "subLists": [
-        {
-          "canonicalForm": "Paris",
-          "list": [
-            "paris"
-          ]
-        },
-        {
-          "canonicalForm": "London",
-          "list": [
-            "london"
-          ]
-        },
-        {
-          "canonicalForm": "Berlin",
-          "list": [
-            "berlin"
-          ]
-        },
-        {
-          "canonicalForm": "New York",
-          "list": [
-            "new york"
-          ]
-        }
-      ],
-      "roles": []
-    }
-  ],
-  "patternAnyEntities": [],
-  "regex_entities": [],
-  "prebuiltEntities": [
-    {
-      "name": "datetimeV2",
-      "roles": []
-    }
-  ],
-  "model_features": [],
-  "regex_features": [],
-  "patterns": [],
-  "utterances": [
-    {
-      "text": "book flight from london to paris on feb 14th",
-      "intent": "Book flight",
-      "entities": [
-        {
-          "entity": "To",
-          "startPos": 27,
-          "endPos": 31
-        },
-        {
-          "entity": "From",
-          "startPos": 17,
-          "endPos": 22
-        }
-      ]
-    },
-    {
-      "text": "book flight to berlin on feb 14th",
-      "intent": "Book flight",
-      "entities": [
-        {
-          "entity": "To",
-          "startPos": 15,
-          "endPos": 20
-        }
-      ]
-    },
-    {
-      "text": "book me a flight from london to paris",
-      "intent": "Book flight",
-      "entities": [
-        {
-          "entity": "From",
-          "startPos": 22,
-          "endPos": 27
-        },
-        {
-          "entity": "To",
-          "startPos": 32,
-          "endPos": 36
-        }
-      ]
-    },
-    {
-      "text": "bye",
-      "intent": "Cancel",
-      "entities": []
-    },
-    {
-      "text": "cancel booking",
-      "intent": "Cancel",
-      "entities": []
-    },
-    {
-      "text": "exit",
-      "intent": "Cancel",
-      "entities": []
-    },
-    {
-      "text": "flight to paris",
-      "intent": "Book flight",
-      "entities": [
-        {
-          "entity": "To",
-          "startPos": 10,
-          "endPos": 14
-        }
-      ]
-    },
-    {
-      "text": "flight to paris from london on feb 14th",
-      "intent": "Book flight",
-      "entities": [
-        {
-          "entity": "To",
-          "startPos": 10,
-          "endPos": 14
-        },
-        {
-          "entity": "From",
-          "startPos": 21,
-          "endPos": 26
-        }
-      ]
-    },
-    {
-      "text": "fly from berlin to paris on may 5th",
-      "intent": "Book flight",
-      "entities": [
-        {
-          "entity": "To",
-          "startPos": 19,
-          "endPos": 23
-        },
-        {
-          "entity": "From",
-          "startPos": 9,
-          "endPos": 14
-        }
-      ]
-    },
-    {
-      "text": "go to paris",
-      "intent": "Book flight",
-      "entities": [
-        {
-          "entity": "To",
-          "startPos": 6,
-          "endPos": 10
-        }
-      ]
-    },
-    {
-      "text": "going from paris to berlin",
-      "intent": "Book flight",
-      "entities": [
-        {
-          "entity": "To",
-          "startPos": 20,
-          "endPos": 25
-        },
-        {
-          "entity": "From",
-          "startPos": 11,
-          "endPos": 15
-        }
-      ]
-    },
-    {
-      "text": "ignore",
-      "intent": "Cancel",
-      "entities": []
-    },
-    {
-      "text": "travel to paris",
-      "intent": "Book flight",
-      "entities": [
-        {
-          "entity": "To",
-          "startPos": 10,
-          "endPos": 14
-        }
-      ]
-    }
-  ],
-  "settings": []
-}
\ No newline at end of file
diff --git a/samples/21.corebot-app-insights/config.py b/samples/21.corebot-app-insights/config.py
deleted file mode 100644
index 339154fc0..000000000
--- a/samples/21.corebot-app-insights/config.py
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/usr/bin/env python
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""Configuration for the bot."""
-
-import os
-
-
-class DefaultConfig:
-    """Configuration for the bot."""
-
-    PORT = 3978
-    APP_ID = os.environ.get("MicrosoftAppId", "")
-    APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "")
-    LUIS_APP_ID = os.environ.get("LuisAppId", "")
-    LUIS_API_KEY = os.environ.get("LuisAPIKey", "")
-    # LUIS endpoint host name, ie "westus.api.cognitive.microsoft.com"
-    LUIS_API_HOST_NAME = os.environ.get("LuisAPIHostName", "")
-    APPINSIGHTS_INSTRUMENTATION_KEY = os.environ.get("AppInsightsInstrumentationKey", "")
diff --git a/samples/21.corebot-app-insights/dialogs/booking_dialog.py b/samples/21.corebot-app-insights/dialogs/booking_dialog.py
deleted file mode 100644
index 139d146fc..000000000
--- a/samples/21.corebot-app-insights/dialogs/booking_dialog.py
+++ /dev/null
@@ -1,131 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""Flight booking dialog."""
-
-from botbuilder.dialogs import WaterfallDialog, WaterfallStepContext, DialogTurnResult
-from botbuilder.dialogs.prompts import ConfirmPrompt, TextPrompt, PromptOptions
-from botbuilder.core import MessageFactory, BotTelemetryClient, NullTelemetryClient
-from datatypes_date_time.timex import Timex
-from .cancel_and_help_dialog import CancelAndHelpDialog
-from .date_resolver_dialog import DateResolverDialog
-
-
-class BookingDialog(CancelAndHelpDialog):
-    """Flight booking implementation."""
-
-    def __init__(
-        self,
-        dialog_id: str = None,
-        telemetry_client: BotTelemetryClient = NullTelemetryClient(),
-    ):
-        super(BookingDialog, self).__init__(
-            dialog_id or BookingDialog.__name__, telemetry_client
-        )
-        self.telemetry_client = telemetry_client
-        text_prompt = TextPrompt(TextPrompt.__name__)
-        text_prompt.telemetry_client = telemetry_client
-
-        waterfall_dialog = WaterfallDialog(
-            WaterfallDialog.__name__,
-            [
-                self.destination_step,
-                self.origin_step,
-                self.travel_date_step,
-                # self.confirm_step,
-                self.final_step,
-            ],
-        )
-        waterfall_dialog.telemetry_client = telemetry_client
-
-        self.add_dialog(text_prompt)
-        # self.add_dialog(ConfirmPrompt(ConfirmPrompt.__name__))
-        self.add_dialog(
-            DateResolverDialog(DateResolverDialog.__name__, self.telemetry_client)
-        )
-        self.add_dialog(waterfall_dialog)
-
-        self.initial_dialog_id = WaterfallDialog.__name__
-
-    async def destination_step(
-        self, step_context: WaterfallStepContext
-    ) -> DialogTurnResult:
-        """Prompt for destination."""
-        booking_details = step_context.options
-
-        if booking_details.destination is None:
-            return await step_context.prompt(
-                TextPrompt.__name__,
-                PromptOptions(
-                    prompt=MessageFactory.text("To what city would you like to travel?")
-                ),
-            )  # pylint: disable=line-too-long,bad-continuation
-        else:
-            return await step_context.next(booking_details.destination)
-
-    async def origin_step(self, step_context: WaterfallStepContext) -> DialogTurnResult:
-        """Prompt for origin city."""
-        booking_details = step_context.options
-
-        # Capture the response to the previous step's prompt
-        booking_details.destination = step_context.result
-        if booking_details.origin is None:
-            return await step_context.prompt(
-                TextPrompt.__name__,
-                PromptOptions(
-                    prompt=MessageFactory.text("From what city will you be travelling?")
-                ),
-            )  # pylint: disable=line-too-long,bad-continuation
-        else:
-            return await step_context.next(booking_details.origin)
-
-    async def travel_date_step(
-        self, step_context: WaterfallStepContext
-    ) -> DialogTurnResult:
-        """Prompt for travel date.
-        This will use the DATE_RESOLVER_DIALOG."""
-
-        booking_details = step_context.options
-
-        # Capture the results of the previous step
-        booking_details.origin = step_context.result
-        if not booking_details.travel_date or self.is_ambiguous(
-            booking_details.travel_date
-        ):
-            return await step_context.begin_dialog(
-                DateResolverDialog.__name__, booking_details.travel_date
-            )  # pylint: disable=line-too-long
-        else:
-            return await step_context.next(booking_details.travel_date)
-
-    async def confirm_step(
-        self, step_context: WaterfallStepContext
-    ) -> DialogTurnResult:
-        """Confirm the information the user has provided."""
-        booking_details = step_context.options
-
-        # Capture the results of the previous step
-        booking_details.travel_date = step_context.result
-        msg = (
-            f"Please confirm, I have you traveling to: { booking_details.destination }"
-            f" from: { booking_details.origin } on: { booking_details.travel_date}."
-        )
-
-        # Offer a YES/NO prompt.
-        return await step_context.prompt(
-            ConfirmPrompt.__name__, PromptOptions(prompt=MessageFactory.text(msg))
-        )
-
-    async def final_step(self, step_context: WaterfallStepContext) -> DialogTurnResult:
-        """Complete the interaction and end the dialog."""
-        if step_context.result:
-            booking_details = step_context.options
-            booking_details.travel_date = step_context.result
-
-            return await step_context.end_dialog(booking_details)
-        else:
-            return await step_context.end_dialog()
-
-    def is_ambiguous(self, timex: str) -> bool:
-        """Ensure time is correct."""
-        timex_property = Timex(timex)
-        return "definite" not in timex_property.types
diff --git a/samples/21.corebot-app-insights/dialogs/cancel_and_help_dialog.py b/samples/21.corebot-app-insights/dialogs/cancel_and_help_dialog.py
deleted file mode 100644
index 2a73c669a..000000000
--- a/samples/21.corebot-app-insights/dialogs/cancel_and_help_dialog.py
+++ /dev/null
@@ -1,55 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""Handle cancel and help intents."""
-
-from botbuilder.core import BotTelemetryClient, NullTelemetryClient
-from botbuilder.dialogs import (
-    ComponentDialog,
-    DialogContext,
-    DialogTurnResult,
-    DialogTurnStatus,
-)
-from botbuilder.schema import ActivityTypes
-
-
-class CancelAndHelpDialog(ComponentDialog):
-    """Implementation of handling cancel and help."""
-
-    def __init__(
-        self,
-        dialog_id: str,
-        telemetry_client: BotTelemetryClient = NullTelemetryClient(),
-    ):
-        super(CancelAndHelpDialog, self).__init__(dialog_id)
-        self.telemetry_client = telemetry_client
-
-    async def on_begin_dialog(
-        self, inner_dc: DialogContext, options: object
-    ) -> DialogTurnResult:
-        result = await self.interrupt(inner_dc)
-        if result is not None:
-            return result
-
-        return await super(CancelAndHelpDialog, self).on_begin_dialog(inner_dc, options)
-
-    async def on_continue_dialog(self, inner_dc: DialogContext) -> DialogTurnResult:
-        result = await self.interrupt(inner_dc)
-        if result is not None:
-            return result
-
-        return await super(CancelAndHelpDialog, self).on_continue_dialog(inner_dc)
-
-    async def interrupt(self, inner_dc: DialogContext) -> DialogTurnResult:
-        """Detect interruptions."""
-        if inner_dc.context.activity.type == ActivityTypes.message:
-            text = inner_dc.context.activity.text.lower()
-
-            if text == "help" or text == "?":
-                await inner_dc.context.send_activity("Show Help...")
-                return DialogTurnResult(DialogTurnStatus.Waiting)
-
-            if text == "cancel" or text == "quit":
-                await inner_dc.context.send_activity("Cancelling")
-                return await inner_dc.cancel_all_dialogs()
-
-        return None
diff --git a/samples/21.corebot-app-insights/dialogs/date_resolver_dialog.py b/samples/21.corebot-app-insights/dialogs/date_resolver_dialog.py
deleted file mode 100644
index f64a27955..000000000
--- a/samples/21.corebot-app-insights/dialogs/date_resolver_dialog.py
+++ /dev/null
@@ -1,89 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""Handle date/time resolution for booking dialog."""
-from botbuilder.core import MessageFactory, BotTelemetryClient, NullTelemetryClient
-from botbuilder.dialogs import WaterfallDialog, DialogTurnResult, WaterfallStepContext
-from botbuilder.dialogs.prompts import (
-    DateTimePrompt,
-    PromptValidatorContext,
-    PromptOptions,
-    DateTimeResolution,
-)
-from datatypes_date_time.timex import Timex
-from .cancel_and_help_dialog import CancelAndHelpDialog
-
-
-class DateResolverDialog(CancelAndHelpDialog):
-    """Resolve the date"""
-
-    def __init__(
-        self,
-        dialog_id: str = None,
-        telemetry_client: BotTelemetryClient = NullTelemetryClient(),
-    ):
-        super(DateResolverDialog, self).__init__(
-            dialog_id or DateResolverDialog.__name__, telemetry_client
-        )
-        self.telemetry_client = telemetry_client
-
-        date_time_prompt = DateTimePrompt(
-            DateTimePrompt.__name__, DateResolverDialog.datetime_prompt_validator
-        )
-        date_time_prompt.telemetry_client = telemetry_client
-
-        waterfall_dialog = WaterfallDialog(
-            WaterfallDialog.__name__ + "2", [self.initial_step, self.final_step]
-        )
-        waterfall_dialog.telemetry_client = telemetry_client
-
-        self.add_dialog(date_time_prompt)
-        self.add_dialog(waterfall_dialog)
-
-        self.initial_dialog_id = WaterfallDialog.__name__ + "2"
-
-    async def initial_step(
-        self, step_context: WaterfallStepContext
-    ) -> DialogTurnResult:
-        """Prompt for the date."""
-        timex = step_context.options
-
-        prompt_msg = "On what date would you like to travel?"
-        reprompt_msg = (
-            "I'm sorry, for best results, please enter your travel "
-            "date including the month, day and year."
-        )
-
-        if timex is None:
-            # We were not given any date at all so prompt the user.
-            return await step_context.prompt(
-                DateTimePrompt.__name__,
-                PromptOptions(  # pylint: disable=bad-continuation
-                    prompt=MessageFactory.text(prompt_msg),
-                    retry_prompt=MessageFactory.text(reprompt_msg),
-                ),
-            )
-        else:
-            # We have a Date we just need to check it is unambiguous.
-            if "definite" in Timex(timex).types:
-                # This is essentially a "reprompt" of the data we were given up front.
-                return await step_context.prompt(
-                    DateTimePrompt.__name__, PromptOptions(prompt=reprompt_msg)
-                )
-            else:
-                return await step_context.next(DateTimeResolution(timex=timex))
-
-    async def final_step(self, step_context: WaterfallStepContext):
-        """Cleanup - set final return value and end dialog."""
-        timex = step_context.result[0].timex
-        return await step_context.end_dialog(timex)
-
-    @staticmethod
-    async def datetime_prompt_validator(prompt_context: PromptValidatorContext) -> bool:
-        """ Validate the date provided is in proper form. """
-        if prompt_context.recognized.succeeded:
-            timex = prompt_context.recognized.value[0].timex.split("T")[0]
-
-            # TODO: Needs TimexProperty
-            return "definite" in Timex(timex).types
-
-        return False
diff --git a/samples/21.corebot-app-insights/dialogs/main_dialog.py b/samples/21.corebot-app-insights/dialogs/main_dialog.py
deleted file mode 100644
index e4807ce8a..000000000
--- a/samples/21.corebot-app-insights/dialogs/main_dialog.py
+++ /dev/null
@@ -1,115 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""Main dialog. """
-
-from botbuilder.core import BotTelemetryClient, NullTelemetryClient
-from botbuilder.dialogs import (
-    ComponentDialog,
-    WaterfallDialog,
-    WaterfallStepContext,
-    DialogTurnResult,
-)
-from botbuilder.dialogs.prompts import TextPrompt, PromptOptions
-from botbuilder.core import MessageFactory
-from booking_details import BookingDetails
-from helpers.luis_helper import LuisHelper
-from .booking_dialog import BookingDialog
-
-
-class MainDialog(ComponentDialog):
-    """Main dialog. """
-
-    def __init__(
-        self,
-        configuration: dict,
-        dialog_id: str = None,
-        telemetry_client: BotTelemetryClient = NullTelemetryClient(),
-    ):
-        super(MainDialog, self).__init__(dialog_id or MainDialog.__name__)
-
-        self._configuration = configuration
-        self.telemetry_client = telemetry_client
-
-        text_prompt = TextPrompt(TextPrompt.__name__)
-        text_prompt.telemetry_client = self.telemetry_client
-
-        booking_dialog = BookingDialog(telemetry_client=self._telemetry_client)
-        booking_dialog.telemetry_client = self.telemetry_client
-
-        wf_dialog = WaterfallDialog(
-            "WFDialog", [self.intro_step, self.act_step, self.final_step]
-        )
-        wf_dialog.telemetry_client = self.telemetry_client
-
-        self.add_dialog(text_prompt)
-        self.add_dialog(booking_dialog)
-        self.add_dialog(wf_dialog)
-
-        self.initial_dialog_id = "WFDialog"
-
-    async def intro_step(self, step_context: WaterfallStepContext) -> DialogTurnResult:
-        """Initial prompt."""
-        if (
-            not self._configuration.get("LUIS_APP_ID", "")
-            or not self._configuration.get("LUIS_API_KEY", "")
-            or not self._configuration.get("LUIS_API_HOST_NAME", "")
-        ):
-            await step_context.context.send_activity(
-                MessageFactory.text(
-                    "NOTE: LUIS is not configured. To enable all"
-                    " capabilities, add 'LUIS_APP_ID', 'LUIS_API_KEY' and 'LUIS_API_HOST_NAME'"
-                    " to the config.py file."
-                )
-            )
-
-            return await step_context.next(None)
-        else:
-            return await step_context.prompt(
-                TextPrompt.__name__,
-                PromptOptions(
-                    prompt=MessageFactory.text("What can I help you with today?")
-                ),
-            )  # pylint: disable=bad-continuation
-
-    async def act_step(self, step_context: WaterfallStepContext) -> DialogTurnResult:
-        """Use language understanding to gather details about booking."""
-        # Call LUIS and gather any potential booking details. (Note the TurnContext
-        # has the response to the prompt.)
-        booking_details = (
-            await LuisHelper.execute_luis_query(
-                self._configuration, step_context.context, self.telemetry_client
-            )
-            if step_context.result is not None
-            else BookingDetails()
-        )  # pylint: disable=bad-continuation
-
-        # In this sample we only have a single Intent we are concerned with. However,
-        # typically a scenario will have multiple different Intents each corresponding
-        # to starting a different child Dialog.
-
-        # Run the BookingDialog giving it whatever details we have from the
-        # model.  The dialog will prompt to find out the remaining details.
-        return await step_context.begin_dialog(BookingDialog.__name__, booking_details)
-
-    async def final_step(self, step_context: WaterfallStepContext) -> DialogTurnResult:
-        """Complete dialog.
-        At this step, with details from the user, display the completed
-        flight booking to the user.
-        """
-        # If the child dialog ("BookingDialog") was cancelled or the user failed
-        # to confirm, the Result here will be null.
-        if step_context.result is not None:
-            result = step_context.result
-
-            # Now we have all the booking details call the booking service.
-            # If the call to the booking service was successful tell the user.
-            # time_property = Timex(result.travel_date)
-            # travel_date_msg = time_property.to_natural_language(datetime.now())
-            msg = (
-                f"I have you booked to {result.destination} from"
-                f" {result.origin} on {result.travel_date}."
-            )
-            await step_context.context.send_activity(MessageFactory.text(msg))
-        else:
-            await step_context.context.send_activity(MessageFactory.text("Thank you."))
-        return await step_context.end_dialog()
diff --git a/samples/21.corebot-app-insights/helpers/__init__.py b/samples/21.corebot-app-insights/helpers/__init__.py
deleted file mode 100644
index 162eef503..000000000
--- a/samples/21.corebot-app-insights/helpers/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""Helpers module."""
-
-from . import activity_helper, luis_helper, dialog_helper
-
-__all__ = ["activity_helper", "dialog_helper", "luis_helper"]
diff --git a/samples/21.corebot-app-insights/helpers/luis_helper.py b/samples/21.corebot-app-insights/helpers/luis_helper.py
deleted file mode 100644
index e244940c4..000000000
--- a/samples/21.corebot-app-insights/helpers/luis_helper.py
+++ /dev/null
@@ -1,71 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-"""Helper to call LUIS service."""
-from botbuilder.ai.luis import LuisRecognizer, LuisApplication, LuisPredictionOptions
-from botbuilder.core import TurnContext, BotTelemetryClient, NullTelemetryClient
-
-from booking_details import BookingDetails
-
-# pylint: disable=line-too-long
-class LuisHelper:
-    """LUIS helper implementation."""
-
-    @staticmethod
-    async def execute_luis_query(
-        configuration,
-        turn_context: TurnContext,
-        telemetry_client: BotTelemetryClient = None,
-    ) -> BookingDetails:
-        """Invoke LUIS service to perform prediction/evaluation of utterance."""
-        booking_details = BookingDetails()
-
-        # pylint:disable=broad-except
-        try:
-            luis_application = LuisApplication(
-                configuration.get("LUIS_APP_ID"),
-                configuration.get("LUIS_API_KEY"),
-                configuration.get("LUIS_API_HOST_NAME"),
-            )
-            options = LuisPredictionOptions()
-            options.telemetry_client = (
-                telemetry_client
-                if telemetry_client is not None
-                else NullTelemetryClient()
-            )
-            recognizer = LuisRecognizer(luis_application, prediction_options=options)
-            recognizer_result = await recognizer.recognize(turn_context)
-            print(f"Recognize Result: {recognizer_result}")
-
-            if recognizer_result.intents:
-                intent = sorted(
-                    recognizer_result.intents,
-                    key=recognizer_result.intents.get,
-                    reverse=True,
-                )[:1][0]
-                if intent == "Book_flight":
-                    # We need to get the result from the LUIS JSON which at every level returns an array.
-                    to_entities = recognizer_result.entities.get("$instance", {}).get(
-                        "To", []
-                    )
-                    if to_entities:
-                        booking_details.destination = to_entities[0]["text"]
-                    from_entities = recognizer_result.entities.get("$instance", {}).get(
-                        "From", []
-                    )
-                    if from_entities:
-                        booking_details.origin = from_entities[0]["text"]
-
-                    # This value will be a TIMEX. And we are only interested in a Date so grab the first result and drop the Time part.
-                    # TIMEX is a format that represents DateTime expressions that include some ambiguity. e.g. missing a Year.
-                    date_entities = recognizer_result.entities.get("$instance", {}).get(
-                        "datetime", []
-                    )
-                    if date_entities:
-                        booking_details.travel_date = (
-                            None
-                        )  # Set when we get a timex format
-        except Exception as exception:
-            print(exception)
-
-        return booking_details
diff --git a/samples/21.corebot-app-insights/main.py b/samples/21.corebot-app-insights/main.py
deleted file mode 100644
index b6d2ccf05..000000000
--- a/samples/21.corebot-app-insights/main.py
+++ /dev/null
@@ -1,93 +0,0 @@
-#!/usr/bin/env python
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-"""
-This sample shows how to create a bot that demonstrates the following:
-- Use [LUIS](https://www.luis.ai) to implement core AI capabilities.
-- Implement a multi-turn conversation using Dialogs.
-- Handle user interruptions for such things as `Help` or `Cancel`.
-- Prompt for and validate requests for information from the user.
-
-"""
-
-import asyncio
-from flask import Flask, request, Response
-from botbuilder.core import (
-    BotFrameworkAdapter,
-    BotFrameworkAdapterSettings,
-    ConversationState,
-    MemoryStorage,
-    UserState,
-    TurnContext,
-)
-from botbuilder.schema import Activity
-from botbuilder.applicationinsights import ApplicationInsightsTelemetryClient
-from botbuilder.applicationinsights.flask import BotTelemetryMiddleware
-
-from dialogs import MainDialog
-from bots import DialogAndWelcomeBot
-
-
-LOOP = asyncio.get_event_loop()
-APP = Flask(__name__, instance_relative_config=True)
-APP.config.from_object("config.DefaultConfig")
-APP.wsgi_app = BotTelemetryMiddleware(APP.wsgi_app)
-
-SETTINGS = BotFrameworkAdapterSettings(APP.config["APP_ID"], APP.config["APP_PASSWORD"])
-ADAPTER = BotFrameworkAdapter(SETTINGS)
-
-# pylint:disable=unused-argument
-async def on_error(context: TurnContext, error: Exception):
-    """ Catch-all for errors."""
-    # Send a message to the user
-    await context.send_activity("Oops. Something went wrong!")
-    # Clear out state
-    await CONVERSATION_STATE.delete(context)
-
-
-ADAPTER.on_turn_error = on_error
-
-# Create MemoryStorage, UserState and ConversationState
-MEMORY = MemoryStorage()
-
-USER_STATE = UserState(MEMORY)
-CONVERSATION_STATE = ConversationState(MEMORY)
-INSTRUMENTATION_KEY = APP.config["APPINSIGHTS_INSTRUMENTATION_KEY"]
-TELEMETRY_CLIENT = ApplicationInsightsTelemetryClient(INSTRUMENTATION_KEY)
-DIALOG = MainDialog(APP.config, telemetry_client=TELEMETRY_CLIENT)
-BOT = DialogAndWelcomeBot(CONVERSATION_STATE, USER_STATE, DIALOG, TELEMETRY_CLIENT)
-
-
-@APP.route("/api/messages", methods=["POST"])
-def messages():
-    """Main bot message handler."""
-    if "application/json" in request.headers["Content-Type"]:
-        body = request.json
-    else:
-        return Response(status=415)
-
-    activity = Activity().deserialize(body)
-    auth_header = (
-        request.headers["Authorization"] if "Authorization" in request.headers else ""
-    )
-
-    async def aux_func(turn_context):
-        await BOT.on_turn(turn_context)
-
-    try:
-        future = asyncio.ensure_future(
-            ADAPTER.process_activity(activity, auth_header, aux_func), loop=LOOP
-        )
-        LOOP.run_until_complete(future)
-        return Response(status=201)
-    except Exception as exception:
-        raise exception
-
-
-if __name__ == "__main__":
-    try:
-        APP.run(debug=True, port=APP.config["PORT"])
-
-    except Exception as exception:
-        raise exception
diff --git a/samples/21.corebot-app-insights/requirements.txt b/samples/21.corebot-app-insights/requirements.txt
deleted file mode 100644
index ffcf72c6b..000000000
--- a/samples/21.corebot-app-insights/requirements.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-Flask>=1.0.2
-asyncio>=3.4.3
-requests>=2.18.1
-botframework-connector>=4.4.0.b1
-botbuilder-schema>=4.4.0.b1
-botbuilder-core>=4.4.0.b1
-botbuilder-dialogs>=4.4.0.b1
-botbuilder-ai>=4.4.0.b1
-botbuilder-applicationinsights>=4.4.0.b1
-datatypes-date-time>=1.0.0.a1
-azure-cognitiveservices-language-luis>=0.2.0
-msrest>=0.6.6
-
diff --git a/samples/45.state-management/README.md b/samples/45.state-management/README.md
deleted file mode 100644
index 30ece10d8..000000000
--- a/samples/45.state-management/README.md
+++ /dev/null
@@ -1,43 +0,0 @@
-# Save user and conversation data
-
-This sample demonstrates how to save user and conversation data in a Python bot.
-The bot maintains conversation state to track and direct the conversation and ask the user questions.
-The bot maintains user state to track the user's answers.
-
-## Running the sample
-- Clone the repository
-```bash
-git clone https://github.com/Microsoft/botbuilder-python.git
-```
-- Run `pip install -r requirements.txt` to install all dependencies
-- Run `python app.py`
-- Alternatively to the last command, you can set the file in an environment variable with `set FLASK_APP=app.py` in windows (`export FLASK_APP=app.py` in mac/linux) and then run `flask run --host=127.0.0.1 --port=3978`
-
-
-### Visual studio code
-- Activate your desired virtual environment
-- Open `botbuilder-python\samples\45.state-management` folder
-- Bring up a terminal, navigate to `botbuilder-python\samples\45.state-management` folder
-- In the terminal, type `pip install -r requirements.txt`
-- In the terminal, type `python app.py`
-
-## Testing the bot using Bot Framework Emulator
-[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel.
-
-- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases)
-
-### Connect to bot using Bot Framework Emulator
-- Launch Bot Framework Emulator
-- Paste this URL in the emulator window - http://localhost:3978/api/messages
-
-
-## Bot State
-
-A key to good bot design is to track the context of a conversation, so that your bot remembers things like the answers to previous questions. Depending on what your bot is used for, you may even need to keep track of state or store information for longer than the lifetime of the conversation. A bot's state is information it remembers in order to respond appropriately to incoming messages. The Bot Builder SDK provides classes for storing and retrieving state data as an object associated with a user or a conversation.
-
-# Further reading
-
-- [Azure Bot Service Introduction](https://docs.microsoft.com/en-us/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0)
-- [Bot State](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-storage-concept?view=azure-bot-service-4.0)
-- [Write directly to storage](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-v4-storage?view=azure-bot-service-4.0&tabs=csharpechorproperty%2Ccsetagoverwrite%2Ccsetag)
-- [Managing conversation and user state](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-v4-state?view=azure-bot-service-4.0)
\ No newline at end of file
diff --git a/samples/45.state-management/app.py b/samples/45.state-management/app.py
deleted file mode 100644
index 04f42895f..000000000
--- a/samples/45.state-management/app.py
+++ /dev/null
@@ -1,87 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-"""
-This sample shows how to manage state in a bot.
-"""
-import asyncio
-import sys
-
-from flask import Flask, request, Response
-from botbuilder.core import (
-    BotFrameworkAdapter,
-    BotFrameworkAdapterSettings,
-    ConversationState,
-    MemoryStorage,
-    TurnContext,
-    UserState,
-)
-from botbuilder.schema import Activity
-
-from bots import StateManagementBot
-
-LOOP = asyncio.get_event_loop()
-APP = Flask(__name__, instance_relative_config=True)
-APP.config.from_object("config.DefaultConfig")
-
-SETTINGS = BotFrameworkAdapterSettings(
-    APP.config["APP_ID"], APP.config["APP_PASSWORD"]
-)
-ADAPTER = BotFrameworkAdapter(SETTINGS)
-
-# Catch-all for errors.
-async def on_error(context: TurnContext, error: Exception):
-    # This check writes out errors to console log
-    # NOTE: In production environment, you should consider logging this to Azure
-    #       application insights.
-    print(f"\n [on_turn_error]: { error }", file=sys.stderr)
-    # Send a message to the user
-    await context.send_activity("Oops. Something went wrong!")
-    # Clear out state
-    await CONVERSATION_STATE.delete(context)
-
-
-ADAPTER.on_turn_error = on_error
-
-# Create MemoryStorage, UserState and ConversationState
-MEMORY = MemoryStorage()
-
-# Commented out user_state because it's not being used.
-USER_STATE = UserState(MEMORY)
-CONVERSATION_STATE = ConversationState(MEMORY)
-
-
-BOT = StateManagementBot(CONVERSATION_STATE, USER_STATE)
-
-
-@APP.route("/api/messages", methods=["POST"])
-def messages():
-    """Main bot message handler."""
-    if "application/json" in request.headers["Content-Type"]:
-        body = request.json
-    else:
-        return Response(status=415)
-
-    activity = Activity().deserialize(body)
-    auth_header = (
-        request.headers["Authorization"] if "Authorization" in request.headers else ""
-    )
-
-    async def aux_func(turn_context):
-        await BOT.on_turn(turn_context)
-
-    try:
-        task = LOOP.create_task(
-            ADAPTER.process_activity(activity, auth_header, aux_func)
-        )
-        LOOP.run_until_complete(task)
-        return Response(status=201)
-    except Exception as exception:
-        raise exception
-
-
-if __name__ == "__main__":
-    try:
-        APP.run(debug=False, port=APP.config["PORT"])  # nosec debug
-    except Exception as exception:
-        raise exception
diff --git a/samples/45.state-management/bots/__init__.py b/samples/45.state-management/bots/__init__.py
deleted file mode 100644
index 535957236..000000000
--- a/samples/45.state-management/bots/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from .state_management_bot import StateManagementBot
-
-__all__ = ["StateManagementBot"]
diff --git a/samples/45.state-management/bots/state_management_bot.py b/samples/45.state-management/bots/state_management_bot.py
deleted file mode 100644
index 40e2640ee..000000000
--- a/samples/45.state-management/bots/state_management_bot.py
+++ /dev/null
@@ -1,98 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import time
-import pytz
-from datetime import datetime
-
-from botbuilder.core import ActivityHandler, ConversationState, TurnContext, UserState
-from botbuilder.schema import ChannelAccount
-
-from data_models import ConversationData, UserProfile
-
-
-class StateManagementBot(ActivityHandler):
-    def __init__(self, conversation_state: ConversationState, user_state: UserState):
-        if conversation_state is None:
-            raise TypeError(
-                "[StateManagementBot]: Missing parameter. conversation_state is required but None was given"
-            )
-        if user_state is None:
-            raise TypeError(
-                "[StateManagementBot]: Missing parameter. user_state is required but None was given"
-            )
-
-        self.conversation_state = conversation_state
-        self.user_state = user_state
-
-        self.conversation_data = self.conversation_state.create_property(
-            "ConversationData"
-        )
-        self.user_profile = self.conversation_state.create_property("UserProfile")
-
-    async def on_turn(self, turn_context: TurnContext):
-        await super().on_turn(turn_context)
-
-        await self.conversation_state.save_changes(turn_context)
-        await self.user_state.save_changes(turn_context)
-
-    async def on_members_added_activity(
-        self, members_added: [ChannelAccount], turn_context: TurnContext
-    ):
-        for member in members_added:
-            if member.id != turn_context.activity.recipient.id:
-                await turn_context.send_activity(
-                    "Welcome to State Bot Sample. Type anything to get started."
-                )
-
-    async def on_message_activity(self, turn_context: TurnContext):
-        # Get the state properties from the turn context.
-        user_profile = await self.user_profile.get(turn_context, UserProfile)
-        conversation_data = await self.conversation_data.get(
-            turn_context, ConversationData
-        )
-
-        if user_profile.name is None:
-            # First time around this is undefined, so we will prompt user for name.
-            if conversation_data.prompted_for_user_name:
-                # Set the name to what the user provided.
-                user_profile.name = turn_context.activity.text
-
-                # Acknowledge that we got their name.
-                await turn_context.send_activity(
-                    f"Thanks { user_profile.name }. To see conversation data, type anything."
-                )
-
-                # Reset the flag to allow the bot to go though the cycle again.
-                conversation_data.prompted_for_user_name = False
-            else:
-                # Prompt the user for their name.
-                await turn_context.send_activity("What is your name?")
-
-                # Set the flag to true, so we don't prompt in the next turn.
-                conversation_data.prompted_for_user_name = True
-        else:
-            # Add message details to the conversation data.
-            conversation_data.timestamp = self.__datetime_from_utc_to_local(
-                turn_context.activity.timestamp
-            )
-            conversation_data.channel_id = turn_context.activity.channel_id
-
-            # Display state data.
-            await turn_context.send_activity(
-                f"{ user_profile.name } sent: { turn_context.activity.text }"
-            )
-            await turn_context.send_activity(
-                f"Message received at: { conversation_data.timestamp }"
-            )
-            await turn_context.send_activity(
-                f"Message received from: { conversation_data.channel_id }"
-            )
-
-    def __datetime_from_utc_to_local(self, utc_datetime):
-        now_timestamp = time.time()
-        offset = datetime.fromtimestamp(now_timestamp) - datetime.utcfromtimestamp(
-            now_timestamp
-        )
-        result = utc_datetime + offset
-        return result.strftime("%I:%M:%S %p, %A, %B %d of %Y")
diff --git a/samples/45.state-management/config.py b/samples/45.state-management/config.py
deleted file mode 100644
index c8c926f07..000000000
--- a/samples/45.state-management/config.py
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/usr/bin/env python3
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import os
-
-""" Bot Configuration """
-
-
-class DefaultConfig(object):
-    """ Bot Configuration """
-
-    PORT = 3978
-    APP_ID = os.environ.get("MicrosoftAppId", "")
-    APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "")
-    LUIS_APP_ID = os.environ.get("LuisAppId", "")
-    LUIS_API_KEY = os.environ.get("LuisAPIKey", "")
-    # LUIS endpoint host name, ie "westus.api.cognitive.microsoft.com"
-    LUIS_API_HOST_NAME = os.environ.get("LuisAPIHostName", "")
diff --git a/samples/45.state-management/data_models/__init__.py b/samples/45.state-management/data_models/__init__.py
deleted file mode 100644
index 4e69f286b..000000000
--- a/samples/45.state-management/data_models/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from .conversation_data import ConversationData
-from .user_profile import UserProfile
-
-__all__ = ["ConversationData", "UserProfile"]
diff --git a/samples/45.state-management/data_models/conversation_data.py b/samples/45.state-management/data_models/conversation_data.py
deleted file mode 100644
index 4b2757e43..000000000
--- a/samples/45.state-management/data_models/conversation_data.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-
-class ConversationData:
-    def __init__(
-        self,
-        timestamp: str = None,
-        channel_id: str = None,
-        prompted_for_user_name: bool = False,
-    ):
-        self.timestamp = timestamp
-        self.channel_id = channel_id
-        self.prompted_for_user_name = prompted_for_user_name
diff --git a/samples/45.state-management/data_models/user_profile.py b/samples/45.state-management/data_models/user_profile.py
deleted file mode 100644
index 36add1ea1..000000000
--- a/samples/45.state-management/data_models/user_profile.py
+++ /dev/null
@@ -1,7 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-
-class UserProfile:
-    def __init__(self, name: str = None):
-        self.name = name
diff --git a/samples/45.state-management/requirements.txt b/samples/45.state-management/requirements.txt
deleted file mode 100644
index 0c4745525..000000000
--- a/samples/45.state-management/requirements.txt
+++ /dev/null
@@ -1 +0,0 @@
-botbuilder-core>=4.4.0b1
\ No newline at end of file
diff --git a/samples/README.md b/samples/README.md
deleted file mode 100644
index 72f1506a9..000000000
--- a/samples/README.md
+++ /dev/null
@@ -1,14 +0,0 @@
-
-# Contributing
-
-This project welcomes contributions and suggestions.  Most contributions require you to agree to a
-Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
-the rights to use your contribution. For details, visit https://cla.microsoft.com.
-
-When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
-a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
-provided by the bot. You will only need to do this once across all repos using our CLA.
-
-This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
-For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
-contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
diff --git a/samples/experimental/101.corebot-bert-bidaf/bot/bots/__init__.py b/samples/experimental/101.corebot-bert-bidaf/bot/bots/__init__.py
deleted file mode 100644
index ee478912d..000000000
--- a/samples/experimental/101.corebot-bert-bidaf/bot/bots/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""bots module."""
-
-from .dialog_bot import DialogBot
-from .dialog_and_welcome_bot import DialogAndWelcomeBot
-
-__all__ = ["DialogBot", "DialogAndWelcomeBot"]
diff --git a/samples/experimental/101.corebot-bert-bidaf/bot/bots/resources/welcomeCard.json b/samples/experimental/101.corebot-bert-bidaf/bot/bots/resources/welcomeCard.json
deleted file mode 100644
index d9a35548c..000000000
--- a/samples/experimental/101.corebot-bert-bidaf/bot/bots/resources/welcomeCard.json
+++ /dev/null
@@ -1,46 +0,0 @@
-{
-  "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
-  "type": "AdaptiveCard",
-  "version": "1.0",
-  "body": [
-    {
-      "type": "Image",
-      "url": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU",
-      "size": "stretch"
-    },
-    {
-      "type": "TextBlock",
-      "spacing": "medium",
-      "size": "default",
-      "weight": "bolder",
-      "text": "Welcome to Bot Framework!",
-      "wrap": true,
-      "maxLines": 0
-    },
-    {
-      "type": "TextBlock",
-      "size": "default",
-      "isSubtle": "true",
-      "text": "Now that you have successfully run your bot, follow the links in this Adaptive Card to expand your knowledge of Bot Framework.",
-      "wrap": true,
-      "maxLines": 0
-    }
-  ],
-  "actions": [
-    {
-      "type": "Action.OpenUrl",
-      "title": "Get an overview",
-      "url": "https://docs.microsoft.com/en-us/azure/bot-service/?view=azure-bot-service-4.0"
-    },
-    {
-      "type": "Action.OpenUrl",
-      "title": "Ask a question",
-      "url": "https://stackoverflow.com/questions/tagged/botframework"
-    },
-    {
-      "type": "Action.OpenUrl",
-      "title": "Learn how to deploy",
-      "url": "https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-deploy-azure?view=azure-bot-service-4.0"
-    }
-  ]
-}
\ No newline at end of file
diff --git a/samples/experimental/101.corebot-bert-bidaf/bot/dialogs/__init__.py b/samples/experimental/101.corebot-bert-bidaf/bot/dialogs/__init__.py
deleted file mode 100644
index d37afdc97..000000000
--- a/samples/experimental/101.corebot-bert-bidaf/bot/dialogs/__init__.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""Dialogs module"""
-from .booking_dialog import BookingDialog
-from .cancel_and_help_dialog import CancelAndHelpDialog
-from .date_resolver_dialog import DateResolverDialog
-from .main_dialog import MainDialog
-
-__all__ = ["BookingDialog", "CancelAndHelpDialog", "DateResolverDialog", "MainDialog"]
diff --git a/samples/experimental/101.corebot-bert-bidaf/bot/helpers/activity_helper.py b/samples/experimental/101.corebot-bert-bidaf/bot/helpers/activity_helper.py
deleted file mode 100644
index bbd0ac848..000000000
--- a/samples/experimental/101.corebot-bert-bidaf/bot/helpers/activity_helper.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""Helper to create reply object."""
-
-from datetime import datetime
-from botbuilder.schema import (
-    Activity,
-    ActivityTypes,
-    ChannelAccount,
-    ConversationAccount,
-)
-
-
-def create_activity_reply(activity: Activity, text: str = None, locale: str = None):
-    """Helper to create reply object."""
-    return Activity(
-        type=ActivityTypes.message,
-        timestamp=datetime.utcnow(),
-        from_property=ChannelAccount(
-            id=getattr(activity.recipient, "id", None),
-            name=getattr(activity.recipient, "name", None),
-        ),
-        recipient=ChannelAccount(
-            id=activity.from_property.id, name=activity.from_property.name
-        ),
-        reply_to_id=activity.id,
-        service_url=activity.service_url,
-        channel_id=activity.channel_id,
-        conversation=ConversationAccount(
-            is_group=activity.conversation.is_group,
-            id=activity.conversation.id,
-            name=activity.conversation.name,
-        ),
-        text=text or "",
-        locale=locale or "",
-        attachments=[],
-        entities=[],
-    )
diff --git a/samples/experimental/101.corebot-bert-bidaf/bot/helpers/dialog_helper.py b/samples/experimental/101.corebot-bert-bidaf/bot/helpers/dialog_helper.py
deleted file mode 100644
index 56ba5b05f..000000000
--- a/samples/experimental/101.corebot-bert-bidaf/bot/helpers/dialog_helper.py
+++ /dev/null
@@ -1,22 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""Utility to run dialogs."""
-from botbuilder.core import StatePropertyAccessor, TurnContext
-from botbuilder.dialogs import Dialog, DialogSet, DialogTurnStatus
-
-
-class DialogHelper:
-    """Dialog Helper implementation."""
-
-    @staticmethod
-    async def run_dialog(
-        dialog: Dialog, turn_context: TurnContext, accessor: StatePropertyAccessor
-    ):  # pylint: disable=line-too-long
-        """Run dialog."""
-        dialog_set = DialogSet(accessor)
-        dialog_set.add(dialog)
-
-        dialog_context = await dialog_set.create_context(turn_context)
-        results = await dialog_context.continue_dialog()
-        if results.status == DialogTurnStatus.Empty:
-            await dialog_context.begin_dialog(dialog.id)
diff --git a/samples/python_django/13.core-bot/README-LUIS.md b/samples/python_django/13.core-bot/README-LUIS.md
deleted file mode 100644
index b6b9b925f..000000000
--- a/samples/python_django/13.core-bot/README-LUIS.md
+++ /dev/null
@@ -1,216 +0,0 @@
-# Setting up LUIS via CLI:
-
-This README contains information on how to create and deploy a LUIS application. When the bot is ready to be deployed to production, we recommend creating a LUIS Endpoint Resource for usage with your LUIS App.
-
-> _For instructions on how to create a LUIS Application via the LUIS portal, see these Quickstart steps:_
-> 1. _[Quickstart: Create a new app in the LUIS portal][Quickstart-create]_
-> 2. _[Quickstart: Deploy an app in the LUIS portal][Quickstart-deploy]_
-
-  [Quickstart-create]: https://docs.microsoft.com/azure/cognitive-services/luis/get-started-portal-build-app
-  [Quickstart-deploy]:https://docs.microsoft.com/azure/cognitive-services/luis/get-started-portal-deploy-app
-
-## Table of Contents:
-
-- [Prerequisites](#Prerequisites)
-- [Import a new LUIS Application using a local LUIS application](#Import-a-new-LUIS-Application-using-a-local-LUIS-application)
-- [How to create a LUIS Endpoint resource in Azure and pair it with a LUIS Application](#How-to-create-a-LUIS-Endpoint-resource-in-Azure-and-pair-it-with-a-LUIS-Application)
-
-___
-
-## [Prerequisites](#Table-of-Contents):
-
-#### Install Azure CLI >=2.0.61:
-
-Visit the following page to find the correct installer for your OS:
-- https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest
-
-#### Install LUIS CLI >=2.4.0:
-
-Open a CLI of your choice and type the following:
-
-```bash
-npm i -g luis-apis@^2.4.0
-```
-
-#### LUIS portal account:
-
-You should already have a LUIS account with either https://luis.ai, https://eu.luis.ai, or https://au.luis.ai. To determine where to create a LUIS account, consider where you will deploy your LUIS applications, and then place them in [the corresponding region][LUIS-Authoring-Regions].
-
-After you've created your account, you need your [Authoring Key][LUIS-AKey] and a LUIS application ID.
-
-  [LUIS-Authoring-Regions]: https://docs.microsoft.com/azure/cognitive-services/luis/luis-reference-regions#luis-authoring-regions]
-  [LUIS-AKey]: https://docs.microsoft.com/azure/cognitive-services/luis/luis-concept-keys#authoring-key
-
-___
-
-## [Import a new LUIS Application using a local LUIS application](#Table-of-Contents)
-
-### 1. Import the local LUIS application to luis.ai
-
-```bash
-luis import application --region "LuisAppAuthoringRegion" --authoringKey "LuisAuthoringKey" --appName "FlightBooking" --in "./cognitiveModels/FlightBooking.json"
-```
-
-Outputs the following JSON:
-
-```json
-{
-    "id": "########-####-####-####-############",
-    "name": "FlightBooking",
-    "description": "A LUIS model that uses intent and entities.",
-    "culture": "en-us",
-    "usageScenario": "",
-    "domain": "",
-    "versionsCount": 1,
-    "createdDateTime": "2019-03-29T18:32:02Z",
-    "endpoints": {},
-    "endpointHitsCount": 0,
-    "activeVersion": "0.1",
-    "ownerEmail": "bot@contoso.com",
-    "tokenizerVersion": "1.0.0"
-}
-```
-
-For the next step, you'll need the `"id"` value for `--appId` and the `"activeVersion"` value for `--versionId`.
-
-### 2. Train the LUIS Application
-
-```bash
-luis train version --region "LuisAppAuthoringRegion" --authoringKey "LuisAuthoringKey" --appId "LuisAppId" --versionId "LuisAppversion" --wait
-```
-
-### 3. Publish the LUIS Application
-
-```bash
-luis publish version --region "LuisAppAuthoringRegion" --authoringKey "LuisAuthoringKey" --appId "LuisAppId" --versionId "LuisAppversion" --publishRegion "LuisAppPublishRegion"
-```
-
-> `--region` corresponds to the region you _author_ your application in. The regions available for this are "westus", "westeurope" and "australiaeast". 
-> These regions correspond to the three available portals, https://luis.ai, https://eu.luis.ai, or https://au.luis.ai. 
-> `--publishRegion` corresponds to the region of the endpoint you're publishing to, (e.g. "westus", "southeastasia", "westeurope", "brazilsouth"). 
-> See the [reference docs][Endpoint-API] for a list of available publish/endpoint regions.
-
-  [Endpoint-API]: https://westus.dev.cognitive.microsoft.com/docs/services/5819c76f40a6350ce09de1ac/operations/5819c77140a63516d81aee78
-
-Outputs the following:
-
-```json
- {
-    "versionId": "0.1",
-    "isStaging": false,
-    "endpointUrl": "https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/########-####-####-####-############",
-    "region": "westus",
-    "assignedEndpointKey": null,
-    "endpointRegion": "westus",
-    "failedRegions": "",
-    "publishedDateTime": "2019-03-29T18:40:32Z",
-    "directVersionPublish": false
-}
-```
-
-To see how to create an LUIS Cognitive Service Resource in Azure, please see [the next README][README-LUIS]. This Resource should be used when you want to move your bot to production. The instructions will show you how to create and pair the resource with a LUIS Application.
-
-  [README-LUIS]: ./README-LUIS.md
-
-___
-
-## [How to create a LUIS Endpoint resource in Azure and pair it with a LUIS Application](#Table-of-Contents)
-
-### 1. Create a new LUIS Cognitive Services resource on Azure via Azure CLI
-
-> _Note:_ 
-> _If you don't have a Resource Group in your Azure subscription, you can create one through the Azure portal or through using:_
-> ```bash
-> az group create --subscription "AzureSubscriptionGuid" --location "westus" --name "ResourceGroupName"
-> ```
-> _To see a list of valid locations, use `az account list-locations`_
-
-
-```bash
-# Use Azure CLI to create the LUIS Key resource on Azure
-az cognitiveservices account create --kind "luis" --name "NewLuisResourceName" --sku "S0" --location "westus" --subscription "AzureSubscriptionGuid" -g "ResourceGroupName"
-```
-
-The command will output a response similar to the JSON below:
-
-```json
-{
-  "endpoint": "https://westus.api.cognitive.microsoft.com/luis/v2.0",
-  "etag": "\"########-####-####-####-############\"",
-  "id": "/subscriptions/########-####-####-####-############/resourceGroups/ResourceGroupName/providers/Microsoft.CognitiveServices/accounts/NewLuisResourceName",
-  "internalId": "################################",
-  "kind": "luis",
-  "location": "westus",
-  "name": "NewLuisResourceName",
-  "provisioningState": "Succeeded",
-  "resourceGroup": "ResourceGroupName",
-  "sku": {
-    "name": "S0",
-    "tier": null
-  },
-  "tags": null,
-  "type": "Microsoft.CognitiveServices/accounts"
-}
-```
-
-
-
-Take the output from the previous command and create a JSON file in the following format:
-
-```json
-{
-    "azureSubscriptionId": "00000000-0000-0000-0000-000000000000",
-    "resourceGroup": "ResourceGroupName",
-    "accountName": "NewLuisResourceName"
-}
-```
-
-### 2. Retrieve ARM access token via Azure CLI
-
-```bash
-az account get-access-token --subscription "AzureSubscriptionGuid"
-```
-
-This will return an object that looks like this:
-
-```json
-{
-  "accessToken": "eyJ0eXAiOiJKVtokentokentokentokentokeng1dCI6Ik4tbEMwbi05REFMcXdodUhZbkhRNjNHZUNYYyIsItokenI6Ik4tbEMwbi05REFMcXdodUhZbkhRNjNHZUNYYyJ9.eyJhdWQiOiJodHRwczovL21hbmFnZW1lbnQuY29yZS53aW5kb3dzLm5ldC8iLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDcvIiwiaWF0IjoxNTUzODc3MTUwLCJuYmYiOjE1NTM4NzcxNTAsImV4cCI6MTU1Mzg4MTA1MCwiX2NsYWltX25hbWVzIjp7Imdyb3VwcyI6InNyYzEifSwiX2NsYWltX3NvdXJjZXMiOnsic3JjMSI6eyJlbmRwb2ludCI6Imh0dHBzOi8vZ3JhcGgud2luZG93cy5uZXQvNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3L3VzZXJzL2ZmZTQyM2RkLWJhM2YtNDg0Ny04NjgyLWExNTI5MDA4MjM4Ny9nZXRNZW1iZXJPYmplY3RzIn19LCJhY3IiOiIxIiwiYWlvIjoiQVZRQXEvOEtBQUFBeGVUc201NDlhVHg4RE1mMFlRVnhGZmxxOE9RSC9PODR3QktuSmRqV1FqTkkwbmxLYzB0bHJEZzMyMFZ5bWZGaVVBSFBvNUFFUTNHL0FZNDRjdk01T3M0SEt0OVJkcE5JZW9WU0dzd0kvSkk9IiwiYW1yIjpbIndpYSIsIm1mYSJdLCJhcHBpZCI6IjA0YjA3Nzk1LThkZGItNDYxYS1iYmVlLTAyZjllMWJmN2I0NiIsImFwcGlkYWNyIjoiMCIsImRldmljZWlkIjoiNDhmNDVjNjEtMTg3Zi00MjUxLTlmZWItMTllZGFkZmMwMmE3IiwiZmFtaWx5X25hbWUiOiJHdW0iLCJnaXZlbl9uYW1lIjoiU3RldmVuIiwiaXBhZGRyIjoiMTY3LjIyMC4yLjU1IiwibmFtZSI6IlN0ZXZlbiBHdW0iLCJvaWQiOiJmZmU0MjNkZC1iYTNmLTQ4NDctODY4Mi1hMTUyOTAwODIzODciLCJvbnByZW1fc2lkIjoiUy0xLTUtMjEtMjEyNzUyMTE4NC0xNjA0MDEyOTIwLTE4ODc5Mjc1MjctMjYwOTgyODUiLCJwdWlkIjoiMTAwMzdGRkVBMDQ4NjlBNyIsInJoIjoiSSIsInNjcCI6InVzZXJfaW1wZXJzb25hdGlvbiIsInN1YiI6Ik1rMGRNMWszN0U5ckJyMjhieUhZYjZLSU85LXVFQVVkZFVhNWpkSUd1Nk0iLCJ0aWQiOiI3MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDciLCJ1bmlxdWVfbmFtZSI6InN0Z3VtQG1pY3Jvc29mdC5jb20iLCJ1cG4iOiJzdGd1bUBtaWNyb3NvZnQuY29tIiwidXRpIjoiT2w2NGN0TXY4RVNEQzZZQWRqRUFtokenInZlciI6IjEuMCJ9.kFAsEilE0mlS1pcpqxf4rEnRKeYsehyk-gz-zJHUrE__oad3QjgDSBDPrR_ikLdweynxbj86pgG4QFaHURNCeE6SzrbaIrNKw-n9jrEtokenlosOxg_0l2g1LeEUOi5Q4gQREAU_zvSbl-RY6sAadpOgNHtGvz3Rc6FZRITfkckSLmsKAOFoh-aWC6tFKG8P52rtB0qVVRz9tovBeNqkMYL49s9ypduygbXNVwSQhm5JszeWDgrFuVFHBUP_iENCQYGQpEZf_KvjmX1Ur1F9Eh9nb4yI2gFlKncKNsQl-tokenK7-tokentokentokentokentokentokenatoken",
-  "expiresOn": "2200-12-31 23:59:59.999999",
-  "subscription": "AzureSubscriptionGuid",
-  "tenant": "tenant-guid",
-  "tokenType": "Bearer"
-}
-```
-
-The value needed for the next step is the `"accessToken"`.
-
-### 3. Use `luis add appazureaccount` to pair your LUIS resource with a LUIS Application
-
-```bash
-luis add appazureaccount --in "path/to/created/requestBody.json" --appId "LuisAppId" --authoringKey "LuisAuthoringKey" --armToken "accessToken"
-```
-
-If successful, it should yield a response like this:
-
-```json
-{
-  "code": "Success",
-  "message": "Operation Successful"
-}
-```
-
-### 4. See the LUIS Cognitive Services' keys
-
-```bash
-az cognitiveservices account keys list --name "NewLuisResourceName" --subscription "AzureSubscriptionGuid" -g "ResourceGroupName"
-```
-
-This will return an object that looks like this:
-
-```json
-{
-  "key1": "9a69####dc8f####8eb4####399f####",
-  "key2": "####f99e####4b1a####fb3b####6b9f"
-}
-```
\ No newline at end of file
diff --git a/samples/python_django/13.core-bot/README.md b/samples/python_django/13.core-bot/README.md
deleted file mode 100644
index 1724d8d04..000000000
--- a/samples/python_django/13.core-bot/README.md
+++ /dev/null
@@ -1,61 +0,0 @@
-# CoreBot
-
-Bot Framework v4 core bot sample.
-
-This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to:
-
-- Use [LUIS](https://www.luis.ai) to implement core AI capabilities
-- Implement a multi-turn conversation using Dialogs
-- Handle user interruptions for such things as `Help` or `Cancel`
-- Prompt for and validate requests for information from the user
-
-## Prerequisites
-
-This sample **requires** prerequisites in order to run.
-
-### Overview
-
-This bot uses [LUIS](https://www.luis.ai), an AI based cognitive service, to implement language understanding.
-
-### Install Python 3.6
-
-
-### Create a LUIS Application to enable language understanding
-
-LUIS language model setup, training, and application configuration steps can be found [here](https://docs.microsoft.com/azure/bot-service/bot-builder-howto-v4-luis?view=azure-bot-service-4.0&tabs=cs).
-
-If you wish to create a LUIS application via the CLI, these steps can be found in the [README-LUIS.md](README-LUIS.md).
-
-
-### Configure your bot to use your LUIS app
-
-Update config.py with your newly imported LUIS app id, LUIS API key from https:///user/settings, LUIS API host name, ie .api.cognitive.microsoft.com.  LUIS authoring region is listed on https:///user/settings.
-
-
-## Testing the bot using Bot Framework Emulator
-
-[Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel.
-
-- Install the Bot Framework Emulator version 4.3.0 or greater from [here](https://github.com/Microsoft/BotFramework-Emulator/releases)
-
-### Connect to the bot using Bot Framework Emulator
-
-- Launch Bot Framework Emulator
-- File -> Open Bot
-- Enter a Bot URL of `http://localhost:3978/api/messages`
-
-
-## Further reading
-
-- [Bot Framework Documentation](https://docs.botframework.com)
-- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0)
-- [Dialogs](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-dialog?view=azure-bot-service-4.0)
-- [Gathering Input Using Prompts](https://docs.microsoft.com/azure/bot-service/bot-builder-prompts?view=azure-bot-service-4.0&tabs=csharp)
-- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0)
-- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0)
-- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0)
-- [.NET Core CLI tools](https://docs.microsoft.com/dotnet/core/tools/?tabs=netcore2x)
-- [Azure CLI](https://docs.microsoft.com/cli/azure/?view=azure-cli-latest)
-- [Azure Portal](https://portal.azure.com)
-- [Language Understanding using LUIS](https://docs.microsoft.com/azure/cognitive-services/luis/)
-- [Channels and Bot Connector Service](https://docs.microsoft.com/azure/bot-service/bot-concepts?view=azure-bot-service-4.0)
\ No newline at end of file
diff --git a/samples/python_django/13.core-bot/booking_details.py b/samples/python_django/13.core-bot/booking_details.py
deleted file mode 100644
index 4502ee974..000000000
--- a/samples/python_django/13.core-bot/booking_details.py
+++ /dev/null
@@ -1,15 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-"""Booking detail."""
-
-
-class BookingDetails:
-    """Booking detail implementation"""
-
-    def __init__(
-        self, destination: str = None, origin: str = None, travel_date: str = None
-    ):
-        self.destination = destination
-        self.origin = origin
-        self.travel_date = travel_date
diff --git a/samples/python_django/13.core-bot/bots/__init__.py b/samples/python_django/13.core-bot/bots/__init__.py
deleted file mode 100644
index ee478912d..000000000
--- a/samples/python_django/13.core-bot/bots/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""bots module."""
-
-from .dialog_bot import DialogBot
-from .dialog_and_welcome_bot import DialogAndWelcomeBot
-
-__all__ = ["DialogBot", "DialogAndWelcomeBot"]
diff --git a/samples/python_django/13.core-bot/bots/bots.py b/samples/python_django/13.core-bot/bots/bots.py
deleted file mode 100644
index a1d783449..000000000
--- a/samples/python_django/13.core-bot/bots/bots.py
+++ /dev/null
@@ -1,54 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-""" Bot initialization """
-# pylint: disable=line-too-long
-import sys
-import asyncio
-from django.apps import AppConfig
-from botbuilder.core import (
-    BotFrameworkAdapter,
-    BotFrameworkAdapterSettings,
-    TurnContext,
-    ConversationState,
-    MemoryStorage,
-    UserState,
-)
-from dialogs import MainDialog
-from bots import DialogAndWelcomeBot
-import config
-
-
-class BotConfig(AppConfig):
-    """ Bot initialization """
-
-    name = "bots"
-    appConfig = config.DefaultConfig
-
-    SETTINGS = BotFrameworkAdapterSettings(appConfig.APP_ID, appConfig.APP_PASSWORD)
-    ADAPTER = BotFrameworkAdapter(SETTINGS)
-    LOOP = asyncio.get_event_loop()
-
-    # Create MemoryStorage, UserState and ConversationState
-    memory = MemoryStorage()
-    user_state = UserState(memory)
-    conversation_state = ConversationState(memory)
-
-    dialog = MainDialog(appConfig)
-    bot = DialogAndWelcomeBot(conversation_state, user_state, dialog)
-
-    async def on_error(self, context: TurnContext, error: Exception):
-        """
-        Catch-all for errors.
-        This check writes out errors to console log
-        NOTE: In production environment, you should consider logging this to Azure
-        application insights.
-        """
-        print(f"\n [on_turn_error]: { error }", file=sys.stderr)
-        # Send a message to the user
-        await context.send_activity("Oops. Something went wrong!")
-        # Clear out state
-        await self.conversation_state.delete(context)
-
-    def ready(self):
-        self.ADAPTER.on_turn_error = self.on_error
diff --git a/samples/python_django/13.core-bot/bots/dialog_and_welcome_bot.py b/samples/python_django/13.core-bot/bots/dialog_and_welcome_bot.py
deleted file mode 100644
index fe030d056..000000000
--- a/samples/python_django/13.core-bot/bots/dialog_and_welcome_bot.py
+++ /dev/null
@@ -1,44 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""Main dialog to welcome users."""
-import json
-import os.path
-from typing import List
-from botbuilder.core import TurnContext
-from botbuilder.schema import Activity, Attachment, ChannelAccount
-from helpers.activity_helper import create_activity_reply
-from .dialog_bot import DialogBot
-
-
-class DialogAndWelcomeBot(DialogBot):
-    """Main dialog to welcome users implementation."""
-
-    async def on_members_added_activity(
-        self, members_added: List[ChannelAccount], turn_context: TurnContext
-    ):
-        for member in members_added:
-            # Greet anyone that was not the target (recipient) of this message.
-            # To learn more about Adaptive Cards, see https://aka.ms/msbot-adaptivecards
-            # for more details.
-            if member.id != turn_context.activity.recipient.id:
-                welcome_card = self.create_adaptive_card_attachment()
-                response = self.create_response(turn_context.activity, welcome_card)
-                await turn_context.send_activity(response)
-
-    def create_response(self, activity: Activity, attachment: Attachment):
-        """Create an attachment message response."""
-        response = create_activity_reply(activity)
-        response.attachments = [attachment]
-        return response
-
-    # Load attachment from file.
-    def create_adaptive_card_attachment(self):
-        """Create an adaptive card."""
-        relative_path = os.path.abspath(os.path.dirname(__file__))
-        path = os.path.join(relative_path, "resources/welcomeCard.json")
-        with open(path) as card_file:
-            card = json.load(card_file)
-
-        return Attachment(
-            content_type="application/vnd.microsoft.card.adaptive", content=card
-        )
diff --git a/samples/python_django/13.core-bot/bots/dialog_bot.py b/samples/python_django/13.core-bot/bots/dialog_bot.py
deleted file mode 100644
index f8b221e87..000000000
--- a/samples/python_django/13.core-bot/bots/dialog_bot.py
+++ /dev/null
@@ -1,47 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""Implements bot Activity handler."""
-
-from botbuilder.core import ActivityHandler, ConversationState, UserState, TurnContext
-from botbuilder.dialogs import Dialog
-from helpers.dialog_helper import DialogHelper
-
-
-class DialogBot(ActivityHandler):
-    """Main activity handler for the bot."""
-
-    def __init__(
-        self,
-        conversation_state: ConversationState,
-        user_state: UserState,
-        dialog: Dialog,
-    ):
-        if conversation_state is None:
-            raise Exception(
-                "[DialogBot]: Missing parameter. conversation_state is required"
-            )
-        if user_state is None:
-            raise Exception("[DialogBot]: Missing parameter. user_state is required")
-        if dialog is None:
-            raise Exception("[DialogBot]: Missing parameter. dialog is required")
-
-        self.conversation_state = conversation_state
-        self.user_state = user_state
-        self.dialog = dialog
-        self.dialogState = self.conversation_state.create_property(
-            "DialogState"
-        )  # pylint: disable=C0103
-
-    async def on_turn(self, turn_context: TurnContext):
-        await super().on_turn(turn_context)
-
-        # Save any state changes that might have occured during the turn.
-        await self.conversation_state.save_changes(turn_context, False)
-        await self.user_state.save_changes(turn_context, False)
-
-    async def on_message_activity(self, turn_context: TurnContext):
-        await DialogHelper.run_dialog(
-            self.dialog,
-            turn_context,
-            self.conversation_state.create_property("DialogState"),
-        )  # pylint: disable=C0103
diff --git a/samples/python_django/13.core-bot/bots/resources/welcomeCard.json b/samples/python_django/13.core-bot/bots/resources/welcomeCard.json
deleted file mode 100644
index 100aa5287..000000000
--- a/samples/python_django/13.core-bot/bots/resources/welcomeCard.json
+++ /dev/null
@@ -1,46 +0,0 @@
-{
-  "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
-  "type": "AdaptiveCard",
-  "version": "1.0",
-  "body": [
-    {
-      "type": "Image",
-      "url": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU",
-      "size": "stretch"
-    },
-    {
-      "type": "TextBlock",
-      "spacing": "medium",
-      "size": "default",
-      "weight": "bolder",
-      "text": "Welcome to Bot Framework!",
-      "wrap": true,
-      "maxLines": 0
-    },
-    {
-      "type": "TextBlock",
-      "size": "default",
-      "isSubtle": "true",
-      "text": "Now that you have successfully run your bot, follow the links in this Adaptive Card to expand your knowledge of Bot Framework.",
-      "wrap": true,
-      "maxLines": 0
-    }
-  ],
-  "actions": [
-    {
-      "type": "Action.OpenUrl",
-      "title": "Get an overview",
-      "url": "https://docs.microsoft.com/en-us/azure/bot-service/?view=azure-bot-service-4.0"
-    },
-    {
-      "type": "Action.OpenUrl",
-      "title": "Ask a question",
-      "url": "https://stackoverflow.com/questions/tagged/botframework"
-    },
-    {
-      "type": "Action.OpenUrl",
-      "title": "Learn how to deploy",
-      "url": "https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-deploy-azure?view=azure-bot-service-4.0"
-    }
-  ]
-}
\ No newline at end of file
diff --git a/samples/python_django/13.core-bot/bots/settings.py b/samples/python_django/13.core-bot/bots/settings.py
deleted file mode 100644
index 99fd265c7..000000000
--- a/samples/python_django/13.core-bot/bots/settings.py
+++ /dev/null
@@ -1,118 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-"""
-Django settings for bots project.
-
-Generated by 'django-admin startproject' using Django 2.2.1.
-
-For more information on this file, see
-https://docs.djangoproject.com/en/2.2/topics/settings/
-
-For the full list of settings and their values, see
-https://docs.djangoproject.com/en/2.2/ref/settings/
-"""
-
-import os
-
-# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
-BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
-
-
-# Quick-start development settings - unsuitable for production
-# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
-
-# SECURITY WARNING: keep the secret key used in production secret!
-SECRET_KEY = "My Secret Key"
-
-# SECURITY WARNING: don't run with debug turned on in production!
-DEBUG = True
-
-ALLOWED_HOSTS = []
-
-
-# Application definition
-
-INSTALLED_APPS = [
-    "django.contrib.admin",
-    "django.contrib.auth",
-    "django.contrib.contenttypes",
-    "django.contrib.sessions",
-    "django.contrib.messages",
-    "django.contrib.staticfiles",
-    "bots.bots.BotConfig",
-]
-
-MIDDLEWARE = [
-    "django.middleware.security.SecurityMiddleware",
-    "django.contrib.sessions.middleware.SessionMiddleware",
-    "django.middleware.common.CommonMiddleware",
-    "django.middleware.csrf.CsrfViewMiddleware",
-    "django.contrib.auth.middleware.AuthenticationMiddleware",
-    "django.contrib.messages.middleware.MessageMiddleware",
-    "django.middleware.clickjacking.XFrameOptionsMiddleware",
-]
-
-ROOT_URLCONF = "bots.urls"
-
-TEMPLATES = [
-    {
-        "BACKEND": "django.template.backends.django.DjangoTemplates",
-        "DIRS": [],
-        "APP_DIRS": True,
-        "OPTIONS": {
-            "context_processors": [
-                "django.template.context_processors.debug",
-                "django.template.context_processors.request",
-                "django.contrib.auth.context_processors.auth",
-                "django.contrib.messages.context_processors.messages",
-            ]
-        },
-    }
-]
-
-WSGI_APPLICATION = "bots.wsgi.application"
-
-
-# Database
-# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
-
-DATABASES = {
-    "default": {
-        "ENGINE": "django.db.backends.sqlite3",
-        "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
-    }
-}
-
-
-# Password validation
-# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
-
-AUTH_PASSWORD_VALIDATORS = [
-    {
-        "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
-    },
-    {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
-    {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
-    {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
-]
-
-
-# Internationalization
-# https://docs.djangoproject.com/en/2.2/topics/i18n/
-
-LANGUAGE_CODE = "en-us"
-
-TIME_ZONE = "UTC"
-
-USE_I18N = True
-
-USE_L10N = True
-
-USE_TZ = True
-
-
-# Static files (CSS, JavaScript, Images)
-# https://docs.djangoproject.com/en/2.2/howto/static-files/
-
-STATIC_URL = "/static/"
diff --git a/samples/python_django/13.core-bot/bots/urls.py b/samples/python_django/13.core-bot/bots/urls.py
deleted file mode 100644
index 99cf42018..000000000
--- a/samples/python_django/13.core-bot/bots/urls.py
+++ /dev/null
@@ -1,15 +0,0 @@
-#!/usr/bin/env python
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-""" URL configuration for bot message handler """
-
-from django.urls import path
-from django.views.decorators.csrf import csrf_exempt
-from . import views
-
-# pylint:disable=invalid-name
-urlpatterns = [
-    path("", views.home, name="home"),
-    path("api/messages", csrf_exempt(views.messages), name="messages"),
-]
diff --git a/samples/python_django/13.core-bot/bots/views.py b/samples/python_django/13.core-bot/bots/views.py
deleted file mode 100644
index 04f354424..000000000
--- a/samples/python_django/13.core-bot/bots/views.py
+++ /dev/null
@@ -1,53 +0,0 @@
-#!/usr/bin/env python
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-"""
-This sample shows how to create a bot that demonstrates the following:
-- Use [LUIS](https://www.luis.ai) to implement core AI capabilities.
-- Implement a multi-turn conversation using Dialogs.
-- Handle user interruptions for such things as `Help` or `Cancel`.
-- Prompt for and validate requests for information from the user.
-"""
-
-import asyncio
-import json
-from django.http import HttpResponse
-from django.apps import apps
-from botbuilder.schema import Activity
-
-# pylint: disable=line-too-long
-def home():
-    """Default handler."""
-    return HttpResponse("Hello!")
-
-
-def messages(request):
-    """Main bot message handler."""
-    if "application/json" in request.headers["Content-Type"]:
-        body = json.loads(request.body.decode("utf-8"))
-    else:
-        return HttpResponse(status=415)
-
-    activity = Activity().deserialize(body)
-    auth_header = (
-        request.headers["Authorization"] if "Authorization" in request.headers else ""
-    )
-
-    bot_app = apps.get_app_config("bots")
-    bot = bot_app.bot
-    loop = bot_app.LOOP
-    adapter = bot_app.ADAPTER
-
-    async def aux_func(turn_context):
-        await bot.on_turn(turn_context)
-
-    try:
-        task = asyncio.ensure_future(
-            adapter.process_activity(activity, auth_header, aux_func), loop=loop
-        )
-        loop.run_until_complete(task)
-        return HttpResponse(status=201)
-    except Exception as exception:
-        raise exception
-    return HttpResponse("This is message processing!")
diff --git a/samples/python_django/13.core-bot/bots/wsgi.py b/samples/python_django/13.core-bot/bots/wsgi.py
deleted file mode 100644
index 869e12e78..000000000
--- a/samples/python_django/13.core-bot/bots/wsgi.py
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/usr/bin/env python
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-"""
-WSGI config for bots project.
-
-It exposes the WSGI callable as a module-level variable named ``application``.
-
-For more information on this file, see
-https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/
-"""
-
-import os
-from django.core.wsgi import get_wsgi_application
-
-# pylint:disable=invalid-name
-os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bots.settings")
-application = get_wsgi_application()
diff --git a/samples/python_django/13.core-bot/cognitiveModels/FlightBooking.json b/samples/python_django/13.core-bot/cognitiveModels/FlightBooking.json
deleted file mode 100644
index 0a0d6c4a7..000000000
--- a/samples/python_django/13.core-bot/cognitiveModels/FlightBooking.json
+++ /dev/null
@@ -1,226 +0,0 @@
-{
-  "luis_schema_version": "3.2.0",
-  "versionId": "0.1",
-  "name": "Airline Reservation",
-  "desc": "A LUIS model that uses intent and entities.",
-  "culture": "en-us",
-  "tokenizerVersion": "1.0.0",
-  "intents": [
-    {
-      "name": "Book flight"
-    },
-    {
-      "name": "Cancel"
-    },
-    {
-      "name": "None"
-    }
-  ],
-  "entities": [],
-  "composites": [
-    {
-      "name": "From",
-      "children": [
-        "Airport"
-      ],
-      "roles": []
-    },
-    {
-      "name": "To",
-      "children": [
-        "Airport"
-      ],
-      "roles": []
-    }
-  ],
-  "closedLists": [
-    {
-      "name": "Airport",
-      "subLists": [
-        {
-          "canonicalForm": "Paris",
-          "list": [
-            "paris"
-          ]
-        },
-        {
-          "canonicalForm": "London",
-          "list": [
-            "london"
-          ]
-        },
-        {
-          "canonicalForm": "Berlin",
-          "list": [
-            "berlin"
-          ]
-        },
-        {
-          "canonicalForm": "New York",
-          "list": [
-            "new york"
-          ]
-        }
-      ],
-      "roles": []
-    }
-  ],
-  "patternAnyEntities": [],
-  "regex_entities": [],
-  "prebuiltEntities": [
-    {
-      "name": "datetimeV2",
-      "roles": []
-    }
-  ],
-  "model_features": [],
-  "regex_features": [],
-  "patterns": [],
-  "utterances": [
-    {
-      "text": "book flight from london to paris on feb 14th",
-      "intent": "Book flight",
-      "entities": [
-        {
-          "entity": "To",
-          "startPos": 27,
-          "endPos": 31
-        },
-        {
-          "entity": "From",
-          "startPos": 17,
-          "endPos": 22
-        }
-      ]
-    },
-    {
-      "text": "book flight to berlin on feb 14th",
-      "intent": "Book flight",
-      "entities": [
-        {
-          "entity": "To",
-          "startPos": 15,
-          "endPos": 20
-        }
-      ]
-    },
-    {
-      "text": "book me a flight from london to paris",
-      "intent": "Book flight",
-      "entities": [
-        {
-          "entity": "From",
-          "startPos": 22,
-          "endPos": 27
-        },
-        {
-          "entity": "To",
-          "startPos": 32,
-          "endPos": 36
-        }
-      ]
-    },
-    {
-      "text": "bye",
-      "intent": "Cancel",
-      "entities": []
-    },
-    {
-      "text": "cancel booking",
-      "intent": "Cancel",
-      "entities": []
-    },
-    {
-      "text": "exit",
-      "intent": "Cancel",
-      "entities": []
-    },
-    {
-      "text": "flight to paris",
-      "intent": "Book flight",
-      "entities": [
-        {
-          "entity": "To",
-          "startPos": 10,
-          "endPos": 14
-        }
-      ]
-    },
-    {
-      "text": "flight to paris from london on feb 14th",
-      "intent": "Book flight",
-      "entities": [
-        {
-          "entity": "To",
-          "startPos": 10,
-          "endPos": 14
-        },
-        {
-          "entity": "From",
-          "startPos": 21,
-          "endPos": 26
-        }
-      ]
-    },
-    {
-      "text": "fly from berlin to paris on may 5th",
-      "intent": "Book flight",
-      "entities": [
-        {
-          "entity": "To",
-          "startPos": 19,
-          "endPos": 23
-        },
-        {
-          "entity": "From",
-          "startPos": 9,
-          "endPos": 14
-        }
-      ]
-    },
-    {
-      "text": "go to paris",
-      "intent": "Book flight",
-      "entities": [
-        {
-          "entity": "To",
-          "startPos": 6,
-          "endPos": 10
-        }
-      ]
-    },
-    {
-      "text": "going from paris to berlin",
-      "intent": "Book flight",
-      "entities": [
-        {
-          "entity": "To",
-          "startPos": 20,
-          "endPos": 25
-        },
-        {
-          "entity": "From",
-          "startPos": 11,
-          "endPos": 15
-        }
-      ]
-    },
-    {
-      "text": "ignore",
-      "intent": "Cancel",
-      "entities": []
-    },
-    {
-      "text": "travel to paris",
-      "intent": "Book flight",
-      "entities": [
-        {
-          "entity": "To",
-          "startPos": 10,
-          "endPos": 14
-        }
-      ]
-    }
-  ],
-  "settings": []
-}
\ No newline at end of file
diff --git a/samples/python_django/13.core-bot/config.py b/samples/python_django/13.core-bot/config.py
deleted file mode 100644
index c2dbd7827..000000000
--- a/samples/python_django/13.core-bot/config.py
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/usr/bin/env python3
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-""" Bot Configuration """
-
-
-class DefaultConfig(object):
-    """ Bot Configuration """
-
-    PORT = 3978
-    APP_ID = ""
-    APP_PASSWORD = ""
-
-    LUIS_APP_ID = ""
-    # LUIS authoring key from LUIS portal or LUIS Cognitive Service subscription key
-    LUIS_API_KEY = ""
-    # LUIS endpoint host name, ie "https://westus.api.cognitive.microsoft.com"
-    LUIS_API_HOST_NAME = ""
diff --git a/samples/python_django/13.core-bot/db.sqlite3 b/samples/python_django/13.core-bot/db.sqlite3
deleted file mode 100644
index e69de29bb..000000000
diff --git a/samples/python_django/13.core-bot/dialogs/__init__.py b/samples/python_django/13.core-bot/dialogs/__init__.py
deleted file mode 100644
index 88d9489fd..000000000
--- a/samples/python_django/13.core-bot/dialogs/__init__.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""Dialogs module"""
-from .booking_dialog import BookingDialog
-from .cancel_and_help_dialog import CancelAndHelpDialog
-from .date_resolver_dialog import DateResolverDialog
-from .main_dialog import MainDialog
-
-__all__ = ["BookingDialog", "CancelAndHelpDialog", "DateResolverDialog", "MainDialog"]
diff --git a/samples/python_django/13.core-bot/dialogs/booking_dialog.py b/samples/python_django/13.core-bot/dialogs/booking_dialog.py
deleted file mode 100644
index 8b345fd7c..000000000
--- a/samples/python_django/13.core-bot/dialogs/booking_dialog.py
+++ /dev/null
@@ -1,119 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""Flight booking dialog."""
-
-from botbuilder.dialogs import WaterfallDialog, WaterfallStepContext, DialogTurnResult
-from botbuilder.dialogs.prompts import ConfirmPrompt, TextPrompt, PromptOptions
-from botbuilder.core import MessageFactory
-from datatypes_date_time.timex import Timex
-from .cancel_and_help_dialog import CancelAndHelpDialog
-from .date_resolver_dialog import DateResolverDialog
-
-
-class BookingDialog(CancelAndHelpDialog):
-    """Flight booking implementation."""
-
-    def __init__(self, dialog_id: str = None):
-        super(BookingDialog, self).__init__(dialog_id or BookingDialog.__name__)
-
-        self.add_dialog(TextPrompt(TextPrompt.__name__))
-        # self.add_dialog(ConfirmPrompt(ConfirmPrompt.__name__))
-        self.add_dialog(DateResolverDialog(DateResolverDialog.__name__))
-        self.add_dialog(
-            WaterfallDialog(
-                WaterfallDialog.__name__,
-                [
-                    self.destination_step,
-                    self.origin_step,
-                    self.travel_date_step,
-                    # self.confirm_step,
-                    self.final_step,
-                ],
-            )
-        )
-
-        self.initial_dialog_id = WaterfallDialog.__name__
-
-    async def destination_step(
-        self, step_context: WaterfallStepContext
-    ) -> DialogTurnResult:
-        """Prompt for destination."""
-        booking_details = step_context.options
-
-        if booking_details.destination is None:
-            return await step_context.prompt(
-                TextPrompt.__name__,
-                PromptOptions(
-                    prompt=MessageFactory.text("To what city would you like to travel?")
-                ),
-            )  # pylint: disable=line-too-long,bad-continuation
-        else:
-            return await step_context.next(booking_details.destination)
-
-    async def origin_step(self, step_context: WaterfallStepContext) -> DialogTurnResult:
-        """Prompt for origin city."""
-        booking_details = step_context.options
-
-        # Capture the response to the previous step's prompt
-        booking_details.destination = step_context.result
-        if booking_details.origin is None:
-            return await step_context.prompt(
-                TextPrompt.__name__,
-                PromptOptions(
-                    prompt=MessageFactory.text("From what city will you be travelling?")
-                ),
-            )  # pylint: disable=line-too-long,bad-continuation
-        else:
-            return await step_context.next(booking_details.origin)
-
-    async def travel_date_step(
-        self, step_context: WaterfallStepContext
-    ) -> DialogTurnResult:
-        """Prompt for travel date.
-        This will use the DATE_RESOLVER_DIALOG."""
-
-        booking_details = step_context.options
-
-        # Capture the results of the previous step
-        booking_details.origin = step_context.result
-        if not booking_details.travel_date or self.is_ambiguous(
-            booking_details.travel_date
-        ):
-            return await step_context.begin_dialog(
-                DateResolverDialog.__name__, booking_details.travel_date
-            )  # pylint: disable=line-too-long
-        else:
-            return await step_context.next(booking_details.travel_date)
-
-    async def confirm_step(
-        self, step_context: WaterfallStepContext
-    ) -> DialogTurnResult:
-        """Confirm the information the user has provided."""
-        booking_details = step_context.options
-
-        # Capture the results of the previous step
-        booking_details.travel_date = step_context.result
-        msg = (
-            f"Please confirm, I have you traveling to: { booking_details.destination }"
-            f" from: { booking_details.origin } on: { booking_details.travel_date}."
-        )
-
-        # Offer a YES/NO prompt.
-        return await step_context.prompt(
-            ConfirmPrompt.__name__, PromptOptions(prompt=MessageFactory.text(msg))
-        )
-
-    async def final_step(self, step_context: WaterfallStepContext) -> DialogTurnResult:
-        """Complete the interaction and end the dialog."""
-        if step_context.result:
-            booking_details = step_context.options
-            booking_details.travel_date = step_context.result
-
-            return await step_context.end_dialog(booking_details)
-        else:
-            return await step_context.end_dialog()
-
-    def is_ambiguous(self, timex: str) -> bool:
-        """Ensure time is correct."""
-        timex_property = Timex(timex)
-        return "definite" not in timex_property.types
diff --git a/samples/python_django/13.core-bot/dialogs/cancel_and_help_dialog.py b/samples/python_django/13.core-bot/dialogs/cancel_and_help_dialog.py
deleted file mode 100644
index 35cb15ec2..000000000
--- a/samples/python_django/13.core-bot/dialogs/cancel_and_help_dialog.py
+++ /dev/null
@@ -1,45 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""Handle cancel and help intents."""
-from botbuilder.dialogs import (
-    ComponentDialog,
-    DialogContext,
-    DialogTurnResult,
-    DialogTurnStatus,
-)
-from botbuilder.schema import ActivityTypes
-
-
-class CancelAndHelpDialog(ComponentDialog):
-    """Implementation of handling cancel and help."""
-
-    async def on_begin_dialog(
-        self, inner_dc: DialogContext, options: object
-    ) -> DialogTurnResult:
-        result = await self.interrupt(inner_dc)
-        if result is not None:
-            return result
-
-        return await super(CancelAndHelpDialog, self).on_begin_dialog(inner_dc, options)
-
-    async def on_continue_dialog(self, inner_dc: DialogContext) -> DialogTurnResult:
-        result = await self.interrupt(inner_dc)
-        if result is not None:
-            return result
-
-        return await super(CancelAndHelpDialog, self).on_continue_dialog(inner_dc)
-
-    async def interrupt(self, inner_dc: DialogContext) -> DialogTurnResult:
-        """Detect interruptions."""
-        if inner_dc.context.activity.type == ActivityTypes.message:
-            text = inner_dc.context.activity.text.lower()
-
-            if text == "help" or text == "?":
-                await inner_dc.context.send_activity("Show Help...")
-                return DialogTurnResult(DialogTurnStatus.Waiting)
-
-            if text == "cancel" or text == "quit":
-                await inner_dc.context.send_activity("Cancelling")
-                return await inner_dc.cancel_all_dialogs()
-
-        return None
diff --git a/samples/python_django/13.core-bot/dialogs/date_resolver_dialog.py b/samples/python_django/13.core-bot/dialogs/date_resolver_dialog.py
deleted file mode 100644
index 6dc683c91..000000000
--- a/samples/python_django/13.core-bot/dialogs/date_resolver_dialog.py
+++ /dev/null
@@ -1,82 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""Handle date/time resolution for booking dialog."""
-from botbuilder.core import MessageFactory
-from botbuilder.dialogs import WaterfallDialog, DialogTurnResult, WaterfallStepContext
-from botbuilder.dialogs.prompts import (
-    DateTimePrompt,
-    PromptValidatorContext,
-    PromptOptions,
-    DateTimeResolution,
-)
-from datatypes_date_time.timex import Timex
-from .cancel_and_help_dialog import CancelAndHelpDialog
-
-
-class DateResolverDialog(CancelAndHelpDialog):
-    """Resolve the date"""
-
-    def __init__(self, dialog_id: str = None):
-        super(DateResolverDialog, self).__init__(
-            dialog_id or DateResolverDialog.__name__
-        )
-
-        self.add_dialog(
-            DateTimePrompt(
-                DateTimePrompt.__name__, DateResolverDialog.datetime_prompt_validator
-            )
-        )
-        self.add_dialog(
-            WaterfallDialog(
-                WaterfallDialog.__name__ + "2", [self.initial_step, self.final_step]
-            )
-        )
-
-        self.initial_dialog_id = WaterfallDialog.__name__ + "2"
-
-    async def initial_step(
-        self, step_context: WaterfallStepContext
-    ) -> DialogTurnResult:
-        """Prompt for the date."""
-        timex = step_context.options
-
-        prompt_msg = "On what date would you like to travel?"
-        reprompt_msg = (
-            "I'm sorry, for best results, please enter your travel "
-            "date including the month, day and year."
-        )
-
-        if timex is None:
-            # We were not given any date at all so prompt the user.
-            return await step_context.prompt(
-                DateTimePrompt.__name__,
-                PromptOptions(  # pylint: disable=bad-continuation
-                    prompt=MessageFactory.text(prompt_msg),
-                    retry_prompt=MessageFactory.text(reprompt_msg),
-                ),
-            )
-        else:
-            # We have a Date we just need to check it is unambiguous.
-            if "definite" in Timex(timex).types:
-                # This is essentially a "reprompt" of the data we were given up front.
-                return await step_context.prompt(
-                    DateTimePrompt.__name__, PromptOptions(prompt=reprompt_msg)
-                )
-            else:
-                return await step_context.next(DateTimeResolution(timex=timex))
-
-    async def final_step(self, step_context: WaterfallStepContext):
-        """Cleanup - set final return value and end dialog."""
-        timex = step_context.result[0].timex
-        return await step_context.end_dialog(timex)
-
-    @staticmethod
-    async def datetime_prompt_validator(prompt_context: PromptValidatorContext) -> bool:
-        """ Validate the date provided is in proper form. """
-        if prompt_context.recognized.succeeded:
-            timex = prompt_context.recognized.value[0].timex.split("T")[0]
-
-            # TODO: Needs TimexProperty
-            return "definite" in Timex(timex).types
-
-        return False
diff --git a/samples/python_django/13.core-bot/dialogs/main_dialog.py b/samples/python_django/13.core-bot/dialogs/main_dialog.py
deleted file mode 100644
index e92fe58a4..000000000
--- a/samples/python_django/13.core-bot/dialogs/main_dialog.py
+++ /dev/null
@@ -1,83 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""Main dialog. """
-from botbuilder.dialogs import (
-    ComponentDialog,
-    WaterfallDialog,
-    WaterfallStepContext,
-    DialogTurnResult,
-)
-from botbuilder.dialogs.prompts import TextPrompt, PromptOptions
-from botbuilder.core import MessageFactory
-from booking_details import BookingDetails
-from helpers.luis_helper import LuisHelper
-from .booking_dialog import BookingDialog
-
-
-class MainDialog(ComponentDialog):
-    """Main dialog. """
-
-    def __init__(self, configuration: dict, dialog_id: str = None):
-        super(MainDialog, self).__init__(dialog_id or MainDialog.__name__)
-
-        self._configuration = configuration
-
-        self.add_dialog(TextPrompt(TextPrompt.__name__))
-        self.add_dialog(BookingDialog())
-        self.add_dialog(
-            WaterfallDialog(
-                "WFDialog", [self.intro_step, self.act_step, self.final_step]
-            )
-        )
-
-        self.initial_dialog_id = "WFDialog"
-
-    async def intro_step(self, step_context: WaterfallStepContext) -> DialogTurnResult:
-        """Initial prompt."""
-        return await step_context.prompt(
-            TextPrompt.__name__,
-            PromptOptions(
-                prompt=MessageFactory.text("What can I help you with today?")
-            ),
-        )
-
-    async def act_step(self, step_context: WaterfallStepContext) -> DialogTurnResult:
-        """Use language understanding to gather details about booking."""
-
-        # In this sample we only have a single Intent we are concerned with.
-        # However, typically a scenario will have multiple different Intents
-        # each corresponding to starting a different child Dialog.
-        booking_details = (
-            await LuisHelper.execute_luis_query(
-                self._configuration, step_context.context
-            )
-            if step_context.result is not None
-            else BookingDetails()
-        )
-
-        # Run the BookingDialog giving it whatever details we have from the
-        # model.  The dialog will prompt to find out the remaining details.
-        return await step_context.begin_dialog(BookingDialog.__name__, booking_details)
-
-    async def final_step(self, step_context: WaterfallStepContext) -> DialogTurnResult:
-        """Complete dialog.
-        At this step, with details from the user, display the completed
-        flight booking to the user.
-        """
-        # If the child dialog ("BookingDialog") was cancelled or the user failed
-        # to confirm, the Result here will be null.
-        if step_context.result is not None:
-            result = step_context.result
-
-            # Now we have all the booking details call the booking service.
-            # If the call to the booking service was successful tell the user.
-            # time_property = Timex(result.travel_date)
-            # travel_date_msg = time_property.to_natural_language(datetime.now())
-            msg = (
-                f"I have you booked to {result.destination} from"
-                f" {result.origin} on {result.travel_date}."
-            )
-            await step_context.context.send_activity(MessageFactory.text(msg))
-        else:
-            await step_context.context.send_activity(MessageFactory.text("Thank you."))
-        return await step_context.end_dialog()
diff --git a/samples/python_django/13.core-bot/helpers/__init__.py b/samples/python_django/13.core-bot/helpers/__init__.py
deleted file mode 100644
index 1ef1e54a6..000000000
--- a/samples/python_django/13.core-bot/helpers/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-"""Helpers module."""
-from . import activity_helper, luis_helper, dialog_helper
-
-__all__ = ["activity_helper", "dialog_helper", "luis_helper"]
diff --git a/samples/python_django/13.core-bot/helpers/activity_helper.py b/samples/python_django/13.core-bot/helpers/activity_helper.py
deleted file mode 100644
index 78353902e..000000000
--- a/samples/python_django/13.core-bot/helpers/activity_helper.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""Helper to create reply object."""
-
-from datetime import datetime
-from botbuilder.schema import (
-    Activity,
-    ActivityTypes,
-    ChannelAccount,
-    ConversationAccount,
-)
-
-
-def create_activity_reply(activity: Activity, text: str = None, locale: str = None):
-    """Helper to create reply object."""
-    return Activity(
-        type=ActivityTypes.message,
-        timestamp=datetime.utcnow(),
-        from_property=ChannelAccount(
-            id=getattr(activity.recipient, "id", None),
-            name=getattr(activity.recipient, "name", None),
-        ),
-        recipient=ChannelAccount(
-            id=activity.from_property.id, name=activity.from_property.name
-        ),
-        reply_to_id=activity.id,
-        service_url=activity.service_url,
-        channel_id=activity.channel_id,
-        conversation=ConversationAccount(
-            is_group=activity.conversation.is_group,
-            id=activity.conversation.id,
-            name=activity.conversation.name,
-        ),
-        text=text or "",
-        locale=locale or "",
-        attachments=[],
-        entities=[],
-    )
diff --git a/samples/python_django/13.core-bot/helpers/luis_helper.py b/samples/python_django/13.core-bot/helpers/luis_helper.py
deleted file mode 100644
index 45a3ab5e5..000000000
--- a/samples/python_django/13.core-bot/helpers/luis_helper.py
+++ /dev/null
@@ -1,63 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-"""Helper to call LUIS service."""
-from botbuilder.ai.luis import LuisRecognizer, LuisApplication
-from botbuilder.core import TurnContext
-
-from booking_details import BookingDetails
-
-# pylint: disable=line-too-long
-class LuisHelper:
-    """LUIS helper implementation."""
-
-    @staticmethod
-    async def execute_luis_query(
-        configuration, turn_context: TurnContext
-    ) -> BookingDetails:
-        """Invoke LUIS service to perform prediction/evaluation of utterance."""
-        booking_details = BookingDetails()
-
-        # pylint:disable=broad-except
-        try:
-            luis_application = LuisApplication(
-                configuration.LUIS_APP_ID,
-                configuration.LUIS_API_KEY,
-                configuration.LUIS_API_HOST_NAME,
-            )
-
-            recognizer = LuisRecognizer(luis_application)
-            recognizer_result = await recognizer.recognize(turn_context)
-
-            if recognizer_result.intents:
-                intent = sorted(
-                    recognizer_result.intents,
-                    key=recognizer_result.intents.get,
-                    reverse=True,
-                )[:1][0]
-                if intent == "Book_flight":
-                    # We need to get the result from the LUIS JSON which at every level returns an array.
-                    to_entities = recognizer_result.entities.get("$instance", {}).get(
-                        "To", []
-                    )
-                    if to_entities:
-                        booking_details.destination = to_entities[0]["text"]
-                    from_entities = recognizer_result.entities.get("$instance", {}).get(
-                        "From", []
-                    )
-                    if from_entities:
-                        booking_details.origin = from_entities[0]["text"]
-
-                    # This value will be a TIMEX. And we are only interested in a Date so grab the first result and drop the Time part.
-                    # TIMEX is a format that represents DateTime expressions that include some ambiguity. e.g. missing a Year.
-                    date_entities = recognizer_result.entities.get("$instance", {}).get(
-                        "datetime", []
-                    )
-                    if date_entities:
-                        booking_details.travel_date = (
-                            None
-                        )  # Set when we get a timex format
-        except Exception as exception:
-            print(exception)
-
-        return booking_details
diff --git a/samples/python_django/13.core-bot/manage.py b/samples/python_django/13.core-bot/manage.py
deleted file mode 100644
index 5b6b9621b..000000000
--- a/samples/python_django/13.core-bot/manage.py
+++ /dev/null
@@ -1,28 +0,0 @@
-#!/usr/bin/env python
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-"""Django's command-line utility for administrative tasks."""
-import os
-import sys
-from django.core.management.commands.runserver import Command as runserver
-import config
-
-
-def main():
-    """Django's command-line utility for administrative tasks."""
-    runserver.default_port = config.DefaultConfig.PORT
-    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bots.settings")
-    try:
-        from django.core.management import execute_from_command_line
-    except ImportError as exc:
-        raise ImportError(
-            "Couldn't import Django. Are you sure it's installed and "
-            "available on your PYTHONPATH environment variable? Did you "
-            "forget to activate a virtual environment?"
-        ) from exc
-    execute_from_command_line(sys.argv)
-
-
-if __name__ == "__main__":
-    main()
diff --git a/samples/python_django/13.core-bot/requirements.txt b/samples/python_django/13.core-bot/requirements.txt
deleted file mode 100644
index bc7fd496e..000000000
--- a/samples/python_django/13.core-bot/requirements.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-Django>=2.2.1
-requests>=2.18.1
-botframework-connector>=4.4.0.b1
-botbuilder-schema>=4.4.0.b1
-botbuilder-core>=4.4.0.b1
-botbuilder-dialogs>=4.4.0.b1
-botbuilder-ai>=4.4.0.b1
-datatypes-date-time>=1.0.0.a1
-azure-cognitiveservices-language-luis>=0.2.0
\ No newline at end of file
diff --git a/specs/DailyBuildProposal.md b/specs/DailyBuildProposal.md
new file mode 100644
index 000000000..f68e154ad
--- /dev/null
+++ b/specs/DailyBuildProposal.md
@@ -0,0 +1,58 @@
+# Daily Build Propsal for .Net BotBuilder SDK
+
+This proposal describes our plan to publish daily builds for consumption. The goals of this are:
+1. Make it easy for developers (1P and 3P) to consume our daily builds.
+2. Exercise our release process frequently, so issues don't arise at critical times.
+3. Meet Developers where they are.
+
+Use the [ASP.Net Team](https://github.com/dotnet/aspnetcore/blob/master/docs/DailyBuilds.md) as inspiration, and draft off the work they do.
+
+# Versioning
+Move to Python suggested versioning for dailies defined in [PEP440](https://www.python.org/dev/peps/pep-0440/#developmental-releases).
+
+The tags we use for preview versions are:
+```
+..dev{incrementing value}
+-rc{incrementing value}
+```
+
+# Daily Builds
+All our Python wheel packages would be pushed to the SDK_Public project at [fuselabs.visualstudio.com](https://fuselabs.visualstudio.com).
+
+    Note: Only a public project on Devops can have a public feed. The public project on our Enterprise Tenant is [SDK_Public](https://fuselabs.visualstudio.com/SDK_Public).
+
+This means developers could add this feed their projects by adding the following command on a pip conf file, or in the pip command itself:
+
+```bash
+extra-index-url=https://pkgs.dev.azure.com/ConversationalAI/BotFramework/_packaging/SDK%40Local/pypi/simple/
+```
+
+## Debugging
+To debug daily builds in VSCode:
+* In the launch.json configuration file set the option `"justMyCode": false`.
+
+## Daily Build Lifecyle
+Daily builds older than 90 days are automatically deleted.
+
+# Summary - Weekly Builds
+Once per week, preferably on a Monday, a daily build is pushed to PyPI test. This build happens from 'main', the same as a standard daily build. This serves 2 purposes:
+
+1. Keeps PyPI "Fresh" for people that don't want daily builds.
+2. Keeps the release pipelines active and working, and prevents issues.
+
+These builds will have the "-dev" tag and ARE the the daily build.
+
+**This release pipeline should be the EXACT same pipeline that releases our production bits.**
+
+Weekly builds older than 1 year should be automatically delisted.
+
+## Adding packages to the feed
+Our existing Release pipelines would add packages to the feed.
+# Migration from MyGet
+
+1. Initially, our daily builds should go to both MyGet and Azure Devops.
+2. Our docs are updated once builds are in both locations.
+3. Towards the end of 2020, we stop publising to MyGet.
+
+# Containers
+ASP.Net and .Net Core 5 also publish a container to [Docker Hub](https://hub.docker.com/_/microsoft-dotnet-nightly-aspnet/) as part of their daily feed. We should consider that, along with our samples, in the next iteration of this work.
diff --git a/samples/experimental/101.corebot-bert-bidaf/Dockerfile_bot b/tests/experimental/101.corebot-bert-bidaf/Dockerfile_bot
similarity index 96%
rename from samples/experimental/101.corebot-bert-bidaf/Dockerfile_bot
rename to tests/experimental/101.corebot-bert-bidaf/Dockerfile_bot
index 322372e67..1ce39d22e 100644
--- a/samples/experimental/101.corebot-bert-bidaf/Dockerfile_bot
+++ b/tests/experimental/101.corebot-bert-bidaf/Dockerfile_bot
@@ -1,35 +1,35 @@
-FROM    tiangolo/uwsgi-nginx-flask:python3.6
-
-# Setup for nginx
-RUN  mkdir -p /home/LogFiles \
-     && apt update \
-     && apt install -y --no-install-recommends vim 
-
-EXPOSE 3978
- 
-COPY /model /model
-
-# Pytorch very large.  Install from wheel.
-RUN wget https://files.pythonhosted.org/packages/69/60/f685fb2cfb3088736bafbc9bdbb455327bdc8906b606da9c9a81bae1c81e/torch-1.1.0-cp36-cp36m-manylinux1_x86_64.whl
-RUN pip3 install torch-1.1.0-cp36-cp36m-manylinux1_x86_64.whl
-
-RUN pip3 install -e /model/
-
-
-COPY ./bot /bot
-
-RUN  pip3 install -r /bot/requirements.txt
-
-ENV FLASK_APP=/bot/main.py
-ENV LANG=C.UTF-8
-ENV LC_ALL=C.UTF-8
-ENV PATH ${PATH}:/home/site/wwwroot
-
-WORKDIR bot
-# Initialize models
-
-
-# For Debugging, uncomment the following: 
-#ENTRYPOINT ["python3.6", "-c", "import time ; time.sleep(500000)"]
-ENTRYPOINT [ "flask" ]
-CMD [ "run", "--port", "3978", "--host", "0.0.0.0" ]
+FROM    tiangolo/uwsgi-nginx-flask:python3.6
+
+# Setup for nginx
+RUN  mkdir -p /home/LogFiles \
+     && apt update \
+     && apt install -y --no-install-recommends vim 
+
+EXPOSE 3978
+ 
+COPY /model /model
+
+# Pytorch very large.  Install from wheel.
+RUN wget https://files.pythonhosted.org/packages/69/60/f685fb2cfb3088736bafbc9bdbb455327bdc8906b606da9c9a81bae1c81e/torch-1.1.0-cp36-cp36m-manylinux1_x86_64.whl
+RUN pip3 install torch-1.1.0-cp36-cp36m-manylinux1_x86_64.whl
+
+RUN pip3 install -e /model/
+
+
+COPY ./bot /bot
+
+RUN  pip3 install -r /bot/requirements.txt
+
+ENV FLASK_APP=/bot/main.py
+ENV LANG=C.UTF-8
+ENV LC_ALL=C.UTF-8
+ENV PATH ${PATH}:/home/site/wwwroot
+
+WORKDIR bot
+# Initialize models
+
+
+# For Debugging, uncomment the following: 
+#ENTRYPOINT ["python3.6", "-c", "import time ; time.sleep(500000)"]
+ENTRYPOINT [ "flask" ]
+CMD [ "run", "--port", "3978", "--host", "0.0.0.0" ]
diff --git a/samples/experimental/101.corebot-bert-bidaf/Dockerfile_model_runtime b/tests/experimental/101.corebot-bert-bidaf/Dockerfile_model_runtime
similarity index 97%
rename from samples/experimental/101.corebot-bert-bidaf/Dockerfile_model_runtime
rename to tests/experimental/101.corebot-bert-bidaf/Dockerfile_model_runtime
index c1a22217f..ed777a1d2 100644
--- a/samples/experimental/101.corebot-bert-bidaf/Dockerfile_model_runtime
+++ b/tests/experimental/101.corebot-bert-bidaf/Dockerfile_model_runtime
@@ -1,29 +1,29 @@
-# https://github.com/tornadoweb/tornado/blob/master/demos/blog/Dockerfile
-FROM python:3.6
-
-# Port the model runtime service will listen on.
-EXPOSE 8880
-
-# Make structure where the models will live.
-RUN mkdir -p /cognitiveModels/bert
-RUN mkdir -p /cognitiveModels/bidaf
-
-# Copy and install models.
-COPY model /model/
-#RUN pip3 install --upgrade pip
-#RUN pip3 install --upgrade nltk
-RUN wget https://files.pythonhosted.org/packages/69/60/f685fb2cfb3088736bafbc9bdbb455327bdc8906b606da9c9a81bae1c81e/torch-1.1.0-cp36-cp36m-manylinux1_x86_64.whl
-RUN pip3 install torch-1.1.0-cp36-cp36m-manylinux1_x86_64.whl
-RUN pip3 install -e /model
-
-# Copy and install model runtime service api.
-COPY model_runtime_svc /model_runtime_svc/
-RUN pip3 install -e /model_runtime_svc
-
-# One time initialization of the models.
-RUN python3 /model_runtime_svc/model_runtime_svc_corebot101/docker_init.py 
-RUN rm /model_runtime_svc/model_runtime_svc_corebot101/docker_init.py 
-
-WORKDIR /model_runtime_svc
-
+# https://github.com/tornadoweb/tornado/blob/master/demos/blog/Dockerfile
+FROM python:3.6
+
+# Port the model runtime service will listen on.
+EXPOSE 8880
+
+# Make structure where the models will live.
+RUN mkdir -p /cognitiveModels/bert
+RUN mkdir -p /cognitiveModels/bidaf
+
+# Copy and install models.
+COPY model /model/
+#RUN pip3 install --upgrade pip
+#RUN pip3 install --upgrade nltk
+RUN wget https://files.pythonhosted.org/packages/69/60/f685fb2cfb3088736bafbc9bdbb455327bdc8906b606da9c9a81bae1c81e/torch-1.1.0-cp36-cp36m-manylinux1_x86_64.whl
+RUN pip3 install torch-1.1.0-cp36-cp36m-manylinux1_x86_64.whl
+RUN pip3 install -e /model
+
+# Copy and install model runtime service api.
+COPY model_runtime_svc /model_runtime_svc/
+RUN pip3 install -e /model_runtime_svc
+
+# One time initialization of the models.
+RUN python3 /model_runtime_svc/model_runtime_svc_corebot101/docker_init.py 
+RUN rm /model_runtime_svc/model_runtime_svc_corebot101/docker_init.py 
+
+WORKDIR /model_runtime_svc
+
 ENTRYPOINT ["python3", "./model_runtime_svc_corebot101/main.py"]
\ No newline at end of file
diff --git a/samples/experimental/101.corebot-bert-bidaf/NOTICE.md b/tests/experimental/101.corebot-bert-bidaf/NOTICE.md
similarity index 100%
rename from samples/experimental/101.corebot-bert-bidaf/NOTICE.md
rename to tests/experimental/101.corebot-bert-bidaf/NOTICE.md
diff --git a/samples/experimental/101.corebot-bert-bidaf/README.md b/tests/experimental/101.corebot-bert-bidaf/README.md
similarity index 97%
rename from samples/experimental/101.corebot-bert-bidaf/README.md
rename to tests/experimental/101.corebot-bert-bidaf/README.md
index 4d66d258b..501f8d600 100644
--- a/samples/experimental/101.corebot-bert-bidaf/README.md
+++ b/tests/experimental/101.corebot-bert-bidaf/README.md
@@ -1,349 +1,349 @@
-# CoreBot-bert-bidaf
-
-Bot Framework v4 core bot sample demonstrating using open source language models employing the BERT and BiDAF.  This is for demonstration purposes only.  
-
-## Table of Contents
-- [Overview](#overview)
-- [Terminology](#terminology)
-- [Setup](#setup)
-- [Model Development](#model-development)
-- [Model Runtime Options](#model-runtime-options)
-  - [In-Process](#in-process)
-  - [Out-of-process to local service](#out-of-process-to-local-service)
-  - [Using Docker Containers](#using-docker-containers)
-
-
-## Overview
-This bot has been created using [Bot Framework](https://dev.botframework.com).  It demonstrates the following:
-- Train a one layer logistic regression classifier on top of the pretrained BERT model to infer intents.
-- Use locally the ONNX BiDAF pre-trained model to infer entities for the scenario, in this case simple flight booking.
-- Run the bot with the model runtime in-process to the bot.
-- Run the bot with the model runtime external to the bot.
-
-## Terminology
-This document uses the following terminology. 
-**Model Development**: Model Development broadly covers gathering data, data processing, training/validation/evaluation and testing.  This can also be thought of as model preparation or authoring time.
-**Model Runtime**: The built model which can be used to perform inferences against bot utterances.  The model runtime refers to the model and the associated code to perform inferences. The model runtime is used when the bot is running to infer intents and entities.
-**Inference**: Applying the bot utterance to a model yields intents and entities.  The intents and entities are the inferences used by the bot.
-
-## Setup
-
-This sample uses the Anaconda environment (which provides Jupyter Lab and other machine learning tools) in order to run.
-
-The following instructions assume using the [Anaconda]() environment (v4.6.11+). 
-
-Note: Be sure to install the **64-bit** version of Anaconda for the purposes of this tutorial.
-
-### Create and activate virtual environment
-
-In your local folder, open an **Anaconda prompt** and run the following commands:
-
-```bash
-cd 101.corebot-bert-bidaf
-conda create -n botsample python=3.6 anaconda -y
-conda activate botsample # source conda 
-
-# Add extension to handle Jupyter kernel based on the new environemnt.
-pip install ipykernel
-ipython kernel install --user --name=botsample
-
-# Add extension for visual controls to display correctly
-conda install -c conda-forge nodejs -y
-jupyter labextension install @jupyter-widgets/jupyterlab-manager
-```
-
-From here on out, all CLI interactions should occur within the `botsample` Anaconda virtual environment.
-
-### Install  models package
-The `models` package contains source to perform model development support and runtime inferencing using the tuned BERT and BiDAF models.
-
-
-```bash
-# Install Pytorch
-conda install -c pytorch pytorch -y  
-
-# Install models package using code in sample
-# This will create the python package that contains all the 
-# models used in the Jupyter Notebooks and the Bot code.
-cd model
-pip install -e . # Note the '.' after -e
-
-# Verify packages installed
-   # On Windows:
-   conda list | findstr "corebot pytorch onnx"
-
-   # On Linux/etc:
-   conda list | grep -e corebot -e pytorch -e onnx
-```
-
-You should see something like:
-```bash
-model-corebot101          0.0.1                    dev_0    
-onnx                      1.5.0                    pypi_0    pypi
-onnxruntime               0.4.0                    pypi_0    pypi
-pytorch                   1.1.0           py3.6_cuda100_cudnn7_1    pytorch
-pytorch-pretrained-bert   0.6.2                    pypi_0    pypi
-```
-
-## Model Development
-Model development in this sample involves building a BERT classifier model and is performed in the Juypter Lab environment.
-
-### Training in Jupyter Lab
-Training the model can be performed in Jupyter Lab.
-Within the Anaconda shell, launch Jupyter Lab from the sample directory.
-
-```bash
-# Start JupyterLab from the root of the sample directory
-(botsample) 101.corebot-bert-bidaf> jupyter lab
-
-```
-#### Click on `notebooks` folder  in the left hand navigation of JupyterLab
-
-
-  Click for screen shot.
-    Selecting notebooks folder in Jupyter
-     -
- 
-
-#### Click on `bert_train.ipynb` notebook
-If running the first time, you should select the `botsample` environment.
-
-  Click for screen shot.
-    Selecting Anaconda `botsample` environment for Jupyter Kernel
-     -
- 
-
-#### Train the model
-
-To build the BERT classifier model, run the Jupyterlab Notebok (Run->**Run All Cells**).
-
-
-  Click for screen shot.
-    Selecting Model to build folder
-     -
- 
-
-This process may take several minutes.  The built model is placed into a directory that will get packaged with the bot during deployment.   The sample demonstrates using this package in-process to the bot, our of process in a separate host that performs inferences, and within Jupyter Notebooks.
-
-After running the Jupyter Notebook, the output should resemble something like the following:
-
-  Click for screen shot.
-    Showing Completed Model Build
-     -
- 
-
-
-
-#### Test the BERT runtime model classification
-Once the model has been built, you can test the model with a separately provided Jupyter Notebook (Run->Run All Cells).
-- Within the `notebooks` folder, select the `bert_model_runtime.ipynb` file.
-- Run the notebook.
-
-[]
-
-- The output shows intents (`Book flight`, `Cancel`) that will be used by the bot.
-
-- Add additional test cases to see how phrases will be inferenced.
-
-- To modify the training data, update the file below.  The format of this is compatible with the LUIS schema. When done modifying the training data [re-train the model](#train-the-model) before testing.
-  `101.corebot-bert-bidaf/model/model_corebot101/bert/training_data/FlightBooking.json`
-  
-  [Click to edit Training Data.](./model/model_corebot101/bert/training_data/FlightBooking.json)
-
-> **NOTE**: The default file output location for the tuned BERT model is `/models/bert`.
-
-### Test the BiDAF runtime model classification
-Similarly, you can test the BiDAF model.  Note there is no explicit data processing for the bidaf model.  The entities detected are configured at runtime in the notebook.
-
-[]
-
-> **NOTE**: The default file output location for the BiDAF model is `/models/bidaf`.
-
-
-
-## Model Runtime Options
-
-The sample can host the model runtime within the bot process or out-of-process in a REST API service.  The following sections demonstrate how to do this on the local machine.  In addition, the sample provides Dockerfiles to run this sample in containers.
-
-### In-process
-Within an Anaconda environment (bring up a new Anaconda shell and activate your virtual environment if you would like to continue having JupyterLab running in the original shell), install dependencies for the bot:
-```bash
-# Install requirements required for the bot
-(botsample) 101.corebot-bert-bidaf> pip install -r requirements.txt
-```
-> **NOTE**: If `requirements.txt` doesn't install, you may have to stop JupyterLab if it's running.
-
-```bash
-# Run the bot
-(botsample) 101.corebot-bert-bidaf> cd bot
-(botsample) 101.corebot-bert-bidaf\bot> python main.py
-```
-
-
-> **NOTE**: If executing `main.py` with Python above doesn't work, try running Flask directly:
->
-> ```bash
-> # Set FLASK_APP with full path to main.py in the sample directory
-> # On linux, use export instead of set.
-> (botsample) 101.corebot-bert-bidaf> set FLASK_APP=main.py 
-> 
-> # Turn on development 
-> (botsample) 101.corebot-bert-bidaf> set FLASK_ENV=development
-> 
-> # Run flask
-> (botsample) 101.corebot-bert-bidaf> flask run  
-> ```
-
-At this point, you can [test the bot](#testing-the-bot-using-bot-framework-emulator).
-
-### Out-of-process to local service
-Sometimes it's helpful to host the model outside the bot's process space and serve inferences from a separate process.  
-
-This section builds on the previous section of [In-process](#in-process).
-
-#### Stop any running bot/model runtime processes
-Ensure there are no running bot or model runtimes executing.  Hit ^C on any Anaconda shells running flask/bot or the model service runtime (`python main.py`).
-
-#### Modify bot configuration for localhost
-To call the out-of-process REST API, the bot configuration is modified. Edit the following file:
-`101.corebot-bert-bidaf/bot/config.py`
-
-Edit the settings for `USE_MODEL_RUNTIME_SERVICE` and set to `True`.
-
-```python
-class DefaultConfig(object):
-    """Bot configuration parameters."""
-    # TCP port that the bot listens on (default:3978)
-    PORT = 3978
-
-    # Azure Application ID (not required if running locally)
-    APP_ID = ""
-    # Azure Application Password (not required if running locally)
-    APP_PASSWORD = ""
-
-    # Determines if the bot calls the models in-proc to the bot or call out of process
-    # to the service api using a REST API.
-    USE_MODEL_RUNTIME_SERVICE = True
-    # Host serving the out-of-process model runtime service api.
-    MODEL_RUNTIME_SERVICE_HOST = "localhost"
-    # TCP serving the out-of-process model runtime service api.
-    MODEL_RUNTIME_SERVICE_PORT = 8880
-```
-#### Set up model runtime service
-Inside a separate Anaconda shell, activate the `botsample` environment, and install the model runtime service.
-
-```bash
-# Install requirements required for model runtime service
-(botsample) 101.corebot-bert-bidaf> cd model_runtime_svc
-(botsample) 101.corebot-bert-bidaf\model_runtime_svc> pip install -e . # Note the dot after the -e switch
-```
-
-#### Run model runtime service
-To run the model runtime service, execute the following:
-```bash
-# Navigate into the model_runtime_svc_corebot101 folder
-cd model_runtime_svc_corebot101
-
-# From 101.corebot-bert-bidaf\model_runtime_svc\model_runtime_svc_corebot101
-python main.py
-```
-If not already running, create a separate Anaconda shell set to the  `botsample` environment and [run the local bot](#local-in-process) as described above.  If it was already running, ensure [the configuration changes made above](#modify-bot-configuration) are running.
-
-At this point, you can [test the bot](#testing-the-bot-using-bot-framework-emulator).
-
-### Using Docker Containers
-This sample also demonstrates using Docker and Docker Compose to run a bot and model runtime service on the local host, that communicate together. 
-
-> **NOTE**:  For Windows: https://hub.docker.com/editions/community/docker-ce-desktop-windows In the configuration dialog make sure the use Linux containers is checked.
-
-
-#### Modify bot configuration for Docker
-To call the out-of-process REST API inside a Docker container, the bot configuration is modified. Edit the following file:
-`101.corebot-bert-bidaf/bot/config.py`
-
-Ensure that the bot configuration is set to serve model predictions remotely by setting `USE_MODEL_RUNTIME_SERVICE` to `True`.
-
-In addition, modify the `MODEL_RUNTIME_SERVICE_HOST` to `api` (previously `localhost`).  This will allow the `bot` container to properly address the model `model runtime api` service container.
-
-The resulting `config.py`should look like the following:
-```python
-class DefaultConfig(object):
-    """Bot configuration parameters."""
-    # TCP port that the bot listens on (default:3978)
-    PORT = 3978
-
-    # Azure Application ID (not required if running locally)
-    APP_ID = ""
-    # Azure Application Password (not required if running locally)
-    APP_PASSWORD = ""
-
-    # Determines if the bot calls the models in-proc to the bot or call out of process
-    # to the service api using a REST API.
-    USE_MODEL_RUNTIME_SERVICE = True
-    # Host serving the out-of-process model runtime service api.
-    MODEL_RUNTIME_SERVICE_HOST = "api"
-    # TCP serving the out-of-process model runtime service api.
-    MODEL_RUNTIME_SERVICE_PORT = 8880
-```
-#### Build the containers
-
-The following command builds both the bot and the model runtime service containers.
-```bash
-# From 101.corebot-bert-bidaf directory
-docker-compose --project-directory . --file docker/docker-compose.yml build
-```
-> **NOTE**: If you get error code 137, you may need to increase the amount of memory supplied to Docker.
-
-#### Run the containers locally
-```bash
-# From 101.corebot-bert-bidaf directory
-docker-compose --project-directory . --file docker/docker-compose.yml up -d
-```
-#### Verify
-```bash
-# From 101.corebot-bert-bidaf directory
-docker-compose --project-directory . --file docker/docker-compose.yml logs
-docker ps
-```
-Look at the logs and docker to ensure the containers are running.
-
-> **NOTE**: When testing the bot inside containers, use your local IP address instead of `localhost` (`http://:3978/api/messages`).  
-> To find your IP address:
->
->   - On **Windows**, `ipconfig` at a command prompt.
->   - On **Linux**, `ip addr` at a command prompt.
-
-
-## Testing the bot using Bot Framework Emulator
-
-[Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel.
-
-- Install the Bot Framework Emulator version 4.3.0 or greater from [here](https://github.com/Microsoft/BotFramework-Emulator/releases)
-
-### Connect to the bot using Bot Framework Emulator
-
-- Launch Bot Framework Emulator
-- File -> Open Bot
-- Enter a Bot URL of `http://localhost:3978/api/messages`
-
-### Test the bot
-In the emulator, type phrases such as`hello`, `book flight from seattle to miami`, etc
-
-
-
-## Further reading
-
-- [Bot Framework Documentation](https://docs.botframework.com)
-- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0)
-- [Dialogs](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-dialog?view=azure-bot-service-4.0)
-- [Gathering Input Using Prompts](https://docs.microsoft.com/azure/bot-service/bot-builder-prompts?view=azure-bot-service-4.0&tabs=csharp)
-- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0)
-- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0)
-- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0)
-- [.NET Core CLI tools](https://docs.microsoft.com/dotnet/core/tools/?tabs=netcore2x)
-- [Azure CLI](https://docs.microsoft.com/cli/azure/?view=azure-cli-latest)
-- [Azure Portal](https://portal.azure.com)
-- [Language Understanding using LUIS](https://docs.microsoft.com/azure/cognitive-services/luis/)
-- [Channels and Bot Connector Service](https://docs.microsoft.com/azure/bot-service/bot-concepts?view=azure-bot-service-4.0)
-- [Google BERT](https://github.com/google-research/bert)
+# CoreBot-bert-bidaf
+
+Bot Framework v4 core bot sample demonstrating using open source language models employing the BERT and BiDAF.  This is for demonstration purposes only.  
+
+## Table of Contents
+- [Overview](#overview)
+- [Terminology](#terminology)
+- [Setup](#setup)
+- [Model Development](#model-development)
+- [Model Runtime Options](#model-runtime-options)
+  - [In-Process](#in-process)
+  - [Out-of-process to local service](#out-of-process-to-local-service)
+  - [Using Docker Containers](#using-docker-containers)
+
+
+## Overview
+This bot has been created using [Bot Framework](https://dev.botframework.com).  It demonstrates the following:
+- Train a one layer logistic regression classifier on top of the pretrained BERT model to infer intents.
+- Use locally the ONNX BiDAF pre-trained model to infer entities for the scenario, in this case simple flight booking.
+- Run the bot with the model runtime in-process to the bot.
+- Run the bot with the model runtime external to the bot.
+
+## Terminology
+This document uses the following terminology. 
+**Model Development**: Model Development broadly covers gathering data, data processing, training/validation/evaluation and testing.  This can also be thought of as model preparation or authoring time.
+**Model Runtime**: The built model which can be used to perform inferences against bot utterances.  The model runtime refers to the model and the associated code to perform inferences. The model runtime is used when the bot is running to infer intents and entities.
+**Inference**: Applying the bot utterance to a model yields intents and entities.  The intents and entities are the inferences used by the bot.
+
+## Setup
+
+This sample uses the Anaconda environment (which provides Jupyter Lab and other machine learning tools) in order to run.
+
+The following instructions assume using the [Anaconda]() environment (v4.6.11+). 
+
+Note: Be sure to install the **64-bit** version of Anaconda for the purposes of this tutorial.
+
+### Create and activate virtual environment
+
+In your local folder, open an **Anaconda prompt** and run the following commands:
+
+```bash
+cd 101.corebot-bert-bidaf
+conda create -n botsample python=3.6 anaconda -y
+conda activate botsample # source conda 
+
+# Add extension to handle Jupyter kernel based on the new environemnt.
+pip install ipykernel
+ipython kernel install --user --name=botsample
+
+# Add extension for visual controls to display correctly
+conda install -c conda-forge nodejs -y
+jupyter labextension install @jupyter-widgets/jupyterlab-manager
+```
+
+From here on out, all CLI interactions should occur within the `botsample` Anaconda virtual environment.
+
+### Install  models package
+The `models` package contains source to perform model development support and runtime inferencing using the tuned BERT and BiDAF models.
+
+
+```bash
+# Install Pytorch
+conda install -c pytorch pytorch -y  
+
+# Install models package using code in sample
+# This will create the python package that contains all the 
+# models used in the Jupyter Notebooks and the Bot code.
+cd model
+pip install -e . # Note the '.' after -e
+
+# Verify packages installed
+   # On Windows:
+   conda list | findstr "corebot pytorch onnx"
+
+   # On Linux/etc:
+   conda list | grep -e corebot -e pytorch -e onnx
+```
+
+You should see something like:
+```bash
+model-corebot101          0.0.1                    dev_0    
+onnx                      1.5.0                    pypi_0    pypi
+onnxruntime               0.4.0                    pypi_0    pypi
+pytorch                   1.1.0           py3.6_cuda100_cudnn7_1    pytorch
+pytorch-pretrained-bert   0.6.2                    pypi_0    pypi
+```
+
+## Model Development
+Model development in this sample involves building a BERT classifier model and is performed in the Juypter Lab environment.
+
+### Training in Jupyter Lab
+Training the model can be performed in Jupyter Lab.
+Within the Anaconda shell, launch Jupyter Lab from the sample directory.
+
+```bash
+# Start JupyterLab from the root of the sample directory
+(botsample) 101.corebot-bert-bidaf> jupyter lab
+
+```
+#### Click on `notebooks` folder  in the left hand navigation of JupyterLab
+
+
+  Click for screen shot.
+    Selecting notebooks folder in Jupyter
+     +
+ 
+
+#### Click on `bert_train.ipynb` notebook
+If running the first time, you should select the `botsample` environment.
+
+  Click for screen shot.
+    Selecting Anaconda `botsample` environment for Jupyter Kernel
+     +
+ 
+
+#### Train the model
+
+To build the BERT classifier model, run the Jupyterlab Notebok (Run->**Run All Cells**).
+
+
+  Click for screen shot.
+    Selecting Model to build folder
+     +
+ 
+
+This process may take several minutes.  The built model is placed into a directory that will get packaged with the bot during deployment.   The sample demonstrates using this package in-process to the bot, our of process in a separate host that performs inferences, and within Jupyter Notebooks.
+
+After running the Jupyter Notebook, the output should resemble something like the following:
+
+  Click for screen shot.
+    Showing Completed Model Build
+     +
+ 
+
+
+
+#### Test the BERT runtime model classification
+Once the model has been built, you can test the model with a separately provided Jupyter Notebook (Run->Run All Cells).
+- Within the `notebooks` folder, select the `bert_model_runtime.ipynb` file.
+- Run the notebook.
+
+[]
+
+- The output shows intents (`Book flight`, `Cancel`) that will be used by the bot.
+
+- Add additional test cases to see how phrases will be inferenced.
+
+- To modify the training data, update the file below.  The format of this is compatible with the LUIS schema. When done modifying the training data [re-train the model](#train-the-model) before testing.
+  `101.corebot-bert-bidaf/model/model_corebot101/bert/training_data/FlightBooking.json`
+  
+  [Click to edit Training Data.](./model/model_corebot101/bert/training_data/FlightBooking.json)
+
+> **NOTE**: The default file output location for the tuned BERT model is `/models/bert`.
+
+### Test the BiDAF runtime model classification
+Similarly, you can test the BiDAF model.  Note there is no explicit data processing for the bidaf model.  The entities detected are configured at runtime in the notebook.
+
+[]
+
+> **NOTE**: The default file output location for the BiDAF model is `/models/bidaf`.
+
+
+
+## Model Runtime Options
+
+The sample can host the model runtime within the bot process or out-of-process in a REST API service.  The following sections demonstrate how to do this on the local machine.  In addition, the sample provides Dockerfiles to run this sample in containers.
+
+### In-process
+Within an Anaconda environment (bring up a new Anaconda shell and activate your virtual environment if you would like to continue having JupyterLab running in the original shell), install dependencies for the bot:
+```bash
+# Install requirements required for the bot
+(botsample) 101.corebot-bert-bidaf> pip install -r requirements.txt
+```
+> **NOTE**: If `requirements.txt` doesn't install, you may have to stop JupyterLab if it's running.
+
+```bash
+# Run the bot
+(botsample) 101.corebot-bert-bidaf> cd bot
+(botsample) 101.corebot-bert-bidaf\bot> python main.py
+```
+
+
+> **NOTE**: If executing `main.py` with Python above doesn't work, try running Flask directly:
+>
+> ```bash
+> # Set FLASK_APP with full path to main.py in the sample directory
+> # On linux, use export instead of set.
+> (botsample) 101.corebot-bert-bidaf> set FLASK_APP=main.py 
+> 
+> # Turn on development 
+> (botsample) 101.corebot-bert-bidaf> set FLASK_ENV=development
+> 
+> # Run flask
+> (botsample) 101.corebot-bert-bidaf> flask run  
+> ```
+
+At this point, you can [test the bot](#testing-the-bot-using-bot-framework-emulator).
+
+### Out-of-process to local service
+Sometimes it's helpful to host the model outside the bot's process space and serve inferences from a separate process.  
+
+This section builds on the previous section of [In-process](#in-process).
+
+#### Stop any running bot/model runtime processes
+Ensure there are no running bot or model runtimes executing.  Hit ^C on any Anaconda shells running flask/bot or the model service runtime (`python main.py`).
+
+#### Modify bot configuration for localhost
+To call the out-of-process REST API, the bot configuration is modified. Edit the following file:
+`101.corebot-bert-bidaf/bot/config.py`
+
+Edit the settings for `USE_MODEL_RUNTIME_SERVICE` and set to `True`.
+
+```python
+class DefaultConfig(object):
+    """Bot configuration parameters."""
+    # TCP port that the bot listens on (default:3978)
+    PORT = 3978
+
+    # Azure Application ID (not required if running locally)
+    APP_ID = ""
+    # Azure Application Password (not required if running locally)
+    APP_PASSWORD = ""
+
+    # Determines if the bot calls the models in-proc to the bot or call out of process
+    # to the service api using a REST API.
+    USE_MODEL_RUNTIME_SERVICE = True
+    # Host serving the out-of-process model runtime service api.
+    MODEL_RUNTIME_SERVICE_HOST = "localhost"
+    # TCP serving the out-of-process model runtime service api.
+    MODEL_RUNTIME_SERVICE_PORT = 8880
+```
+#### Set up model runtime service
+Inside a separate Anaconda shell, activate the `botsample` environment, and install the model runtime service.
+
+```bash
+# Install requirements required for model runtime service
+(botsample) 101.corebot-bert-bidaf> cd model_runtime_svc
+(botsample) 101.corebot-bert-bidaf\model_runtime_svc> pip install -e . # Note the dot after the -e switch
+```
+
+#### Run model runtime service
+To run the model runtime service, execute the following:
+```bash
+# Navigate into the model_runtime_svc_corebot101 folder
+cd model_runtime_svc_corebot101
+
+# From 101.corebot-bert-bidaf\model_runtime_svc\model_runtime_svc_corebot101
+python main.py
+```
+If not already running, create a separate Anaconda shell set to the  `botsample` environment and [run the local bot](#local-in-process) as described above.  If it was already running, ensure [the configuration changes made above](#modify-bot-configuration) are running.
+
+At this point, you can [test the bot](#testing-the-bot-using-bot-framework-emulator).
+
+### Using Docker Containers
+This sample also demonstrates using Docker and Docker Compose to run a bot and model runtime service on the local host, that communicate together. 
+
+> **NOTE**:  For Windows: https://hub.docker.com/editions/community/docker-ce-desktop-windows In the configuration dialog make sure the use Linux containers is checked.
+
+
+#### Modify bot configuration for Docker
+To call the out-of-process REST API inside a Docker container, the bot configuration is modified. Edit the following file:
+`101.corebot-bert-bidaf/bot/config.py`
+
+Ensure that the bot configuration is set to serve model predictions remotely by setting `USE_MODEL_RUNTIME_SERVICE` to `True`.
+
+In addition, modify the `MODEL_RUNTIME_SERVICE_HOST` to `api` (previously `localhost`).  This will allow the `bot` container to properly address the model `model runtime api` service container.
+
+The resulting `config.py`should look like the following:
+```python
+class DefaultConfig(object):
+    """Bot configuration parameters."""
+    # TCP port that the bot listens on (default:3978)
+    PORT = 3978
+
+    # Azure Application ID (not required if running locally)
+    APP_ID = ""
+    # Azure Application Password (not required if running locally)
+    APP_PASSWORD = ""
+
+    # Determines if the bot calls the models in-proc to the bot or call out of process
+    # to the service api using a REST API.
+    USE_MODEL_RUNTIME_SERVICE = True
+    # Host serving the out-of-process model runtime service api.
+    MODEL_RUNTIME_SERVICE_HOST = "api"
+    # TCP serving the out-of-process model runtime service api.
+    MODEL_RUNTIME_SERVICE_PORT = 8880
+```
+#### Build the containers
+
+The following command builds both the bot and the model runtime service containers.
+```bash
+# From 101.corebot-bert-bidaf directory
+docker-compose --project-directory . --file docker/docker-compose.yml build
+```
+> **NOTE**: If you get error code 137, you may need to increase the amount of memory supplied to Docker.
+
+#### Run the containers locally
+```bash
+# From 101.corebot-bert-bidaf directory
+docker-compose --project-directory . --file docker/docker-compose.yml up -d
+```
+#### Verify
+```bash
+# From 101.corebot-bert-bidaf directory
+docker-compose --project-directory . --file docker/docker-compose.yml logs
+docker ps
+```
+Look at the logs and docker to ensure the containers are running.
+
+> **NOTE**: When testing the bot inside containers, use your local IP address instead of `localhost` (`http://:3978/api/messages`).  
+> To find your IP address:
+>
+>   - On **Windows**, `ipconfig` at a command prompt.
+>   - On **Linux**, `ip addr` at a command prompt.
+
+
+## Testing the bot using Bot Framework Emulator
+
+[Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel.
+
+- Install the Bot Framework Emulator version 4.3.0 or greater from [here](https://github.com/Microsoft/BotFramework-Emulator/releases)
+
+### Connect to the bot using Bot Framework Emulator
+
+- Launch Bot Framework Emulator
+- File -> Open Bot
+- Enter a Bot URL of `http://localhost:3978/api/messages`
+
+### Test the bot
+In the emulator, type phrases such as`hello`, `book flight from seattle to miami`, etc
+
+
+
+## Further reading
+
+- [Bot Framework Documentation](https://docs.botframework.com)
+- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0)
+- [Dialogs](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-dialog?view=azure-bot-service-4.0)
+- [Gathering Input Using Prompts](https://docs.microsoft.com/azure/bot-service/bot-builder-prompts?view=azure-bot-service-4.0&tabs=csharp)
+- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0)
+- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0)
+- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0)
+- [.NET Core CLI tools](https://docs.microsoft.com/dotnet/core/tools/?tabs=netcore2x)
+- [Azure CLI](https://docs.microsoft.com/cli/azure/?view=azure-cli-latest)
+- [Azure Portal](https://portal.azure.com)
+- [Language Understanding using LUIS](https://docs.microsoft.com/azure/cognitive-services/luis/)
+- [Channels and Bot Connector Service](https://docs.microsoft.com/azure/bot-service/bot-concepts?view=azure-bot-service-4.0)
+- [Google BERT](https://github.com/google-research/bert)
 - [ONNX BiDAF](https://github.com/onnx/models/tree/master/bidaf)
\ No newline at end of file
diff --git a/samples/21.corebot-app-insights/bots/__init__.py b/tests/experimental/101.corebot-bert-bidaf/bot/bots/__init__.py
similarity index 100%
rename from samples/21.corebot-app-insights/bots/__init__.py
rename to tests/experimental/101.corebot-bert-bidaf/bot/bots/__init__.py
diff --git a/samples/experimental/101.corebot-bert-bidaf/bot/bots/dialog_and_welcome_bot.py b/tests/experimental/101.corebot-bert-bidaf/bot/bots/dialog_and_welcome_bot.py
similarity index 100%
rename from samples/experimental/101.corebot-bert-bidaf/bot/bots/dialog_and_welcome_bot.py
rename to tests/experimental/101.corebot-bert-bidaf/bot/bots/dialog_and_welcome_bot.py
diff --git a/samples/experimental/101.corebot-bert-bidaf/bot/bots/dialog_bot.py b/tests/experimental/101.corebot-bert-bidaf/bot/bots/dialog_bot.py
similarity index 100%
rename from samples/experimental/101.corebot-bert-bidaf/bot/bots/dialog_bot.py
rename to tests/experimental/101.corebot-bert-bidaf/bot/bots/dialog_bot.py
diff --git a/samples/21.corebot-app-insights/bots/resources/welcomeCard.json b/tests/experimental/101.corebot-bert-bidaf/bot/bots/resources/welcomeCard.json
similarity index 100%
rename from samples/21.corebot-app-insights/bots/resources/welcomeCard.json
rename to tests/experimental/101.corebot-bert-bidaf/bot/bots/resources/welcomeCard.json
diff --git a/samples/experimental/101.corebot-bert-bidaf/bot/config.py b/tests/experimental/101.corebot-bert-bidaf/bot/config.py
similarity index 97%
rename from samples/experimental/101.corebot-bert-bidaf/bot/config.py
rename to tests/experimental/101.corebot-bert-bidaf/bot/config.py
index 4e8bfd007..89b234435 100644
--- a/samples/experimental/101.corebot-bert-bidaf/bot/config.py
+++ b/tests/experimental/101.corebot-bert-bidaf/bot/config.py
@@ -1,26 +1,26 @@
-#!/usr/bin/env python3
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""Bot/Flask Configuration parameters.
-Configuration parameters for the bot.
-"""
-
-
-class DefaultConfig(object):
-    """Bot configuration parameters."""
-
-    # TCP port that the bot listens on (default:3978)
-    PORT = 3978
-
-    # Azure Application ID (not required if running locally)
-    APP_ID = ""
-    # Azure Application Password (not required if running locally)
-    APP_PASSWORD = ""
-
-    # Determines if the bot calls the models in-proc to the bot or call out of process
-    # to the service api.
-    USE_MODEL_RUNTIME_SERVICE = False
-    # Host serving the out-of-process model runtime service api.
-    MODEL_RUNTIME_SERVICE_HOST = "localhost"
-    # TCP serving the out-of-process model runtime service api.
-    MODEL_RUNTIME_SERVICE_PORT = 8880
+#!/usr/bin/env python3
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+"""Bot/Flask Configuration parameters.
+Configuration parameters for the bot.
+"""
+
+
+class DefaultConfig(object):
+    """Bot configuration parameters."""
+
+    # TCP port that the bot listens on (default:3978)
+    PORT = 3978
+
+    # Azure Application ID (not required if running locally)
+    APP_ID = ""
+    # Azure Application Password (not required if running locally)
+    APP_PASSWORD = ""
+
+    # Determines if the bot calls the models in-proc to the bot or call out of process
+    # to the service api.
+    USE_MODEL_RUNTIME_SERVICE = False
+    # Host serving the out-of-process model runtime service api.
+    MODEL_RUNTIME_SERVICE_HOST = "localhost"
+    # TCP serving the out-of-process model runtime service api.
+    MODEL_RUNTIME_SERVICE_PORT = 8880
diff --git a/samples/21.corebot-app-insights/dialogs/__init__.py b/tests/experimental/101.corebot-bert-bidaf/bot/dialogs/__init__.py
similarity index 100%
rename from samples/21.corebot-app-insights/dialogs/__init__.py
rename to tests/experimental/101.corebot-bert-bidaf/bot/dialogs/__init__.py
diff --git a/samples/experimental/101.corebot-bert-bidaf/bot/dialogs/booking_dialog.py b/tests/experimental/101.corebot-bert-bidaf/bot/dialogs/booking_dialog.py
similarity index 100%
rename from samples/experimental/101.corebot-bert-bidaf/bot/dialogs/booking_dialog.py
rename to tests/experimental/101.corebot-bert-bidaf/bot/dialogs/booking_dialog.py
diff --git a/samples/experimental/101.corebot-bert-bidaf/bot/dialogs/cancel_and_help_dialog.py b/tests/experimental/101.corebot-bert-bidaf/bot/dialogs/cancel_and_help_dialog.py
similarity index 100%
rename from samples/experimental/101.corebot-bert-bidaf/bot/dialogs/cancel_and_help_dialog.py
rename to tests/experimental/101.corebot-bert-bidaf/bot/dialogs/cancel_and_help_dialog.py
diff --git a/samples/experimental/101.corebot-bert-bidaf/bot/dialogs/date_resolver_dialog.py b/tests/experimental/101.corebot-bert-bidaf/bot/dialogs/date_resolver_dialog.py
similarity index 100%
rename from samples/experimental/101.corebot-bert-bidaf/bot/dialogs/date_resolver_dialog.py
rename to tests/experimental/101.corebot-bert-bidaf/bot/dialogs/date_resolver_dialog.py
diff --git a/samples/experimental/101.corebot-bert-bidaf/bot/dialogs/main_dialog.py b/tests/experimental/101.corebot-bert-bidaf/bot/dialogs/main_dialog.py
similarity index 100%
rename from samples/experimental/101.corebot-bert-bidaf/bot/dialogs/main_dialog.py
rename to tests/experimental/101.corebot-bert-bidaf/bot/dialogs/main_dialog.py
diff --git a/samples/experimental/101.corebot-bert-bidaf/bot/helpers/__init__.py b/tests/experimental/101.corebot-bert-bidaf/bot/helpers/__init__.py
similarity index 100%
rename from samples/experimental/101.corebot-bert-bidaf/bot/helpers/__init__.py
rename to tests/experimental/101.corebot-bert-bidaf/bot/helpers/__init__.py
diff --git a/samples/21.corebot-app-insights/helpers/activity_helper.py b/tests/experimental/101.corebot-bert-bidaf/bot/helpers/activity_helper.py
similarity index 100%
rename from samples/21.corebot-app-insights/helpers/activity_helper.py
rename to tests/experimental/101.corebot-bert-bidaf/bot/helpers/activity_helper.py
diff --git a/samples/21.corebot-app-insights/helpers/dialog_helper.py b/tests/experimental/101.corebot-bert-bidaf/bot/helpers/dialog_helper.py
similarity index 100%
rename from samples/21.corebot-app-insights/helpers/dialog_helper.py
rename to tests/experimental/101.corebot-bert-bidaf/bot/helpers/dialog_helper.py
diff --git a/samples/experimental/101.corebot-bert-bidaf/bot/main.py b/tests/experimental/101.corebot-bert-bidaf/bot/main.py
similarity index 100%
rename from samples/experimental/101.corebot-bert-bidaf/bot/main.py
rename to tests/experimental/101.corebot-bert-bidaf/bot/main.py
diff --git a/samples/experimental/101.corebot-bert-bidaf/requirements.txt b/tests/experimental/101.corebot-bert-bidaf/bot/requirements.txt
similarity index 100%
rename from samples/experimental/101.corebot-bert-bidaf/requirements.txt
rename to tests/experimental/101.corebot-bert-bidaf/bot/requirements.txt
diff --git a/samples/experimental/101.corebot-bert-bidaf/docker/docker-compose.yml b/tests/experimental/101.corebot-bert-bidaf/docker/docker-compose.yml
similarity index 94%
rename from samples/experimental/101.corebot-bert-bidaf/docker/docker-compose.yml
rename to tests/experimental/101.corebot-bert-bidaf/docker/docker-compose.yml
index 29c6de853..55599a3c9 100644
--- a/samples/experimental/101.corebot-bert-bidaf/docker/docker-compose.yml
+++ b/tests/experimental/101.corebot-bert-bidaf/docker/docker-compose.yml
@@ -1,20 +1,20 @@
-version: '3.7'
-services:
-  bot:
-    build:
-      context: .
-      dockerfile: Dockerfile_bot
-    ports:
-      - "3978:3978"
-    links:
-      - api
-    environment:
-      MODEL_RUNTIME_API_HOST : api
-
-  api:
-    build:
-      context: .
-      dockerfile: Dockerfile_model_runtime
-    ports:
-      - "8880:8880"
-      
+version: '3.7'
+services:
+  bot:
+    build:
+      context: .
+      dockerfile: Dockerfile_bot
+    ports:
+      - "3978:3978"
+    links:
+      - api
+    environment:
+      MODEL_RUNTIME_API_HOST : api
+
+  api:
+    build:
+      context: .
+      dockerfile: Dockerfile_model_runtime
+    ports:
+      - "8880:8880"
+      
diff --git a/samples/experimental/101.corebot-bert-bidaf/media/jupyter_lab_bert_complete.PNG b/tests/experimental/101.corebot-bert-bidaf/media/jupyter_lab_bert_complete.PNG
similarity index 100%
rename from samples/experimental/101.corebot-bert-bidaf/media/jupyter_lab_bert_complete.PNG
rename to tests/experimental/101.corebot-bert-bidaf/media/jupyter_lab_bert_complete.PNG
diff --git a/samples/experimental/101.corebot-bert-bidaf/media/jupyter_lab_bert_runtime.PNG b/tests/experimental/101.corebot-bert-bidaf/media/jupyter_lab_bert_runtime.PNG
similarity index 100%
rename from samples/experimental/101.corebot-bert-bidaf/media/jupyter_lab_bert_runtime.PNG
rename to tests/experimental/101.corebot-bert-bidaf/media/jupyter_lab_bert_runtime.PNG
diff --git a/samples/experimental/101.corebot-bert-bidaf/media/jupyter_lab_bert_train.PNG b/tests/experimental/101.corebot-bert-bidaf/media/jupyter_lab_bert_train.PNG
similarity index 100%
rename from samples/experimental/101.corebot-bert-bidaf/media/jupyter_lab_bert_train.PNG
rename to tests/experimental/101.corebot-bert-bidaf/media/jupyter_lab_bert_train.PNG
diff --git a/samples/experimental/101.corebot-bert-bidaf/media/jupyter_lab_bidaf_runtime.PNG b/tests/experimental/101.corebot-bert-bidaf/media/jupyter_lab_bidaf_runtime.PNG
similarity index 100%
rename from samples/experimental/101.corebot-bert-bidaf/media/jupyter_lab_bidaf_runtime.PNG
rename to tests/experimental/101.corebot-bert-bidaf/media/jupyter_lab_bidaf_runtime.PNG
diff --git a/samples/experimental/101.corebot-bert-bidaf/media/jupyter_lab_model_nav.PNG b/tests/experimental/101.corebot-bert-bidaf/media/jupyter_lab_model_nav.PNG
similarity index 100%
rename from samples/experimental/101.corebot-bert-bidaf/media/jupyter_lab_model_nav.PNG
rename to tests/experimental/101.corebot-bert-bidaf/media/jupyter_lab_model_nav.PNG
diff --git a/samples/experimental/101.corebot-bert-bidaf/media/jupyter_lab_run_all_cells.PNG b/tests/experimental/101.corebot-bert-bidaf/media/jupyter_lab_run_all_cells.PNG
similarity index 100%
rename from samples/experimental/101.corebot-bert-bidaf/media/jupyter_lab_run_all_cells.PNG
rename to tests/experimental/101.corebot-bert-bidaf/media/jupyter_lab_run_all_cells.PNG
diff --git a/samples/experimental/101.corebot-bert-bidaf/media/jupyter_lab_select_kernel.PNG b/tests/experimental/101.corebot-bert-bidaf/media/jupyter_lab_select_kernel.PNG
similarity index 100%
rename from samples/experimental/101.corebot-bert-bidaf/media/jupyter_lab_select_kernel.PNG
rename to tests/experimental/101.corebot-bert-bidaf/media/jupyter_lab_select_kernel.PNG
diff --git a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/about.py b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/about.py
similarity index 97%
rename from samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/about.py
rename to tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/about.py
index 340b35e3e..e6dd2b2d7 100644
--- a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/about.py
+++ b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/about.py
@@ -1,14 +1,14 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""Package information."""
-import os
-
-__title__ = "model_corebot101"
-__version__ = (
-    os.environ["packageVersion"] if "packageVersion" in os.environ else "0.0.1"
-)
-__uri__ = "https://www.github.com/Microsoft/botbuilder-python"
-__author__ = "Microsoft"
-__description__ = "Microsoft Bot Framework Bot Builder"
-__summary__ = "Microsoft Bot Framework Bot Builder SDK for Python."
-__license__ = "MIT"
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+"""Package information."""
+import os
+
+__title__ = "model_corebot101"
+__version__ = (
+    os.environ["packageVersion"] if "packageVersion" in os.environ else "0.0.1"
+)
+__uri__ = "https://www.github.com/Microsoft/botbuilder-python"
+__author__ = "Microsoft"
+__description__ = "Microsoft Bot Framework Bot Builder"
+__summary__ = "Microsoft Bot Framework Bot Builder SDK for Python."
+__license__ = "MIT"
diff --git a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/common/__init__.py b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/common/__init__.py
similarity index 97%
rename from samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/common/__init__.py
rename to tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/common/__init__.py
index b339b691f..f9d109364 100644
--- a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/common/__init__.py
+++ b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/common/__init__.py
@@ -1,8 +1,8 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from .bert_util import BertUtil
-from .input_example import InputExample
-from .input_features import InputFeatures
-
-__all__ = ["BertUtil", "InputExample", "InputFeatures"]
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from .bert_util import BertUtil
+from .input_example import InputExample
+from .input_features import InputFeatures
+
+__all__ = ["BertUtil", "InputExample", "InputFeatures"]
diff --git a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/common/bert_util.py b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/common/bert_util.py
similarity index 97%
rename from samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/common/bert_util.py
rename to tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/common/bert_util.py
index ee9ab630e..800cee607 100644
--- a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/common/bert_util.py
+++ b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/common/bert_util.py
@@ -1,156 +1,156 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import logging
-from typing import List
-
-from .input_features import InputFeatures
-from scipy.stats import pearsonr, spearmanr
-from sklearn.metrics import f1_score
-
-
-class BertUtil:
-    logger = logging.getLogger(__name__)
-
-    @classmethod
-    def convert_examples_to_features(
-        cls, examples, label_list, max_seq_length, tokenizer, output_mode
-    ) -> List:
-        """Loads a data file into a list of `InputBatch`s."""
-
-        label_map = {label: i for i, label in enumerate(label_list)}
-
-        features = []
-        for (ex_index, example) in enumerate(examples):
-            if ex_index % 10000 == 0:
-                cls.logger.info("Writing example %d of %d" % (ex_index, len(examples)))
-
-            tokens_a = tokenizer.tokenize(example.text_a)
-
-            tokens_b = None
-            if example.text_b:
-                tokens_b = tokenizer.tokenize(example.text_b)
-                # Modifies `tokens_a` and `tokens_b` in place so that the total
-                # length is less than the specified length.
-                # Account for [CLS], [SEP], [SEP] with "- 3"
-                BertUtil._truncate_seq_pair(tokens_a, tokens_b, max_seq_length - 3)
-            else:
-                # Account for [CLS] and [SEP] with "- 2"
-                if len(tokens_a) > max_seq_length - 2:
-                    tokens_a = tokens_a[: (max_seq_length - 2)]
-
-            # The convention in BERT is:
-            # (a) For sequence pairs:
-            #  tokens:   [CLS] is this jack ##son ##ville ? [SEP] no it is not . [SEP]
-            #  type_ids: 0   0  0    0    0     0       0 0    1  1  1  1   1 1
-            # (b) For single sequences:
-            #  tokens:   [CLS] the dog is hairy . [SEP]
-            #  type_ids: 0   0   0   0  0     0 0
-            #
-            # Where "type_ids" are used to indicate whether this is the first
-            # sequence or the second sequence. The embedding vectors for `type=0` and
-            # `type=1` were learned during pre-training and are added to the wordpiece
-            # embedding vector (and position vector). This is not *strictly* necessary
-            # since the [SEP] token unambiguously separates the sequences, but it makes
-            # it easier for the model to learn the concept of sequences.
-            #
-            # For classification tasks, the first vector (corresponding to [CLS]) is
-            # used as as the "sentence vector". Note that this only makes sense because
-            # the entire model is fine-tuned.
-            tokens = ["[CLS]"] + tokens_a + ["[SEP]"]
-            segment_ids = [0] * len(tokens)
-
-            if tokens_b:
-                tokens += tokens_b + ["[SEP]"]
-                segment_ids += [1] * (len(tokens_b) + 1)
-
-            input_ids = tokenizer.convert_tokens_to_ids(tokens)
-
-            # The mask has 1 for real tokens and 0 for padding tokens. Only real
-            # tokens are attended to.
-            input_mask = [1] * len(input_ids)
-
-            # Zero-pad up to the sequence length.
-            padding = [0] * (max_seq_length - len(input_ids))
-            input_ids += padding
-            input_mask += padding
-            segment_ids += padding
-
-            assert len(input_ids) == max_seq_length
-            assert len(input_mask) == max_seq_length
-            assert len(segment_ids) == max_seq_length
-
-            if output_mode == "classification":
-                label_id = label_map[example.label]
-            elif output_mode == "regression":
-                label_id = float(example.label)
-            else:
-                raise KeyError(output_mode)
-
-            if ex_index < 5:
-                cls.logger.info("*** Example ***")
-                cls.logger.info("guid: %s" % (example.guid))
-                cls.logger.info("tokens: %s" % " ".join([str(x) for x in tokens]))
-                cls.logger.info("input_ids: %s" % " ".join([str(x) for x in input_ids]))
-                cls.logger.info(
-                    "input_mask: %s" % " ".join([str(x) for x in input_mask])
-                )
-                cls.logger.info(
-                    "segment_ids: %s" % " ".join([str(x) for x in segment_ids])
-                )
-                cls.logger.info("label: %s (id = %d)" % (example.label, label_id))
-
-            features.append(
-                InputFeatures(
-                    input_ids=input_ids,
-                    input_mask=input_mask,
-                    segment_ids=segment_ids,
-                    label_id=label_id,
-                )
-            )
-        return features
-
-    @staticmethod
-    def _truncate_seq_pair(tokens_a, tokens_b, max_length):
-        """Truncates a sequence pair in place to the maximum length."""
-
-        # This is a simple heuristic which will always truncate the longer sequence
-        # one token at a time. This makes more sense than truncating an equal percent
-        # of tokens from each, since if one sequence is very short then each token
-        # that's truncated likely contains more information than a longer sequence.
-        while True:
-            total_length = len(tokens_a) + len(tokens_b)
-            if total_length <= max_length:
-                break
-            if len(tokens_a) > len(tokens_b):
-                tokens_a.pop()
-            else:
-                tokens_b.pop()
-
-    @staticmethod
-    def simple_accuracy(preds, labels):
-        return (preds == labels).mean()
-
-    @staticmethod
-    def acc_and_f1(preds, labels):
-        acc = BertUtil.simple_accuracy(preds, labels)
-        f1 = f1_score(y_true=labels, y_pred=preds)
-        return {"acc": acc, "f1": f1, "acc_and_f1": (acc + f1) / 2}
-
-    @staticmethod
-    def pearson_and_spearman(preds, labels):
-        pearson_corr = pearsonr(preds, labels)[0]
-        spearman_corr = spearmanr(preds, labels)[0]
-        return {
-            "pearson": pearson_corr,
-            "spearmanr": spearman_corr,
-            "corr": (pearson_corr + spearman_corr) / 2,
-        }
-
-    @staticmethod
-    def compute_metrics(task_name, preds, labels):
-        assert len(preds) == len(labels)
-        if task_name == "flight_booking":
-            return BertUtil.acc_and_f1(preds, labels)
-        else:
-            raise KeyError(task_name)
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import logging
+from typing import List
+
+from .input_features import InputFeatures
+from scipy.stats import pearsonr, spearmanr
+from sklearn.metrics import f1_score
+
+
+class BertUtil:
+    logger = logging.getLogger(__name__)
+
+    @classmethod
+    def convert_examples_to_features(
+        cls, examples, label_list, max_seq_length, tokenizer, output_mode
+    ) -> List:
+        """Loads a data file into a list of `InputBatch`s."""
+
+        label_map = {label: i for i, label in enumerate(label_list)}
+
+        features = []
+        for (ex_index, example) in enumerate(examples):
+            if ex_index % 10000 == 0:
+                cls.logger.info("Writing example %d of %d" % (ex_index, len(examples)))
+
+            tokens_a = tokenizer.tokenize(example.text_a)
+
+            tokens_b = None
+            if example.text_b:
+                tokens_b = tokenizer.tokenize(example.text_b)
+                # Modifies `tokens_a` and `tokens_b` in place so that the total
+                # length is less than the specified length.
+                # Account for [CLS], [SEP], [SEP] with "- 3"
+                BertUtil._truncate_seq_pair(tokens_a, tokens_b, max_seq_length - 3)
+            else:
+                # Account for [CLS] and [SEP] with "- 2"
+                if len(tokens_a) > max_seq_length - 2:
+                    tokens_a = tokens_a[: (max_seq_length - 2)]
+
+            # The convention in BERT is:
+            # (a) For sequence pairs:
+            #  tokens:   [CLS] is this jack ##son ##ville ? [SEP] no it is not . [SEP]
+            #  type_ids: 0   0  0    0    0     0       0 0    1  1  1  1   1 1
+            # (b) For single sequences:
+            #  tokens:   [CLS] the dog is hairy . [SEP]
+            #  type_ids: 0   0   0   0  0     0 0
+            #
+            # Where "type_ids" are used to indicate whether this is the first
+            # sequence or the second sequence. The embedding vectors for `type=0` and
+            # `type=1` were learned during pre-training and are added to the wordpiece
+            # embedding vector (and position vector). This is not *strictly* necessary
+            # since the [SEP] token unambiguously separates the sequences, but it makes
+            # it easier for the model to learn the concept of sequences.
+            #
+            # For classification tasks, the first vector (corresponding to [CLS]) is
+            # used as as the "sentence vector". Note that this only makes sense because
+            # the entire model is fine-tuned.
+            tokens = ["[CLS]"] + tokens_a + ["[SEP]"]
+            segment_ids = [0] * len(tokens)
+
+            if tokens_b:
+                tokens += tokens_b + ["[SEP]"]
+                segment_ids += [1] * (len(tokens_b) + 1)
+
+            input_ids = tokenizer.convert_tokens_to_ids(tokens)
+
+            # The mask has 1 for real tokens and 0 for padding tokens. Only real
+            # tokens are attended to.
+            input_mask = [1] * len(input_ids)
+
+            # Zero-pad up to the sequence length.
+            padding = [0] * (max_seq_length - len(input_ids))
+            input_ids += padding
+            input_mask += padding
+            segment_ids += padding
+
+            assert len(input_ids) == max_seq_length
+            assert len(input_mask) == max_seq_length
+            assert len(segment_ids) == max_seq_length
+
+            if output_mode == "classification":
+                label_id = label_map[example.label]
+            elif output_mode == "regression":
+                label_id = float(example.label)
+            else:
+                raise KeyError(output_mode)
+
+            if ex_index < 5:
+                cls.logger.info("*** Example ***")
+                cls.logger.info("guid: %s" % (example.guid))
+                cls.logger.info("tokens: %s" % " ".join([str(x) for x in tokens]))
+                cls.logger.info("input_ids: %s" % " ".join([str(x) for x in input_ids]))
+                cls.logger.info(
+                    "input_mask: %s" % " ".join([str(x) for x in input_mask])
+                )
+                cls.logger.info(
+                    "segment_ids: %s" % " ".join([str(x) for x in segment_ids])
+                )
+                cls.logger.info("label: %s (id = %d)" % (example.label, label_id))
+
+            features.append(
+                InputFeatures(
+                    input_ids=input_ids,
+                    input_mask=input_mask,
+                    segment_ids=segment_ids,
+                    label_id=label_id,
+                )
+            )
+        return features
+
+    @staticmethod
+    def _truncate_seq_pair(tokens_a, tokens_b, max_length):
+        """Truncates a sequence pair in place to the maximum length."""
+
+        # This is a simple heuristic which will always truncate the longer sequence
+        # one token at a time. This makes more sense than truncating an equal percent
+        # of tokens from each, since if one sequence is very short then each token
+        # that's truncated likely contains more information than a longer sequence.
+        while True:
+            total_length = len(tokens_a) + len(tokens_b)
+            if total_length <= max_length:
+                break
+            if len(tokens_a) > len(tokens_b):
+                tokens_a.pop()
+            else:
+                tokens_b.pop()
+
+    @staticmethod
+    def simple_accuracy(preds, labels):
+        return (preds == labels).mean()
+
+    @staticmethod
+    def acc_and_f1(preds, labels):
+        acc = BertUtil.simple_accuracy(preds, labels)
+        f1 = f1_score(y_true=labels, y_pred=preds)
+        return {"acc": acc, "f1": f1, "acc_and_f1": (acc + f1) / 2}
+
+    @staticmethod
+    def pearson_and_spearman(preds, labels):
+        pearson_corr = pearsonr(preds, labels)[0]
+        spearman_corr = spearmanr(preds, labels)[0]
+        return {
+            "pearson": pearson_corr,
+            "spearmanr": spearman_corr,
+            "corr": (pearson_corr + spearman_corr) / 2,
+        }
+
+    @staticmethod
+    def compute_metrics(task_name, preds, labels):
+        assert len(preds) == len(labels)
+        if task_name == "flight_booking":
+            return BertUtil.acc_and_f1(preds, labels)
+        else:
+            raise KeyError(task_name)
diff --git a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/common/input_example.py b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/common/input_example.py
similarity index 97%
rename from samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/common/input_example.py
rename to tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/common/input_example.py
index b674642f3..63410a11f 100644
--- a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/common/input_example.py
+++ b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/common/input_example.py
@@ -1,23 +1,23 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-
-class InputExample(object):
-    """A single training/test example for sequence classification."""
-
-    def __init__(self, guid, text_a, text_b=None, label=None):
-        """Constructs a InputExample.
-
-        Args:
-            guid: Unique id for the example.
-            text_a: string. The untokenized text of the first sequence. For single
-            sequence tasks, only this sequence must be specified.
-            text_b: (Optional) string. The untokenized text of the second sequence.
-            Only must be specified for sequence pair tasks.
-            label: (Optional) string. The label of the example. This should be
-            specified for train and dev examples, but not for test examples.
-        """
-        self.guid = guid
-        self.text_a = text_a
-        self.text_b = text_b
-        self.label = label
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+
+class InputExample(object):
+    """A single training/test example for sequence classification."""
+
+    def __init__(self, guid, text_a, text_b=None, label=None):
+        """Constructs a InputExample.
+
+        Args:
+            guid: Unique id for the example.
+            text_a: string. The untokenized text of the first sequence. For single
+            sequence tasks, only this sequence must be specified.
+            text_b: (Optional) string. The untokenized text of the second sequence.
+            Only must be specified for sequence pair tasks.
+            label: (Optional) string. The label of the example. This should be
+            specified for train and dev examples, but not for test examples.
+        """
+        self.guid = guid
+        self.text_a = text_a
+        self.text_b = text_b
+        self.label = label
diff --git a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/common/input_features.py b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/common/input_features.py
similarity index 96%
rename from samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/common/input_features.py
rename to tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/common/input_features.py
index 97be63ecf..0138e75e2 100644
--- a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/common/input_features.py
+++ b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/common/input_features.py
@@ -1,12 +1,12 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-
-class InputFeatures(object):
-    """A single set of features of data."""
-
-    def __init__(self, input_ids, input_mask, segment_ids, label_id):
-        self.input_ids = input_ids
-        self.input_mask = input_mask
-        self.segment_ids = segment_ids
-        self.label_id = label_id
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+
+class InputFeatures(object):
+    """A single set of features of data."""
+
+    def __init__(self, input_ids, input_mask, segment_ids, label_id):
+        self.input_ids = input_ids
+        self.input_mask = input_mask
+        self.segment_ids = segment_ids
+        self.label_id = label_id
diff --git a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/model_runtime/__init__.py b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/model_runtime/__init__.py
similarity index 96%
rename from samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/model_runtime/__init__.py
rename to tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/model_runtime/__init__.py
index 8c5946fe4..22497eea5 100644
--- a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/model_runtime/__init__.py
+++ b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/model_runtime/__init__.py
@@ -1,6 +1,6 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from .bert_model_runtime import BertModelRuntime
-
-__all__ = ["BertModelRuntime"]
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from .bert_model_runtime import BertModelRuntime
+
+__all__ = ["BertModelRuntime"]
diff --git a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/model_runtime/bert_model_runtime.py b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/model_runtime/bert_model_runtime.py
similarity index 97%
rename from samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/model_runtime/bert_model_runtime.py
rename to tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/model_runtime/bert_model_runtime.py
index 112c1167b..bb66ddc07 100644
--- a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/model_runtime/bert_model_runtime.py
+++ b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/model_runtime/bert_model_runtime.py
@@ -1,122 +1,122 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""Bert model runtime."""
-
-import os
-import sys
-from typing import List
-import numpy as np
-import torch
-from torch.utils.data import DataLoader, SequentialSampler, TensorDataset
-from pytorch_pretrained_bert import BertForSequenceClassification, BertTokenizer
-from model_corebot101.bert.common.bert_util import BertUtil
-from model_corebot101.bert.common.input_example import InputExample
-
-
-class BertModelRuntime:
-    """Model runtime for the Bert model."""
-
-    def __init__(
-        self,
-        model_dir: str,
-        label_list: List[str],
-        max_seq_length: int = 128,
-        output_mode: str = "classification",
-        no_cuda: bool = False,
-        do_lower_case: bool = True,
-    ):
-        self.model_dir = model_dir
-        self.label_list = label_list
-        self.num_labels = len(self.label_list)
-        self.max_seq_length = max_seq_length
-        self.output_mode = output_mode
-        self.no_cuda = no_cuda
-        self.do_lower_case = do_lower_case
-        self._load_model()
-
-    # pylint:disable=unused-argument
-    @staticmethod
-    def init_bert(bert_model_dir: str) -> bool:
-        """ Handle any one-time initlization """
-        if os.path.isdir(bert_model_dir):
-            print("bert model directory already present..", file=sys.stderr)
-        else:
-            print("Creating bert model directory..", file=sys.stderr)
-            os.makedirs(bert_model_dir, exist_ok=True)
-        return True
-
-    def _load_model(self) -> None:
-        self.device = torch.device(
-            "cuda" if torch.cuda.is_available() and not self.no_cuda else "cpu"
-        )
-        self.n_gpu = torch.cuda.device_count()
-
-        # Load a trained model and vocabulary that you have fine-tuned
-        self.model = BertForSequenceClassification.from_pretrained(
-            self.model_dir, num_labels=self.num_labels
-        )
-        self.tokenizer = BertTokenizer.from_pretrained(
-            self.model_dir, do_lower_case=self.do_lower_case
-        )
-        self.model.to(self.device)
-
-    def serve(self, query: str) -> str:
-        example = InputExample(
-            guid="", text_a=query, text_b=None, label=self.label_list[0]
-        )
-        examples = [example]
-
-        eval_features = BertUtil.convert_examples_to_features(
-            examples,
-            self.label_list,
-            self.max_seq_length,
-            self.tokenizer,
-            self.output_mode,
-        )
-        all_input_ids = torch.tensor(
-            [f.input_ids for f in eval_features], dtype=torch.long
-        )
-        all_input_mask = torch.tensor(
-            [f.input_mask for f in eval_features], dtype=torch.long
-        )
-        all_segment_ids = torch.tensor(
-            [f.segment_ids for f in eval_features], dtype=torch.long
-        )
-
-        if self.output_mode == "classification":
-            all_label_ids = torch.tensor(
-                [f.label_id for f in eval_features], dtype=torch.long
-            )
-
-        eval_data = TensorDataset(
-            all_input_ids, all_input_mask, all_segment_ids, all_label_ids
-        )
-        # Run prediction for full data
-        eval_sampler = SequentialSampler(eval_data)
-        eval_dataloader = DataLoader(eval_data, sampler=eval_sampler, batch_size=1)
-
-        self.model.eval()
-        nb_eval_steps = 0
-        preds = []
-
-        for input_ids, input_mask, segment_ids, label_ids in eval_dataloader:
-            input_ids = input_ids.to(self.device)
-            input_mask = input_mask.to(self.device)
-            segment_ids = segment_ids.to(self.device)
-
-            with torch.no_grad():
-                logits = self.model(input_ids, segment_ids, input_mask, labels=None)
-
-            nb_eval_steps += 1
-            if len(preds) == 0:
-                preds.append(logits.detach().cpu().numpy())
-            else:
-                preds[0] = np.append(preds[0], logits.detach().cpu().numpy(), axis=0)
-
-        preds = preds[0]
-        if self.output_mode == "classification":
-            preds = np.argmax(preds, axis=1)
-
-        label_id = preds[0]
-        pred_label = self.label_list[label_id]
-        return pred_label
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+"""Bert model runtime."""
+
+import os
+import sys
+from typing import List
+import numpy as np
+import torch
+from torch.utils.data import DataLoader, SequentialSampler, TensorDataset
+from pytorch_pretrained_bert import BertForSequenceClassification, BertTokenizer
+from model_corebot101.bert.common.bert_util import BertUtil
+from model_corebot101.bert.common.input_example import InputExample
+
+
+class BertModelRuntime:
+    """Model runtime for the Bert model."""
+
+    def __init__(
+        self,
+        model_dir: str,
+        label_list: List[str],
+        max_seq_length: int = 128,
+        output_mode: str = "classification",
+        no_cuda: bool = False,
+        do_lower_case: bool = True,
+    ):
+        self.model_dir = model_dir
+        self.label_list = label_list
+        self.num_labels = len(self.label_list)
+        self.max_seq_length = max_seq_length
+        self.output_mode = output_mode
+        self.no_cuda = no_cuda
+        self.do_lower_case = do_lower_case
+        self._load_model()
+
+    # pylint:disable=unused-argument
+    @staticmethod
+    def init_bert(bert_model_dir: str) -> bool:
+        """ Handle any one-time initlization """
+        if os.path.isdir(bert_model_dir):
+            print("bert model directory already present..", file=sys.stderr)
+        else:
+            print("Creating bert model directory..", file=sys.stderr)
+            os.makedirs(bert_model_dir, exist_ok=True)
+        return True
+
+    def _load_model(self) -> None:
+        self.device = torch.device(
+            "cuda" if torch.cuda.is_available() and not self.no_cuda else "cpu"
+        )
+        self.n_gpu = torch.cuda.device_count()
+
+        # Load a trained model and vocabulary that you have fine-tuned
+        self.model = BertForSequenceClassification.from_pretrained(
+            self.model_dir, num_labels=self.num_labels
+        )
+        self.tokenizer = BertTokenizer.from_pretrained(
+            self.model_dir, do_lower_case=self.do_lower_case
+        )
+        self.model.to(self.device)
+
+    def serve(self, query: str) -> str:
+        example = InputExample(
+            guid="", text_a=query, text_b=None, label=self.label_list[0]
+        )
+        examples = [example]
+
+        eval_features = BertUtil.convert_examples_to_features(
+            examples,
+            self.label_list,
+            self.max_seq_length,
+            self.tokenizer,
+            self.output_mode,
+        )
+        all_input_ids = torch.tensor(
+            [f.input_ids for f in eval_features], dtype=torch.long
+        )
+        all_input_mask = torch.tensor(
+            [f.input_mask for f in eval_features], dtype=torch.long
+        )
+        all_segment_ids = torch.tensor(
+            [f.segment_ids for f in eval_features], dtype=torch.long
+        )
+
+        if self.output_mode == "classification":
+            all_label_ids = torch.tensor(
+                [f.label_id for f in eval_features], dtype=torch.long
+            )
+
+        eval_data = TensorDataset(
+            all_input_ids, all_input_mask, all_segment_ids, all_label_ids
+        )
+        # Run prediction for full data
+        eval_sampler = SequentialSampler(eval_data)
+        eval_dataloader = DataLoader(eval_data, sampler=eval_sampler, batch_size=1)
+
+        self.model.eval()
+        nb_eval_steps = 0
+        preds = []
+
+        for input_ids, input_mask, segment_ids, label_ids in eval_dataloader:
+            input_ids = input_ids.to(self.device)
+            input_mask = input_mask.to(self.device)
+            segment_ids = segment_ids.to(self.device)
+
+            with torch.no_grad():
+                logits = self.model(input_ids, segment_ids, input_mask, labels=None)
+
+            nb_eval_steps += 1
+            if len(preds) == 0:
+                preds.append(logits.detach().cpu().numpy())
+            else:
+                preds[0] = np.append(preds[0], logits.detach().cpu().numpy(), axis=0)
+
+        preds = preds[0]
+        if self.output_mode == "classification":
+            preds = np.argmax(preds, axis=1)
+
+        label_id = preds[0]
+        pred_label = self.label_list[label_id]
+        return pred_label
diff --git a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/requirements.txt b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/requirements.txt
similarity index 92%
rename from samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/requirements.txt
rename to tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/requirements.txt
index 10f898f8b..f9d97a146 100644
--- a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/requirements.txt
+++ b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/requirements.txt
@@ -1,3 +1,3 @@
-torch
-tqdm
-pytorch-pretrained-bert
+torch
+tqdm
+pytorch-pretrained-bert
diff --git a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/train/__init__.py b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/train/__init__.py
similarity index 97%
rename from samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/train/__init__.py
rename to tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/train/__init__.py
index 277890a19..1bd0ac221 100644
--- a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/train/__init__.py
+++ b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/train/__init__.py
@@ -1,9 +1,9 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""Bert tuning training."""
-
-from .args import Args
-from .bert_train_eval import BertTrainEval
-from .flight_booking_processor import FlightBookingProcessor
-
-__all__ = ["Args", "BertTrainEval", "FlightBookingProcessor"]
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+"""Bert tuning training."""
+
+from .args import Args
+from .bert_train_eval import BertTrainEval
+from .flight_booking_processor import FlightBookingProcessor
+
+__all__ = ["Args", "BertTrainEval", "FlightBookingProcessor"]
diff --git a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/train/args.py b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/train/args.py
similarity index 96%
rename from samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/train/args.py
rename to tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/train/args.py
index c49036572..3d0f77811 100644
--- a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/train/args.py
+++ b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/train/args.py
@@ -1,58 +1,58 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""Arguments for the model. """
-
-import os
-import sys
-from pathlib import Path
-
-# pylint:disable=line-too-long
-class Args:
-    """Arguments for the model."""
-
-    training_data_dir: str = None
-    bert_model: str = None
-    task_name: str = None
-    model_dir: str = None
-    cleanup_output_dir: bool = False
-    cache_dir: str = ""
-    max_seq_length: int = 128
-    do_train: bool = None
-    do_eval: bool = None
-    do_lower_case: bool = None
-    train_batch_size: int = 4
-    eval_batch_size: int = 8
-    learning_rate: float = 5e-5
-    num_train_epochs: float = 3.0
-    warmup_proportion: float = 0.1
-    no_cuda: bool = None
-    local_rank: int = -1
-    seed: int = 42
-    gradient_accumulation_steps: int = 1
-    fp16: bool = None
-    loss_scale: float = 0
-
-    @classmethod
-    def for_flight_booking(
-        cls,
-        training_data_dir: str = os.path.abspath(
-            os.path.join(os.path.dirname(os.path.abspath(__file__)), "../training_data")
-        ),
-        task_name: str = "flight_booking",
-    ):
-        """Return the flight booking args."""
-        args = cls()
-
-        args.training_data_dir = training_data_dir
-        args.task_name = task_name
-        home_dir = str(Path.home())
-        args.model_dir = os.path.abspath(os.path.join(home_dir, "models/bert"))
-        args.bert_model = "bert-base-uncased"
-        args.do_lower_case = True
-
-        print(
-            f"Bert Model training_data_dir is set to {args.training_data_dir}",
-            file=sys.stderr,
-        )
-        print(f"Bert Model model_dir is set to {args.model_dir}", file=sys.stderr)
-        return args
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+"""Arguments for the model. """
+
+import os
+import sys
+from pathlib import Path
+
+# pylint:disable=line-too-long
+class Args:
+    """Arguments for the model."""
+
+    training_data_dir: str = None
+    bert_model: str = None
+    task_name: str = None
+    model_dir: str = None
+    cleanup_output_dir: bool = False
+    cache_dir: str = ""
+    max_seq_length: int = 128
+    do_train: bool = None
+    do_eval: bool = None
+    do_lower_case: bool = None
+    train_batch_size: int = 4
+    eval_batch_size: int = 8
+    learning_rate: float = 5e-5
+    num_train_epochs: float = 3.0
+    warmup_proportion: float = 0.1
+    no_cuda: bool = None
+    local_rank: int = -1
+    seed: int = 42
+    gradient_accumulation_steps: int = 1
+    fp16: bool = None
+    loss_scale: float = 0
+
+    @classmethod
+    def for_flight_booking(
+        cls,
+        training_data_dir: str = os.path.abspath(
+            os.path.join(os.path.dirname(os.path.abspath(__file__)), "../training_data")
+        ),
+        task_name: str = "flight_booking",
+    ):
+        """Return the flight booking args."""
+        args = cls()
+
+        args.training_data_dir = training_data_dir
+        args.task_name = task_name
+        home_dir = str(Path.home())
+        args.model_dir = os.path.abspath(os.path.join(home_dir, "models/bert"))
+        args.bert_model = "bert-base-uncased"
+        args.do_lower_case = True
+
+        print(
+            f"Bert Model training_data_dir is set to {args.training_data_dir}",
+            file=sys.stderr,
+        )
+        print(f"Bert Model model_dir is set to {args.model_dir}", file=sys.stderr)
+        return args
diff --git a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/train/bert_train_eval.py b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/train/bert_train_eval.py
similarity index 97%
rename from samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/train/bert_train_eval.py
rename to tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/train/bert_train_eval.py
index fde3fce80..11d6d558e 100644
--- a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/train/bert_train_eval.py
+++ b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/train/bert_train_eval.py
@@ -1,375 +1,375 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import logging
-import os
-import random
-import shutil
-import numpy as np
-import torch
-from .args import Args
-
-from model_corebot101.bert.common.bert_util import BertUtil
-from model_corebot101.bert.train.flight_booking_processor import FlightBookingProcessor
-from pytorch_pretrained_bert.file_utils import (
-    CONFIG_NAME,
-    PYTORCH_PRETRAINED_BERT_CACHE,
-    WEIGHTS_NAME,
-)
-from pytorch_pretrained_bert.modeling import (
-    BertForSequenceClassification,
-    BertPreTrainedModel,
-)
-from pytorch_pretrained_bert.optimization import BertAdam
-from pytorch_pretrained_bert.tokenization import BertTokenizer
-from torch.nn import CrossEntropyLoss
-from torch.utils.data import DataLoader, RandomSampler, SequentialSampler, TensorDataset
-from torch.utils.data.distributed import DistributedSampler
-
-from tqdm import tqdm, trange
-
-
-class BertTrainEval:
-    logger = logging.getLogger(__name__)
-
-    def __init__(self, args: Args):
-        self.processor = FlightBookingProcessor()
-        self.output_mode = "classification"
-        self.args = args
-        self._prepare()
-        self.model = self._prepare_model()
-
-    @classmethod
-    def train_eval(cls, cleanup_output_dir: bool = False) -> None:
-        # uncomment the following line for debugging.
-        # import pdb; pdb.set_trace()
-        args = Args.for_flight_booking()
-        args.do_train = True
-        args.do_eval = True
-        args.cleanup_output_dir = cleanup_output_dir
-        bert = cls(args)
-        bert.train()
-        bert.eval()
-
-    def train(self) -> None:
-        # Prepare optimizer
-        param_optimizer = list(self.model.named_parameters())
-        no_decay = ["bias", "LayerNorm.bias", "LayerNorm.weight"]
-        optimizer_grouped_parameters = [
-            {
-                "params": [
-                    p for n, p in param_optimizer if not any(nd in n for nd in no_decay)
-                ],
-                "weight_decay": 0.01,
-            },
-            {
-                "params": [
-                    p for n, p in param_optimizer if any(nd in n for nd in no_decay)
-                ],
-                "weight_decay": 0.0,
-            },
-        ]
-        optimizer = BertAdam(
-            optimizer_grouped_parameters,
-            lr=self.args.learning_rate,
-            warmup=self.args.warmup_proportion,
-            t_total=self.num_train_optimization_steps,
-        )
-
-        global_step: int = 0
-        nb_tr_steps = 0
-        tr_loss: float = 0
-        train_features = BertUtil.convert_examples_to_features(
-            self.train_examples,
-            self.label_list,
-            self.args.max_seq_length,
-            self.tokenizer,
-            self.output_mode,
-        )
-        self.logger.info("***** Running training *****")
-        self.logger.info("  Num examples = %d", len(self.train_examples))
-        self.logger.info("  Batch size = %d", self.args.train_batch_size)
-        self.logger.info("  Num steps = %d", self.num_train_optimization_steps)
-        all_input_ids = torch.tensor(
-            [f.input_ids for f in train_features], dtype=torch.long
-        )
-        all_input_mask = torch.tensor(
-            [f.input_mask for f in train_features], dtype=torch.long
-        )
-        all_segment_ids = torch.tensor(
-            [f.segment_ids for f in train_features], dtype=torch.long
-        )
-
-        if self.output_mode == "classification":
-            all_label_ids = torch.tensor(
-                [f.label_id for f in train_features], dtype=torch.long
-            )
-
-        train_data = TensorDataset(
-            all_input_ids, all_input_mask, all_segment_ids, all_label_ids
-        )
-        if self.args.local_rank == -1:
-            train_sampler = RandomSampler(train_data)
-        else:
-            train_sampler = DistributedSampler(train_data)
-        train_dataloader = DataLoader(
-            train_data, sampler=train_sampler, batch_size=self.args.train_batch_size
-        )
-
-        self.model.train()
-        for _ in trange(int(self.args.num_train_epochs), desc="Epoch"):
-            tr_loss = 0
-            nb_tr_examples, nb_tr_steps = 0, 0
-            for step, batch in enumerate(tqdm(train_dataloader, desc="Iteration")):
-                batch = tuple(t.to(self.device) for t in batch)
-                input_ids, input_mask, segment_ids, label_ids = batch
-
-                # define a new function to compute loss values for both output_modes
-                logits = self.model(input_ids, segment_ids, input_mask, labels=None)
-
-                if self.output_mode == "classification":
-                    loss_fct = CrossEntropyLoss()
-                    loss = loss_fct(
-                        logits.view(-1, self.num_labels), label_ids.view(-1)
-                    )
-
-                if self.args.gradient_accumulation_steps > 1:
-                    loss = loss / self.args.gradient_accumulation_steps
-
-                loss.backward()
-
-                tr_loss += loss.item()
-                nb_tr_examples += input_ids.size(0)
-                nb_tr_steps += 1
-                if (step + 1) % self.args.gradient_accumulation_steps == 0:
-                    optimizer.step()
-                    optimizer.zero_grad()
-                    global_step += 1
-
-        if self.args.local_rank == -1 or torch.distributed.get_rank() == 0:
-            # Save a trained model, configuration and tokenizer
-            model_to_save = (
-                self.model.module if hasattr(self.model, "module") else self.model
-            )  # Only save the model it-self
-
-            # If we save using the predefined names, we can load using `from_pretrained`
-            output_model_file = os.path.join(self.args.model_dir, WEIGHTS_NAME)
-            output_config_file = os.path.join(self.args.model_dir, CONFIG_NAME)
-
-            torch.save(model_to_save.state_dict(), output_model_file)
-            model_to_save.config.to_json_file(output_config_file)
-            self.tokenizer.save_vocabulary(self.args.model_dir)
-
-            # Load a trained model and vocabulary that you have fine-tuned
-            self.model = BertForSequenceClassification.from_pretrained(
-                self.args.model_dir, num_labels=self.num_labels
-            )
-            self.tokenizer = BertTokenizer.from_pretrained(
-                self.args.model_dir, do_lower_case=self.args.do_lower_case
-            )
-        else:
-            self.model = BertForSequenceClassification.from_pretrained(
-                self.args.bert_model, num_labels=self.num_labels
-            )
-        self.model.to(self.device)
-
-        self.tr_loss, self.global_step = tr_loss, global_step
-
-        self.logger.info("DONE TRAINING."),
-
-    def eval(self) -> None:
-        if not (self.args.local_rank == -1 or torch.distributed.get_rank() == 0):
-            return
-
-        eval_examples = self.processor.get_dev_examples(self.args.training_data_dir)
-        eval_features = BertUtil.convert_examples_to_features(
-            eval_examples,
-            self.label_list,
-            self.args.max_seq_length,
-            self.tokenizer,
-            self.output_mode,
-        )
-        self.logger.info("***** Running evaluation *****")
-        self.logger.info("  Num examples = %d", len(eval_examples))
-        self.logger.info("  Batch size = %d", self.args.eval_batch_size)
-        all_input_ids = torch.tensor(
-            [f.input_ids for f in eval_features], dtype=torch.long
-        )
-        all_input_mask = torch.tensor(
-            [f.input_mask for f in eval_features], dtype=torch.long
-        )
-        all_segment_ids = torch.tensor(
-            [f.segment_ids for f in eval_features], dtype=torch.long
-        )
-
-        if self.output_mode == "classification":
-            all_label_ids = torch.tensor(
-                [f.label_id for f in eval_features], dtype=torch.long
-            )
-
-        eval_data = TensorDataset(
-            all_input_ids, all_input_mask, all_segment_ids, all_label_ids
-        )
-        # Run prediction for full data
-        eval_sampler = SequentialSampler(eval_data)
-        eval_dataloader = DataLoader(
-            eval_data, sampler=eval_sampler, batch_size=self.args.eval_batch_size
-        )
-
-        self.model.eval()
-        eval_loss = 0
-        nb_eval_steps = 0
-        preds = []
-
-        for input_ids, input_mask, segment_ids, label_ids in tqdm(
-            eval_dataloader, desc="Evaluating"
-        ):
-            input_ids = input_ids.to(self.device)
-            input_mask = input_mask.to(self.device)
-            segment_ids = segment_ids.to(self.device)
-            label_ids = label_ids.to(self.device)
-
-            with torch.no_grad():
-                logits = self.model(input_ids, segment_ids, input_mask, labels=None)
-
-            # create eval loss and other metric required by the task
-            if self.output_mode == "classification":
-                loss_fct = CrossEntropyLoss()
-                tmp_eval_loss = loss_fct(
-                    logits.view(-1, self.num_labels), label_ids.view(-1)
-                )
-
-            eval_loss += tmp_eval_loss.mean().item()
-            nb_eval_steps += 1
-            if len(preds) == 0:
-                preds.append(logits.detach().cpu().numpy())
-            else:
-                preds[0] = np.append(preds[0], logits.detach().cpu().numpy(), axis=0)
-
-        eval_loss = eval_loss / nb_eval_steps
-        preds = preds[0]
-        if self.output_mode == "classification":
-            preds = np.argmax(preds, axis=1)
-        result = BertUtil.compute_metrics(self.task_name, preds, all_label_ids.numpy())
-        loss = self.tr_loss / self.global_step if self.args.do_train else None
-
-        result["eval_loss"] = eval_loss
-        result["global_step"] = self.global_step
-        result["loss"] = loss
-
-        output_eval_file = os.path.join(self.args.model_dir, "eval_results.txt")
-        with open(output_eval_file, "w") as writer:
-            self.logger.info("***** Eval results *****")
-            for key in sorted(result.keys()):
-                self.logger.info("  %s = %s", key, str(result[key]))
-                writer.write("%s = %s\n" % (key, str(result[key])))
-
-        self.logger.info("DONE EVALUATING.")
-
-    def _prepare(self, cleanup_output_dir: bool = False) -> None:
-        if self.args.local_rank == -1 or self.args.no_cuda:
-            self.device = torch.device(
-                "cuda" if torch.cuda.is_available() and not self.args.no_cuda else "cpu"
-            )
-            self.n_gpu = torch.cuda.device_count()
-        else:
-            torch.cuda.set_device(self.args.local_rank)
-            self.device = torch.device("cuda", self.args.local_rank)
-            self.n_gpu = 1
-            # Initializes the distributed backend which will take care of sychronizing nodes/GPUs
-            torch.distributed.init_process_group(backend="nccl")
-
-        logging.basicConfig(
-            format="%(asctime)s - %(levelname)s - %(name)s -   %(message)s",
-            datefmt="%m/%d/%Y %H:%M:%S",
-            level=logging.INFO if self.args.local_rank in [-1, 0] else logging.WARN,
-        )
-
-        self.logger.info(
-            "device: {} n_gpu: {}, distributed training: {}, 16-bits training: {}".format(
-                self.device,
-                self.n_gpu,
-                bool(self.args.local_rank != -1),
-                self.args.fp16,
-            )
-        )
-
-        if self.args.gradient_accumulation_steps < 1:
-            raise ValueError(
-                "Invalid gradient_accumulation_steps parameter: {}, should be >= 1".format(
-                    self.args.gradient_accumulation_steps
-                )
-            )
-
-        self.args.train_batch_size = (
-            self.args.train_batch_size // self.args.gradient_accumulation_steps
-        )
-
-        random.seed(self.args.seed)
-        np.random.seed(self.args.seed)
-        torch.manual_seed(self.args.seed)
-        if self.n_gpu > 0:
-            torch.cuda.manual_seed_all(self.args.seed)
-
-        if not self.args.do_train and not self.args.do_eval:
-            raise ValueError("At least one of `do_train` or `do_eval` must be True.")
-
-        if self.args.cleanup_output_dir:
-            if os.path.exists(self.args.model_dir):
-                shutil.rmtree(self.args.model_dir)
-
-        if (
-            os.path.exists(self.args.model_dir)
-            and os.listdir(self.args.model_dir)
-            and self.args.do_train
-        ):
-            raise ValueError(
-                "Output directory ({}) already exists and is not empty.".format(
-                    self.args.model_dir
-                )
-            )
-        if not os.path.exists(self.args.model_dir):
-            os.makedirs(self.args.model_dir)
-
-        self.task_name = self.args.task_name.lower()
-
-        self.label_list = self.processor.get_labels()
-        self.num_labels = len(self.label_list)
-
-        self.tokenizer = BertTokenizer.from_pretrained(
-            self.args.bert_model, do_lower_case=self.args.do_lower_case
-        )
-
-        self.train_examples = None
-        self.num_train_optimization_steps = None
-        if self.args.do_train:
-            self.train_examples = self.processor.get_train_examples(
-                self.args.training_data_dir
-            )
-            self.num_train_optimization_steps = (
-                int(
-                    len(self.train_examples)
-                    / self.args.train_batch_size
-                    / self.args.gradient_accumulation_steps
-                )
-                * self.args.num_train_epochs
-            )
-            if self.args.local_rank != -1:
-                self.num_train_optimization_steps = (
-                    self.num_train_optimization_steps
-                    // torch.distributed.get_world_size()
-                )
-
-    def _prepare_model(self) -> BertPreTrainedModel:
-        if self.args.cache_dir:
-            cache_dir = self.args.cache_dir
-        else:
-            cache_dir = os.path.join(
-                str(PYTORCH_PRETRAINED_BERT_CACHE),
-                f"distributed_{self.args.local_rank}",
-            )
-        model = BertForSequenceClassification.from_pretrained(
-            self.args.bert_model, cache_dir=cache_dir, num_labels=self.num_labels
-        )
-        model.to(self.device)
-        return model
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import logging
+import os
+import random
+import shutil
+import numpy as np
+import torch
+from .args import Args
+
+from model_corebot101.bert.common.bert_util import BertUtil
+from model_corebot101.bert.train.flight_booking_processor import FlightBookingProcessor
+from pytorch_pretrained_bert.file_utils import (
+    CONFIG_NAME,
+    PYTORCH_PRETRAINED_BERT_CACHE,
+    WEIGHTS_NAME,
+)
+from pytorch_pretrained_bert.modeling import (
+    BertForSequenceClassification,
+    BertPreTrainedModel,
+)
+from pytorch_pretrained_bert.optimization import BertAdam
+from pytorch_pretrained_bert.tokenization import BertTokenizer
+from torch.nn import CrossEntropyLoss
+from torch.utils.data import DataLoader, RandomSampler, SequentialSampler, TensorDataset
+from torch.utils.data.distributed import DistributedSampler
+
+from tqdm import tqdm, trange
+
+
+class BertTrainEval:
+    logger = logging.getLogger(__name__)
+
+    def __init__(self, args: Args):
+        self.processor = FlightBookingProcessor()
+        self.output_mode = "classification"
+        self.args = args
+        self._prepare()
+        self.model = self._prepare_model()
+
+    @classmethod
+    def train_eval(cls, cleanup_output_dir: bool = False) -> None:
+        # uncomment the following line for debugging.
+        # import pdb; pdb.set_trace()
+        args = Args.for_flight_booking()
+        args.do_train = True
+        args.do_eval = True
+        args.cleanup_output_dir = cleanup_output_dir
+        bert = cls(args)
+        bert.train()
+        bert.eval()
+
+    def train(self) -> None:
+        # Prepare optimizer
+        param_optimizer = list(self.model.named_parameters())
+        no_decay = ["bias", "LayerNorm.bias", "LayerNorm.weight"]
+        optimizer_grouped_parameters = [
+            {
+                "params": [
+                    p for n, p in param_optimizer if not any(nd in n for nd in no_decay)
+                ],
+                "weight_decay": 0.01,
+            },
+            {
+                "params": [
+                    p for n, p in param_optimizer if any(nd in n for nd in no_decay)
+                ],
+                "weight_decay": 0.0,
+            },
+        ]
+        optimizer = BertAdam(
+            optimizer_grouped_parameters,
+            lr=self.args.learning_rate,
+            warmup=self.args.warmup_proportion,
+            t_total=self.num_train_optimization_steps,
+        )
+
+        global_step: int = 0
+        nb_tr_steps = 0
+        tr_loss: float = 0
+        train_features = BertUtil.convert_examples_to_features(
+            self.train_examples,
+            self.label_list,
+            self.args.max_seq_length,
+            self.tokenizer,
+            self.output_mode,
+        )
+        self.logger.info("***** Running training *****")
+        self.logger.info("  Num examples = %d", len(self.train_examples))
+        self.logger.info("  Batch size = %d", self.args.train_batch_size)
+        self.logger.info("  Num steps = %d", self.num_train_optimization_steps)
+        all_input_ids = torch.tensor(
+            [f.input_ids for f in train_features], dtype=torch.long
+        )
+        all_input_mask = torch.tensor(
+            [f.input_mask for f in train_features], dtype=torch.long
+        )
+        all_segment_ids = torch.tensor(
+            [f.segment_ids for f in train_features], dtype=torch.long
+        )
+
+        if self.output_mode == "classification":
+            all_label_ids = torch.tensor(
+                [f.label_id for f in train_features], dtype=torch.long
+            )
+
+        train_data = TensorDataset(
+            all_input_ids, all_input_mask, all_segment_ids, all_label_ids
+        )
+        if self.args.local_rank == -1:
+            train_sampler = RandomSampler(train_data)
+        else:
+            train_sampler = DistributedSampler(train_data)
+        train_dataloader = DataLoader(
+            train_data, sampler=train_sampler, batch_size=self.args.train_batch_size
+        )
+
+        self.model.train()
+        for _ in trange(int(self.args.num_train_epochs), desc="Epoch"):
+            tr_loss = 0
+            nb_tr_examples, nb_tr_steps = 0, 0
+            for step, batch in enumerate(tqdm(train_dataloader, desc="Iteration")):
+                batch = tuple(t.to(self.device) for t in batch)
+                input_ids, input_mask, segment_ids, label_ids = batch
+
+                # define a new function to compute loss values for both output_modes
+                logits = self.model(input_ids, segment_ids, input_mask, labels=None)
+
+                if self.output_mode == "classification":
+                    loss_fct = CrossEntropyLoss()
+                    loss = loss_fct(
+                        logits.view(-1, self.num_labels), label_ids.view(-1)
+                    )
+
+                if self.args.gradient_accumulation_steps > 1:
+                    loss = loss / self.args.gradient_accumulation_steps
+
+                loss.backward()
+
+                tr_loss += loss.item()
+                nb_tr_examples += input_ids.size(0)
+                nb_tr_steps += 1
+                if (step + 1) % self.args.gradient_accumulation_steps == 0:
+                    optimizer.step()
+                    optimizer.zero_grad()
+                    global_step += 1
+
+        if self.args.local_rank == -1 or torch.distributed.get_rank() == 0:
+            # Save a trained model, configuration and tokenizer
+            model_to_save = (
+                self.model.module if hasattr(self.model, "module") else self.model
+            )  # Only save the model it-self
+
+            # If we save using the predefined names, we can load using `from_pretrained`
+            output_model_file = os.path.join(self.args.model_dir, WEIGHTS_NAME)
+            output_config_file = os.path.join(self.args.model_dir, CONFIG_NAME)
+
+            torch.save(model_to_save.state_dict(), output_model_file)
+            model_to_save.config.to_json_file(output_config_file)
+            self.tokenizer.save_vocabulary(self.args.model_dir)
+
+            # Load a trained model and vocabulary that you have fine-tuned
+            self.model = BertForSequenceClassification.from_pretrained(
+                self.args.model_dir, num_labels=self.num_labels
+            )
+            self.tokenizer = BertTokenizer.from_pretrained(
+                self.args.model_dir, do_lower_case=self.args.do_lower_case
+            )
+        else:
+            self.model = BertForSequenceClassification.from_pretrained(
+                self.args.bert_model, num_labels=self.num_labels
+            )
+        self.model.to(self.device)
+
+        self.tr_loss, self.global_step = tr_loss, global_step
+
+        self.logger.info("DONE TRAINING."),
+
+    def eval(self) -> None:
+        if not (self.args.local_rank == -1 or torch.distributed.get_rank() == 0):
+            return
+
+        eval_examples = self.processor.get_dev_examples(self.args.training_data_dir)
+        eval_features = BertUtil.convert_examples_to_features(
+            eval_examples,
+            self.label_list,
+            self.args.max_seq_length,
+            self.tokenizer,
+            self.output_mode,
+        )
+        self.logger.info("***** Running evaluation *****")
+        self.logger.info("  Num examples = %d", len(eval_examples))
+        self.logger.info("  Batch size = %d", self.args.eval_batch_size)
+        all_input_ids = torch.tensor(
+            [f.input_ids for f in eval_features], dtype=torch.long
+        )
+        all_input_mask = torch.tensor(
+            [f.input_mask for f in eval_features], dtype=torch.long
+        )
+        all_segment_ids = torch.tensor(
+            [f.segment_ids for f in eval_features], dtype=torch.long
+        )
+
+        if self.output_mode == "classification":
+            all_label_ids = torch.tensor(
+                [f.label_id for f in eval_features], dtype=torch.long
+            )
+
+        eval_data = TensorDataset(
+            all_input_ids, all_input_mask, all_segment_ids, all_label_ids
+        )
+        # Run prediction for full data
+        eval_sampler = SequentialSampler(eval_data)
+        eval_dataloader = DataLoader(
+            eval_data, sampler=eval_sampler, batch_size=self.args.eval_batch_size
+        )
+
+        self.model.eval()
+        eval_loss = 0
+        nb_eval_steps = 0
+        preds = []
+
+        for input_ids, input_mask, segment_ids, label_ids in tqdm(
+            eval_dataloader, desc="Evaluating"
+        ):
+            input_ids = input_ids.to(self.device)
+            input_mask = input_mask.to(self.device)
+            segment_ids = segment_ids.to(self.device)
+            label_ids = label_ids.to(self.device)
+
+            with torch.no_grad():
+                logits = self.model(input_ids, segment_ids, input_mask, labels=None)
+
+            # create eval loss and other metric required by the task
+            if self.output_mode == "classification":
+                loss_fct = CrossEntropyLoss()
+                tmp_eval_loss = loss_fct(
+                    logits.view(-1, self.num_labels), label_ids.view(-1)
+                )
+
+            eval_loss += tmp_eval_loss.mean().item()
+            nb_eval_steps += 1
+            if len(preds) == 0:
+                preds.append(logits.detach().cpu().numpy())
+            else:
+                preds[0] = np.append(preds[0], logits.detach().cpu().numpy(), axis=0)
+
+        eval_loss = eval_loss / nb_eval_steps
+        preds = preds[0]
+        if self.output_mode == "classification":
+            preds = np.argmax(preds, axis=1)
+        result = BertUtil.compute_metrics(self.task_name, preds, all_label_ids.numpy())
+        loss = self.tr_loss / self.global_step if self.args.do_train else None
+
+        result["eval_loss"] = eval_loss
+        result["global_step"] = self.global_step
+        result["loss"] = loss
+
+        output_eval_file = os.path.join(self.args.model_dir, "eval_results.txt")
+        with open(output_eval_file, "w") as writer:
+            self.logger.info("***** Eval results *****")
+            for key in sorted(result.keys()):
+                self.logger.info("  %s = %s", key, str(result[key]))
+                writer.write("%s = %s\n" % (key, str(result[key])))
+
+        self.logger.info("DONE EVALUATING.")
+
+    def _prepare(self, cleanup_output_dir: bool = False) -> None:
+        if self.args.local_rank == -1 or self.args.no_cuda:
+            self.device = torch.device(
+                "cuda" if torch.cuda.is_available() and not self.args.no_cuda else "cpu"
+            )
+            self.n_gpu = torch.cuda.device_count()
+        else:
+            torch.cuda.set_device(self.args.local_rank)
+            self.device = torch.device("cuda", self.args.local_rank)
+            self.n_gpu = 1
+            # Initializes the distributed backend which will take care of sychronizing nodes/GPUs
+            torch.distributed.init_process_group(backend="nccl")
+
+        logging.basicConfig(
+            format="%(asctime)s - %(levelname)s - %(name)s -   %(message)s",
+            datefmt="%m/%d/%Y %H:%M:%S",
+            level=logging.INFO if self.args.local_rank in [-1, 0] else logging.WARN,
+        )
+
+        self.logger.info(
+            "device: {} n_gpu: {}, distributed training: {}, 16-bits training: {}".format(
+                self.device,
+                self.n_gpu,
+                bool(self.args.local_rank != -1),
+                self.args.fp16,
+            )
+        )
+
+        if self.args.gradient_accumulation_steps < 1:
+            raise ValueError(
+                "Invalid gradient_accumulation_steps parameter: {}, should be >= 1".format(
+                    self.args.gradient_accumulation_steps
+                )
+            )
+
+        self.args.train_batch_size = (
+            self.args.train_batch_size // self.args.gradient_accumulation_steps
+        )
+
+        random.seed(self.args.seed)
+        np.random.seed(self.args.seed)
+        torch.manual_seed(self.args.seed)
+        if self.n_gpu > 0:
+            torch.cuda.manual_seed_all(self.args.seed)
+
+        if not self.args.do_train and not self.args.do_eval:
+            raise ValueError("At least one of `do_train` or `do_eval` must be True.")
+
+        if self.args.cleanup_output_dir:
+            if os.path.exists(self.args.model_dir):
+                shutil.rmtree(self.args.model_dir)
+
+        if (
+            os.path.exists(self.args.model_dir)
+            and os.listdir(self.args.model_dir)
+            and self.args.do_train
+        ):
+            raise ValueError(
+                "Output directory ({}) already exists and is not empty.".format(
+                    self.args.model_dir
+                )
+            )
+        if not os.path.exists(self.args.model_dir):
+            os.makedirs(self.args.model_dir)
+
+        self.task_name = self.args.task_name.lower()
+
+        self.label_list = self.processor.get_labels()
+        self.num_labels = len(self.label_list)
+
+        self.tokenizer = BertTokenizer.from_pretrained(
+            self.args.bert_model, do_lower_case=self.args.do_lower_case
+        )
+
+        self.train_examples = None
+        self.num_train_optimization_steps = None
+        if self.args.do_train:
+            self.train_examples = self.processor.get_train_examples(
+                self.args.training_data_dir
+            )
+            self.num_train_optimization_steps = (
+                int(
+                    len(self.train_examples)
+                    / self.args.train_batch_size
+                    / self.args.gradient_accumulation_steps
+                )
+                * self.args.num_train_epochs
+            )
+            if self.args.local_rank != -1:
+                self.num_train_optimization_steps = (
+                    self.num_train_optimization_steps
+                    // torch.distributed.get_world_size()
+                )
+
+    def _prepare_model(self) -> BertPreTrainedModel:
+        if self.args.cache_dir:
+            cache_dir = self.args.cache_dir
+        else:
+            cache_dir = os.path.join(
+                str(PYTORCH_PRETRAINED_BERT_CACHE),
+                f"distributed_{self.args.local_rank}",
+            )
+        model = BertForSequenceClassification.from_pretrained(
+            self.args.bert_model, cache_dir=cache_dir, num_labels=self.num_labels
+        )
+        model.to(self.device)
+        return model
diff --git a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/train/flight_booking_processor.py b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/train/flight_booking_processor.py
similarity index 96%
rename from samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/train/flight_booking_processor.py
rename to tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/train/flight_booking_processor.py
index f59759d53..b1104ce92 100644
--- a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/train/flight_booking_processor.py
+++ b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/train/flight_booking_processor.py
@@ -1,51 +1,51 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import json
-import os
-from typing import List, Tuple
-
-from model_corebot101.bert.common.input_example import InputExample
-
-
-class FlightBookingProcessor:
-    """Processor for the flight booking data set."""
-
-    def get_train_examples(self, data_dir):
-        """See base class."""
-        return self._create_examples(
-            self._read_json(os.path.join(data_dir, "FlightBooking.json")), "train"
-        )
-
-    def get_dev_examples(self, data_dir):
-        """See base class."""
-        return self._create_examples(
-            self._read_json(os.path.join(data_dir, "FlightBooking.json")), "dev"
-        )
-
-    def get_labels(self):
-        """See base class."""
-        return ["Book flight", "Cancel"]
-
-    def _create_examples(self, lines, set_type):
-        """Creates examples for the training and dev sets."""
-        examples = []
-        for (i, line) in enumerate(lines):
-            guid = "%s-%s" % (set_type, i)
-            text_a = line[1]
-            label = line[0]
-            examples.append(
-                InputExample(guid=guid, text_a=text_a, text_b=None, label=label)
-            )
-        return examples
-
-    @classmethod
-    def _read_json(cls, input_file):
-        with open(input_file, "r", encoding="utf-8") as f:
-            obj = json.load(f)
-            examples = obj["utterances"]
-            lines: List[Tuple[str, str]] = []
-            for example in examples:
-                lines.append((example["intent"], example["text"]))
-
-            return lines
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import json
+import os
+from typing import List, Tuple
+
+from model_corebot101.bert.common.input_example import InputExample
+
+
+class FlightBookingProcessor:
+    """Processor for the flight booking data set."""
+
+    def get_train_examples(self, data_dir):
+        """See base class."""
+        return self._create_examples(
+            self._read_json(os.path.join(data_dir, "FlightBooking.json")), "train"
+        )
+
+    def get_dev_examples(self, data_dir):
+        """See base class."""
+        return self._create_examples(
+            self._read_json(os.path.join(data_dir, "FlightBooking.json")), "dev"
+        )
+
+    def get_labels(self):
+        """See base class."""
+        return ["Book flight", "Cancel"]
+
+    def _create_examples(self, lines, set_type):
+        """Creates examples for the training and dev sets."""
+        examples = []
+        for (i, line) in enumerate(lines):
+            guid = "%s-%s" % (set_type, i)
+            text_a = line[1]
+            label = line[0]
+            examples.append(
+                InputExample(guid=guid, text_a=text_a, text_b=None, label=label)
+            )
+        return examples
+
+    @classmethod
+    def _read_json(cls, input_file):
+        with open(input_file, "r", encoding="utf-8") as f:
+            obj = json.load(f)
+            examples = obj["utterances"]
+            lines: List[Tuple[str, str]] = []
+            for example in examples:
+                lines.append((example["intent"], example["text"]))
+
+            return lines
diff --git a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/training_data/FlightBooking.json b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/training_data/FlightBooking.json
similarity index 94%
rename from samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/training_data/FlightBooking.json
rename to tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/training_data/FlightBooking.json
index 43781ee85..e2b881b21 100644
--- a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/training_data/FlightBooking.json
+++ b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bert/training_data/FlightBooking.json
@@ -1,241 +1,241 @@
-{
-  "luis_schema_version": "3.2.0",
-  "versionId": "0.1",
-  "name": "Airline Reservation",
-  "desc": "A LUIS model that uses intent and entities.",
-  "culture": "en-us",
-  "tokenizerVersion": "1.0.0",
-  "intents": [
-    {
-      "name": "Book flight"
-    },
-    {
-      "name": "Cancel"
-    },
-    {
-      "name": "None"
-    }
-  ],
-  "entities": [],
-  "composites": [
-    {
-      "name": "From",
-      "children": [
-        "Airport"
-      ],
-      "roles": []
-    },
-    {
-      "name": "To",
-      "children": [
-        "Airport"
-      ],
-      "roles": []
-    }
-  ],
-  "closedLists": [
-    {
-      "name": "Airport",
-      "subLists": [
-        {
-          "canonicalForm": "Paris",
-          "list": [
-            "paris"
-          ]
-        },
-        {
-          "canonicalForm": "London",
-          "list": [
-            "london"
-          ]
-        },
-        {
-          "canonicalForm": "Berlin",
-          "list": [
-            "berlin"
-          ]
-        },
-        {
-          "canonicalForm": "New York",
-          "list": [
-            "new york"
-          ]
-        }
-      ],
-      "roles": []
-    }
-  ],
-  "patternAnyEntities": [],
-  "regex_entities": [],
-  "prebuiltEntities": [
-    {
-      "name": "datetimeV2",
-      "roles": []
-    }
-  ],
-  "model_features": [],
-  "regex_features": [],
-  "patterns": [],
-  "utterances": [
-    {
-      "text": "book flight from london to paris on feb 14th",
-      "intent": "Book flight",
-      "entities": [
-        {
-          "entity": "To",
-          "startPos": 27,
-          "endPos": 31
-        },
-        {
-          "entity": "From",
-          "startPos": 17,
-          "endPos": 22
-        }
-      ]
-    },
-    {
-      "text": "book flight to berlin on feb 14th",
-      "intent": "Book flight",
-      "entities": [
-        {
-          "entity": "To",
-          "startPos": 15,
-          "endPos": 20
-        }
-      ]
-    },
-    {
-      "text": "book me a flight from london to paris",
-      "intent": "Book flight",
-      "entities": [
-        {
-          "entity": "From",
-          "startPos": 22,
-          "endPos": 27
-        },
-        {
-          "entity": "To",
-          "startPos": 32,
-          "endPos": 36
-        }
-      ]
-    },
-    {
-      "text": "bye",
-      "intent": "Cancel",
-      "entities": []
-    },
-    {
-      "text": "cancel booking",
-      "intent": "Cancel",
-      "entities": []
-    },
-    {
-      "text": "exit",
-      "intent": "Cancel",
-      "entities": []
-    },
-    {
-      "text": "I don't want that",
-      "intent": "Cancel",
-      "entities": []
-    },
-    {
-      "text": "not this one",
-      "intent": "Cancel",
-      "entities": []
-    },
-    {
-      "text": "don't want that",
-      "intent": "Cancel",
-      "entities": []
-    },
-    {
-      "text": "flight to paris",
-      "intent": "Book flight",
-      "entities": [
-        {
-          "entity": "To",
-          "startPos": 10,
-          "endPos": 14
-        }
-      ]
-    },
-    {
-      "text": "flight to paris from london on feb 14th",
-      "intent": "Book flight",
-      "entities": [
-        {
-          "entity": "To",
-          "startPos": 10,
-          "endPos": 14
-        },
-        {
-          "entity": "From",
-          "startPos": 21,
-          "endPos": 26
-        }
-      ]
-    },
-    {
-      "text": "fly from berlin to paris on may 5th",
-      "intent": "Book flight",
-      "entities": [
-        {
-          "entity": "To",
-          "startPos": 19,
-          "endPos": 23
-        },
-        {
-          "entity": "From",
-          "startPos": 9,
-          "endPos": 14
-        }
-      ]
-    },
-    {
-      "text": "go to paris",
-      "intent": "Book flight",
-      "entities": [
-        {
-          "entity": "To",
-          "startPos": 6,
-          "endPos": 10
-        }
-      ]
-    },
-    {
-      "text": "going from paris to berlin",
-      "intent": "Book flight",
-      "entities": [
-        {
-          "entity": "To",
-          "startPos": 20,
-          "endPos": 25
-        },
-        {
-          "entity": "From",
-          "startPos": 11,
-          "endPos": 15
-        }
-      ]
-    },
-    {
-      "text": "ignore",
-      "intent": "Cancel",
-      "entities": []
-    },
-    {
-      "text": "travel to paris",
-      "intent": "Book flight",
-      "entities": [
-        {
-          "entity": "To",
-          "startPos": 10,
-          "endPos": 14
-        }
-      ]
-    }
-  ],
-  "settings": []
+{
+  "luis_schema_version": "3.2.0",
+  "versionId": "0.1",
+  "name": "Airline Reservation",
+  "desc": "A LUIS model that uses intent and entities.",
+  "culture": "en-us",
+  "tokenizerVersion": "1.0.0",
+  "intents": [
+    {
+      "name": "Book flight"
+    },
+    {
+      "name": "Cancel"
+    },
+    {
+      "name": "None"
+    }
+  ],
+  "entities": [],
+  "composites": [
+    {
+      "name": "From",
+      "children": [
+        "Airport"
+      ],
+      "roles": []
+    },
+    {
+      "name": "To",
+      "children": [
+        "Airport"
+      ],
+      "roles": []
+    }
+  ],
+  "closedLists": [
+    {
+      "name": "Airport",
+      "subLists": [
+        {
+          "canonicalForm": "Paris",
+          "list": [
+            "paris"
+          ]
+        },
+        {
+          "canonicalForm": "London",
+          "list": [
+            "london"
+          ]
+        },
+        {
+          "canonicalForm": "Berlin",
+          "list": [
+            "berlin"
+          ]
+        },
+        {
+          "canonicalForm": "New York",
+          "list": [
+            "new york"
+          ]
+        }
+      ],
+      "roles": []
+    }
+  ],
+  "patternAnyEntities": [],
+  "regex_entities": [],
+  "prebuiltEntities": [
+    {
+      "name": "datetimeV2",
+      "roles": []
+    }
+  ],
+  "model_features": [],
+  "regex_features": [],
+  "patterns": [],
+  "utterances": [
+    {
+      "text": "book flight from london to paris on feb 14th",
+      "intent": "Book flight",
+      "entities": [
+        {
+          "entity": "To",
+          "startPos": 27,
+          "endPos": 31
+        },
+        {
+          "entity": "From",
+          "startPos": 17,
+          "endPos": 22
+        }
+      ]
+    },
+    {
+      "text": "book flight to berlin on feb 14th",
+      "intent": "Book flight",
+      "entities": [
+        {
+          "entity": "To",
+          "startPos": 15,
+          "endPos": 20
+        }
+      ]
+    },
+    {
+      "text": "book me a flight from london to paris",
+      "intent": "Book flight",
+      "entities": [
+        {
+          "entity": "From",
+          "startPos": 22,
+          "endPos": 27
+        },
+        {
+          "entity": "To",
+          "startPos": 32,
+          "endPos": 36
+        }
+      ]
+    },
+    {
+      "text": "bye",
+      "intent": "Cancel",
+      "entities": []
+    },
+    {
+      "text": "cancel booking",
+      "intent": "Cancel",
+      "entities": []
+    },
+    {
+      "text": "exit",
+      "intent": "Cancel",
+      "entities": []
+    },
+    {
+      "text": "I don't want that",
+      "intent": "Cancel",
+      "entities": []
+    },
+    {
+      "text": "not this one",
+      "intent": "Cancel",
+      "entities": []
+    },
+    {
+      "text": "don't want that",
+      "intent": "Cancel",
+      "entities": []
+    },
+    {
+      "text": "flight to paris",
+      "intent": "Book flight",
+      "entities": [
+        {
+          "entity": "To",
+          "startPos": 10,
+          "endPos": 14
+        }
+      ]
+    },
+    {
+      "text": "flight to paris from london on feb 14th",
+      "intent": "Book flight",
+      "entities": [
+        {
+          "entity": "To",
+          "startPos": 10,
+          "endPos": 14
+        },
+        {
+          "entity": "From",
+          "startPos": 21,
+          "endPos": 26
+        }
+      ]
+    },
+    {
+      "text": "fly from berlin to paris on may 5th",
+      "intent": "Book flight",
+      "entities": [
+        {
+          "entity": "To",
+          "startPos": 19,
+          "endPos": 23
+        },
+        {
+          "entity": "From",
+          "startPos": 9,
+          "endPos": 14
+        }
+      ]
+    },
+    {
+      "text": "go to paris",
+      "intent": "Book flight",
+      "entities": [
+        {
+          "entity": "To",
+          "startPos": 6,
+          "endPos": 10
+        }
+      ]
+    },
+    {
+      "text": "going from paris to berlin",
+      "intent": "Book flight",
+      "entities": [
+        {
+          "entity": "To",
+          "startPos": 20,
+          "endPos": 25
+        },
+        {
+          "entity": "From",
+          "startPos": 11,
+          "endPos": 15
+        }
+      ]
+    },
+    {
+      "text": "ignore",
+      "intent": "Cancel",
+      "entities": []
+    },
+    {
+      "text": "travel to paris",
+      "intent": "Book flight",
+      "entities": [
+        {
+          "entity": "To",
+          "startPos": 10,
+          "endPos": 14
+        }
+      ]
+    }
+  ],
+  "settings": []
 }
\ No newline at end of file
diff --git a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bidaf/model_runtime/__init__.py b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bidaf/model_runtime/__init__.py
similarity index 96%
rename from samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bidaf/model_runtime/__init__.py
rename to tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bidaf/model_runtime/__init__.py
index a7780a076..9d191b568 100644
--- a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bidaf/model_runtime/__init__.py
+++ b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bidaf/model_runtime/__init__.py
@@ -1,6 +1,6 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from .bidaf_model_runtime import BidafModelRuntime
-
-__all__ = ["BidafModelRuntime"]
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from .bidaf_model_runtime import BidafModelRuntime
+
+__all__ = ["BidafModelRuntime"]
diff --git a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bidaf/model_runtime/bidaf_model_runtime.py b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bidaf/model_runtime/bidaf_model_runtime.py
similarity index 97%
rename from samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bidaf/model_runtime/bidaf_model_runtime.py
rename to tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bidaf/model_runtime/bidaf_model_runtime.py
index 982e31054..2f3ed506e 100644
--- a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bidaf/model_runtime/bidaf_model_runtime.py
+++ b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bidaf/model_runtime/bidaf_model_runtime.py
@@ -1,101 +1,101 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-import os
-import sys
-import requests
-import shutil
-from typing import Dict, List, Tuple
-import nltk
-import numpy as np
-from nltk import word_tokenize
-from onnxruntime import InferenceSession
-
-# pylint:disable=line-too-long
-class BidafModelRuntime:
-    def __init__(self, targets: List[str], queries: Dict[str, str], model_dir: str):
-        self.queries = queries
-        self.targets = targets
-        bidaf_model = os.path.abspath(os.path.join(model_dir, "bidaf.onnx"))
-        print(f"Loading Inference session from {bidaf_model}..", file=sys.stderr)
-        self.session = InferenceSession(bidaf_model)
-        print(f"Inference session loaded..", file=sys.stderr)
-        self.processed_queries = self._process_queries()
-        print(f"Processed queries..", file=sys.stderr)
-
-    @staticmethod
-    def init_bidaf(bidaf_model_dir: str, download_ntlk_punkt: bool = False) -> bool:
-        if os.path.isdir(bidaf_model_dir):
-            print("bidaf model directory already present..", file=sys.stderr)
-        else:
-            print("Creating bidaf model directory..", file=sys.stderr)
-            os.makedirs(bidaf_model_dir, exist_ok=True)
-
-        # Download Punkt Sentence Tokenizer
-        if download_ntlk_punkt:
-            nltk.download("punkt", download_dir=bidaf_model_dir)
-            nltk.download("punkt")
-
-        # Download bidaf onnx model
-        onnx_model_file = os.path.abspath(os.path.join(bidaf_model_dir, "bidaf.onnx"))
-
-        print(f"Checking file {onnx_model_file}..", file=sys.stderr)
-        if os.path.isfile(onnx_model_file):
-            print("bidaf.onnx downloaded already!", file=sys.stderr)
-        else:
-            print("Downloading bidaf.onnx...", file=sys.stderr)
-            response = requests.get(
-                "https://onnxzoo.blob.core.windows.net/models/opset_9/bidaf/bidaf.onnx",
-                stream=True,
-            )
-            with open(onnx_model_file, "wb") as f:
-                response.raw.decode_content = True
-                shutil.copyfileobj(response.raw, f)
-        return True
-
-    def serve(self, context: str) -> Dict[str, str]:
-        result = {}
-        cw, cc = BidafModelRuntime._preprocess(context)
-        for target in self.targets:
-            qw, qc = self.processed_queries[target]
-            answer = self.session.run(
-                ["start_pos", "end_pos"],
-                {
-                    "context_word": cw,
-                    "context_char": cc,
-                    "query_word": qw,
-                    "query_char": qc,
-                },
-            )
-            start = answer[0].item()
-            end = answer[1].item()
-            result_item = cw[start : end + 1]
-            result[target] = BidafModelRuntime._convert_result(result_item)
-
-        return result
-
-    def _process_queries(self) -> Dict[str, Tuple[np.ndarray, np.ndarray]]:
-        result = {}
-        for target in self.targets:
-            question = self.queries[target]
-            result[target] = BidafModelRuntime._preprocess(question)
-
-        return result
-
-    @staticmethod
-    def _convert_result(result_item: np.ndarray) -> str:
-        result = []
-        for item in result_item:
-            result.append(item[0])
-
-        return " ".join(result)
-
-    @staticmethod
-    def _preprocess(text: str) -> Tuple[np.ndarray, np.ndarray]:
-        tokens = word_tokenize(text)
-        # split into lower-case word tokens, in numpy array with shape of (seq, 1)
-        words = np.asarray([w.lower() for w in tokens]).reshape(-1, 1)
-        # split words into chars, in numpy array with shape of (seq, 1, 1, 16)
-        chars = [[c for c in t][:16] for t in tokens]
-        chars = [cs + [""] * (16 - len(cs)) for cs in chars]
-        chars = np.asarray(chars).reshape(-1, 1, 1, 16)
-        return words, chars
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+import os
+import sys
+import requests
+import shutil
+from typing import Dict, List, Tuple
+import nltk
+import numpy as np
+from nltk import word_tokenize
+from onnxruntime import InferenceSession
+
+# pylint:disable=line-too-long
+class BidafModelRuntime:
+    def __init__(self, targets: List[str], queries: Dict[str, str], model_dir: str):
+        self.queries = queries
+        self.targets = targets
+        bidaf_model = os.path.abspath(os.path.join(model_dir, "bidaf.onnx"))
+        print(f"Loading Inference session from {bidaf_model}..", file=sys.stderr)
+        self.session = InferenceSession(bidaf_model)
+        print(f"Inference session loaded..", file=sys.stderr)
+        self.processed_queries = self._process_queries()
+        print(f"Processed queries..", file=sys.stderr)
+
+    @staticmethod
+    def init_bidaf(bidaf_model_dir: str, download_ntlk_punkt: bool = False) -> bool:
+        if os.path.isdir(bidaf_model_dir):
+            print("bidaf model directory already present..", file=sys.stderr)
+        else:
+            print("Creating bidaf model directory..", file=sys.stderr)
+            os.makedirs(bidaf_model_dir, exist_ok=True)
+
+        # Download Punkt Sentence Tokenizer
+        if download_ntlk_punkt:
+            nltk.download("punkt", download_dir=bidaf_model_dir)
+            nltk.download("punkt")
+
+        # Download bidaf onnx model
+        onnx_model_file = os.path.abspath(os.path.join(bidaf_model_dir, "bidaf.onnx"))
+
+        print(f"Checking file {onnx_model_file}..", file=sys.stderr)
+        if os.path.isfile(onnx_model_file):
+            print("bidaf.onnx downloaded already!", file=sys.stderr)
+        else:
+            print("Downloading bidaf.onnx...", file=sys.stderr)
+            response = requests.get(
+                "https://onnxzoo.blob.core.windows.net/models/opset_9/bidaf/bidaf.onnx",
+                stream=True,
+            )
+            with open(onnx_model_file, "wb") as f:
+                response.raw.decode_content = True
+                shutil.copyfileobj(response.raw, f)
+        return True
+
+    def serve(self, context: str) -> Dict[str, str]:
+        result = {}
+        cw, cc = BidafModelRuntime._preprocess(context)
+        for target in self.targets:
+            qw, qc = self.processed_queries[target]
+            answer = self.session.run(
+                ["start_pos", "end_pos"],
+                {
+                    "context_word": cw,
+                    "context_char": cc,
+                    "query_word": qw,
+                    "query_char": qc,
+                },
+            )
+            start = answer[0].item()
+            end = answer[1].item()
+            result_item = cw[start : end + 1]
+            result[target] = BidafModelRuntime._convert_result(result_item)
+
+        return result
+
+    def _process_queries(self) -> Dict[str, Tuple[np.ndarray, np.ndarray]]:
+        result = {}
+        for target in self.targets:
+            question = self.queries[target]
+            result[target] = BidafModelRuntime._preprocess(question)
+
+        return result
+
+    @staticmethod
+    def _convert_result(result_item: np.ndarray) -> str:
+        result = []
+        for item in result_item:
+            result.append(item[0])
+
+        return " ".join(result)
+
+    @staticmethod
+    def _preprocess(text: str) -> Tuple[np.ndarray, np.ndarray]:
+        tokens = word_tokenize(text)
+        # split into lower-case word tokens, in numpy array with shape of (seq, 1)
+        words = np.asarray([w.lower() for w in tokens]).reshape(-1, 1)
+        # split words into chars, in numpy array with shape of (seq, 1, 1, 16)
+        chars = [[c for c in t][:16] for t in tokens]
+        chars = [cs + [""] * (16 - len(cs)) for cs in chars]
+        chars = np.asarray(chars).reshape(-1, 1, 1, 16)
+        return words, chars
diff --git a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bidaf/requirements.txt b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bidaf/requirements.txt
similarity index 88%
rename from samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bidaf/requirements.txt
rename to tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bidaf/requirements.txt
index a2eea036e..bb0cd1821 100644
--- a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/bidaf/requirements.txt
+++ b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/bidaf/requirements.txt
@@ -1,3 +1,3 @@
-nltk
-numpy
-onnxruntime
+nltk
+numpy
+onnxruntime
diff --git a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/booking_details.py b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/booking_details.py
similarity index 100%
rename from samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/booking_details.py
rename to tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/booking_details.py
diff --git a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/language_helper.py b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/language_helper.py
similarity index 97%
rename from samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/language_helper.py
rename to tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/language_helper.py
index bc75260f9..c98ae0d09 100644
--- a/samples/experimental/101.corebot-bert-bidaf/model/model_corebot101/language_helper.py
+++ b/tests/experimental/101.corebot-bert-bidaf/model/model_corebot101/language_helper.py
@@ -1,212 +1,212 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""Language helper that invokes the language model.
-This is used from the Bot and Model Runtime to load and invoke the language models.
-"""
-
-import os
-import sys
-from typing import Dict
-from pathlib import Path
-import requests
-from datatypes_date_time.timex import Timex
-from model_corebot101.booking_details import BookingDetails
-from model_corebot101.bidaf.model_runtime import BidafModelRuntime
-from model_corebot101.bert.model_runtime import BertModelRuntime
-from model_corebot101.bert.train import BertTrainEval
-
-# pylint:disable=line-too-long
-class LanguageHelper:
-    """Language helper that invokes the language model."""
-
-    home_dir = str(Path.home())
-    bert_model_dir_default = os.path.abspath(os.path.join(home_dir, "models/bert"))
-    bidaf_model_dir_default = os.path.abspath(os.path.join(home_dir, "models/bidaf"))
-
-    # pylint:disable=bad-continuation
-    def __init__(self):
-        """Create Language Helper.
-        Note: Creating the Bert/Bidaf Model Runtime is only necessary for in-proc usage.
-        """
-        self._bidaf_entities = None
-        self._bert_intents = None
-
-    @property
-    def entities(self) -> BidafModelRuntime:
-        """Model used to detect entities."""
-        return self._bidaf_entities
-
-    @property
-    def intents(self) -> BertModelRuntime:
-        """Model used to detect intents."""
-        return self._bert_intents
-
-    def initialize_models(
-        self,
-        bert_model_dir: str = bert_model_dir_default,
-        bidaf_model_dir: str = bidaf_model_dir_default,
-    ) -> bool:
-        """ Initialize models.
-        Perform initialization of the models.
-        """
-        if not BidafModelRuntime.init_bidaf(bidaf_model_dir, download_ntlk_punkt=True):
-            print(
-                f"bidaf model creation failed at model directory {bidaf_model_dir}..",
-                file=sys.stderr,
-            )
-            return False
-
-        if not BertModelRuntime.init_bert(bert_model_dir):
-            print(
-                "bert model creation failed at model directory {bert_model_dir}..",
-                file=sys.stderr,
-            )
-            return False
-
-        print(f"Loading BERT model from {bert_model_dir}...", file=sys.stderr)
-        if not os.listdir(bert_model_dir):
-            print(f"No BERT model present, building model..", file=sys.stderr)
-            BertTrainEval.train_eval(cleanup_output_dir=True)
-
-        self._bert_intents = BertModelRuntime(
-            model_dir=bert_model_dir, label_list=["Book flight", "Cancel"]
-        )
-        print(f"Loaded BERT model.  Loading BiDaf model..", file=sys.stderr)
-
-        self._bidaf_entities = BidafModelRuntime(
-            targets=["from", "to", "date"],
-            queries={
-                "from": "which city will you travel from?",
-                "to": "which city will you travel to?",
-                "date": "which date will you travel?",
-            },
-            model_dir=bidaf_model_dir,
-        )
-        print(f"Loaded BiDAF model from {bidaf_model_dir}.", file=sys.stderr)
-
-        return True
-
-    async def excecute_query_inproc(self, utterance: str) -> BookingDetails:
-        """Exeecute a query against language model."""
-        booking_details = BookingDetails()
-        intent = self.intents.serve(utterance)
-        print(f'Recognized intent "{intent}" from "{utterance}".', file=sys.stderr)
-        if intent == "Book flight":
-            # Bert gave us the intent.
-            # Now look for entities with BiDAF..
-            entities = self.entities.serve(utterance)
-
-            if "to" in entities:
-                print(f'   Recognized "to" entitiy: {entities["to"]}.', file=sys.stderr)
-                booking_details.destination = entities["to"]
-            if "from" in entities:
-                print(
-                    f'   Recognized "from" entitiy: {entities["from"]}.',
-                    file=sys.stderr,
-                )
-                booking_details.origin = entities["from"]
-            if "date" in entities:
-                # This value will be a TIMEX. And we are only interested in a Date so grab the first result and drop the Time part.
-                # TIMEX is a format that represents DateTime expressions that include some ambiguity. e.g. missing a Year.
-                print(
-                    f'   Recognized "date" entitiy: {entities["date"]}.',
-                    file=sys.stderr,
-                )
-                travel_date = entities["date"]
-                if await LanguageHelper.validate_timex(travel_date):
-                    booking_details.travel_date = travel_date
-
-        return booking_details
-
-    @staticmethod
-    async def excecute_query_service(
-        configuration: dict, utterance: str
-    ) -> BookingDetails:
-        """Invoke lu service to perform prediction/evaluation of utterance."""
-        booking_details = BookingDetails()
-        lu_response = await LanguageHelper.call_model_runtime(configuration, utterance)
-        if lu_response.status_code == 200:
-
-            response_json = lu_response.json()
-            intent = response_json["intent"] if "intent" in response_json else None
-            entities = await LanguageHelper.validate_entities(
-                response_json["entities"] if "entities" in response_json else None
-            )
-            if intent:
-                if "to" in entities:
-                    print(
-                        f'   Recognized "to" entity: {entities["to"]}.', file=sys.stderr
-                    )
-                    booking_details.destination = entities["to"]
-                if "from" in entities:
-                    print(
-                        f'   Recognized "from" entity: {entities["from"]}.',
-                        file=sys.stderr,
-                    )
-                    booking_details.origin = entities["from"]
-                if "date" in entities:
-                    # This value will be a TIMEX. And we are only interested in a Date so grab the first result and drop the Time part.
-                    # TIMEX is a format that represents DateTime expressions that include some ambiguity. e.g. missing a Year.
-                    print(
-                        f'   Recognized "date" entity: {entities["date"]}.',
-                        file=sys.stderr,
-                    )
-                    travel_date = entities["date"]
-                    if await LanguageHelper.validate_timex(travel_date):
-                        booking_details.travel_date = travel_date
-        return booking_details
-
-    @staticmethod
-    async def call_model_runtime(
-        configuration: Dict[str, object], text: str
-    ) -> requests.Request:
-        """ Makes a call to the model runtime api
-
-        The model runtime api signature is:
-          http://:/v1.0/model?q=
-
-        where:
-
-          model_runtime_host - The host running the model runtime api.  To resolve
-            the host running the model runtime api (in the following order):
-            - MODEL_RUNTIME_API environment variable.  Used in docker.
-            - config.py (which contains the DefaultConfig class).  Used running
-                locally.
-
-          port - http port number (ie, 8880)
-
-          q - A query string to process (ie, the text utterance from user)
-
-        For more details: (See TBD swagger file)
-        """
-        port = os.environ.get("MODEL_RUNTIME_SERVICE_PORT")
-        host = os.environ.get("MODEL_RUNTIME_SERVICE_HOST")
-        if host is None:
-            host = configuration["MODEL_RUNTIME_SERVICE_HOST"]
-        if port is None:
-            port = configuration["MODEL_RUNTIME_SERVICE_PORT"]
-
-        api_url = f"http://{host}:{port}/v1.0/model"
-        qstrings = {"q": text}
-        return requests.get(api_url, params=qstrings)
-
-    @staticmethod
-    async def validate_entities(entities: Dict[str, str]) -> bool:
-        """Validate the entities.
-        The to and from cities can't be the same.  If this is detected,
-        remove the ambiguous results. """
-        if "to" in entities and "from" in entities:
-            if entities["to"] == entities["from"]:
-                del entities["to"]
-                del entities["from"]
-        return entities
-
-    @staticmethod
-    async def validate_timex(travel_date: str) -> bool:
-        """Validate the time.
-        Make sure time given in the right format. """
-        # uncomment the following line for debugging.
-        # import pdb; pdb.set_trace()
-        timex_property = Timex(travel_date)
-
-        return len(timex_property.types) > 0 and "definite" not in timex_property.types
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+"""Language helper that invokes the language model.
+This is used from the Bot and Model Runtime to load and invoke the language models.
+"""
+
+import os
+import sys
+from typing import Dict
+from pathlib import Path
+import requests
+from datatypes_date_time.timex import Timex
+from model_corebot101.booking_details import BookingDetails
+from model_corebot101.bidaf.model_runtime import BidafModelRuntime
+from model_corebot101.bert.model_runtime import BertModelRuntime
+from model_corebot101.bert.train import BertTrainEval
+
+# pylint:disable=line-too-long
+class LanguageHelper:
+    """Language helper that invokes the language model."""
+
+    home_dir = str(Path.home())
+    bert_model_dir_default = os.path.abspath(os.path.join(home_dir, "models/bert"))
+    bidaf_model_dir_default = os.path.abspath(os.path.join(home_dir, "models/bidaf"))
+
+    # pylint:disable=bad-continuation
+    def __init__(self):
+        """Create Language Helper.
+        Note: Creating the Bert/Bidaf Model Runtime is only necessary for in-proc usage.
+        """
+        self._bidaf_entities = None
+        self._bert_intents = None
+
+    @property
+    def entities(self) -> BidafModelRuntime:
+        """Model used to detect entities."""
+        return self._bidaf_entities
+
+    @property
+    def intents(self) -> BertModelRuntime:
+        """Model used to detect intents."""
+        return self._bert_intents
+
+    def initialize_models(
+        self,
+        bert_model_dir: str = bert_model_dir_default,
+        bidaf_model_dir: str = bidaf_model_dir_default,
+    ) -> bool:
+        """ Initialize models.
+        Perform initialization of the models.
+        """
+        if not BidafModelRuntime.init_bidaf(bidaf_model_dir, download_ntlk_punkt=True):
+            print(
+                f"bidaf model creation failed at model directory {bidaf_model_dir}..",
+                file=sys.stderr,
+            )
+            return False
+
+        if not BertModelRuntime.init_bert(bert_model_dir):
+            print(
+                "bert model creation failed at model directory {bert_model_dir}..",
+                file=sys.stderr,
+            )
+            return False
+
+        print(f"Loading BERT model from {bert_model_dir}...", file=sys.stderr)
+        if not os.listdir(bert_model_dir):
+            print(f"No BERT model present, building model..", file=sys.stderr)
+            BertTrainEval.train_eval(cleanup_output_dir=True)
+
+        self._bert_intents = BertModelRuntime(
+            model_dir=bert_model_dir, label_list=["Book flight", "Cancel"]
+        )
+        print(f"Loaded BERT model.  Loading BiDaf model..", file=sys.stderr)
+
+        self._bidaf_entities = BidafModelRuntime(
+            targets=["from", "to", "date"],
+            queries={
+                "from": "which city will you travel from?",
+                "to": "which city will you travel to?",
+                "date": "which date will you travel?",
+            },
+            model_dir=bidaf_model_dir,
+        )
+        print(f"Loaded BiDAF model from {bidaf_model_dir}.", file=sys.stderr)
+
+        return True
+
+    async def excecute_query_inproc(self, utterance: str) -> BookingDetails:
+        """Exeecute a query against language model."""
+        booking_details = BookingDetails()
+        intent = self.intents.serve(utterance)
+        print(f'Recognized intent "{intent}" from "{utterance}".', file=sys.stderr)
+        if intent == "Book flight":
+            # Bert gave us the intent.
+            # Now look for entities with BiDAF..
+            entities = self.entities.serve(utterance)
+
+            if "to" in entities:
+                print(f'   Recognized "to" entitiy: {entities["to"]}.', file=sys.stderr)
+                booking_details.destination = entities["to"]
+            if "from" in entities:
+                print(
+                    f'   Recognized "from" entitiy: {entities["from"]}.',
+                    file=sys.stderr,
+                )
+                booking_details.origin = entities["from"]
+            if "date" in entities:
+                # This value will be a TIMEX. And we are only interested in a Date so grab the first result and drop the Time part.
+                # TIMEX is a format that represents DateTime expressions that include some ambiguity. e.g. missing a Year.
+                print(
+                    f'   Recognized "date" entitiy: {entities["date"]}.',
+                    file=sys.stderr,
+                )
+                travel_date = entities["date"]
+                if await LanguageHelper.validate_timex(travel_date):
+                    booking_details.travel_date = travel_date
+
+        return booking_details
+
+    @staticmethod
+    async def excecute_query_service(
+        configuration: dict, utterance: str
+    ) -> BookingDetails:
+        """Invoke lu service to perform prediction/evaluation of utterance."""
+        booking_details = BookingDetails()
+        lu_response = await LanguageHelper.call_model_runtime(configuration, utterance)
+        if lu_response.status_code == 200:
+
+            response_json = lu_response.json()
+            intent = response_json["intent"] if "intent" in response_json else None
+            entities = await LanguageHelper.validate_entities(
+                response_json["entities"] if "entities" in response_json else None
+            )
+            if intent:
+                if "to" in entities:
+                    print(
+                        f'   Recognized "to" entity: {entities["to"]}.', file=sys.stderr
+                    )
+                    booking_details.destination = entities["to"]
+                if "from" in entities:
+                    print(
+                        f'   Recognized "from" entity: {entities["from"]}.',
+                        file=sys.stderr,
+                    )
+                    booking_details.origin = entities["from"]
+                if "date" in entities:
+                    # This value will be a TIMEX. And we are only interested in a Date so grab the first result and drop the Time part.
+                    # TIMEX is a format that represents DateTime expressions that include some ambiguity. e.g. missing a Year.
+                    print(
+                        f'   Recognized "date" entity: {entities["date"]}.',
+                        file=sys.stderr,
+                    )
+                    travel_date = entities["date"]
+                    if await LanguageHelper.validate_timex(travel_date):
+                        booking_details.travel_date = travel_date
+        return booking_details
+
+    @staticmethod
+    async def call_model_runtime(
+        configuration: Dict[str, object], text: str
+    ) -> requests.Request:
+        """ Makes a call to the model runtime api
+
+        The model runtime api signature is:
+          http://:/v1.0/model?q=
+
+        where:
+
+          model_runtime_host - The host running the model runtime api.  To resolve
+            the host running the model runtime api (in the following order):
+            - MODEL_RUNTIME_API environment variable.  Used in docker.
+            - config.py (which contains the DefaultConfig class).  Used running
+                locally.
+
+          port - http port number (ie, 8880)
+
+          q - A query string to process (ie, the text utterance from user)
+
+        For more details: (See TBD swagger file)
+        """
+        port = os.environ.get("MODEL_RUNTIME_SERVICE_PORT")
+        host = os.environ.get("MODEL_RUNTIME_SERVICE_HOST")
+        if host is None:
+            host = configuration["MODEL_RUNTIME_SERVICE_HOST"]
+        if port is None:
+            port = configuration["MODEL_RUNTIME_SERVICE_PORT"]
+
+        api_url = f"http://{host}:{port}/v1.0/model"
+        qstrings = {"q": text}
+        return requests.get(api_url, params=qstrings)
+
+    @staticmethod
+    async def validate_entities(entities: Dict[str, str]) -> bool:
+        """Validate the entities.
+        The to and from cities can't be the same.  If this is detected,
+        remove the ambiguous results. """
+        if "to" in entities and "from" in entities:
+            if entities["to"] == entities["from"]:
+                del entities["to"]
+                del entities["from"]
+        return entities
+
+    @staticmethod
+    async def validate_timex(travel_date: str) -> bool:
+        """Validate the time.
+        Make sure time given in the right format. """
+        # uncomment the following line for debugging.
+        # import pdb; pdb.set_trace()
+        timex_property = Timex(travel_date)
+
+        return len(timex_property.types) > 0 and "definite" not in timex_property.types
diff --git a/samples/experimental/101.corebot-bert-bidaf/model/setup.py b/tests/experimental/101.corebot-bert-bidaf/model/setup.py
similarity index 96%
rename from samples/experimental/101.corebot-bert-bidaf/model/setup.py
rename to tests/experimental/101.corebot-bert-bidaf/model/setup.py
index e10cc4872..86a7180b7 100644
--- a/samples/experimental/101.corebot-bert-bidaf/model/setup.py
+++ b/tests/experimental/101.corebot-bert-bidaf/model/setup.py
@@ -1,51 +1,51 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import os
-from setuptools import setup
-
-REQUIRES = [
-    "torch",
-    "tqdm",
-    "pytorch-pretrained-bert",
-    "onnxruntime>=0.4.0",
-    "onnx>=1.5.0",
-    "datatypes-date-time>=1.0.0.a1",
-    "nltk>=3.4.1",
-]
-
-
-root = os.path.abspath(os.path.dirname(__file__))
-
-with open(os.path.join(root, "model_corebot101", "about.py")) as f:
-    package_info = {}
-    info = f.read()
-    exec(info, package_info)
-
-setup(
-    name=package_info["__title__"],
-    version=package_info["__version__"],
-    url=package_info["__uri__"],
-    author=package_info["__author__"],
-    description=package_info["__description__"],
-    keywords="botframework azure botbuilder",
-    long_description=package_info["__summary__"],
-    license=package_info["__license__"],
-    packages=[
-        "model_corebot101.bert.train",
-        "model_corebot101.bert.common",
-        "model_corebot101.bert.model_runtime",
-        "model_corebot101.bidaf.model_runtime",
-    ],
-    install_requires=REQUIRES,
-    dependency_links=["https://github.com/pytorch/pytorch"],
-    include_package_data=True,
-    classifiers=[
-        "Programming Language :: Python :: 3.6",
-        "Intended Audience :: Developers",
-        "License :: OSI Approved :: MIT License",
-        "Operating System :: OS Independent",
-        "Development Status :: 3 - Alpha",
-        "Topic :: Scientific/Engineering :: Artificial Intelligence",
-    ],
-)
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+from setuptools import setup
+
+REQUIRES = [
+    "torch",
+    "tqdm",
+    "pytorch-pretrained-bert",
+    "onnxruntime>=0.4.0",
+    "onnx>=1.5.0",
+    "datatypes-date-time>=1.0.0.a1",
+    "nltk>=3.4.1",
+]
+
+
+root = os.path.abspath(os.path.dirname(__file__))
+
+with open(os.path.join(root, "model_corebot101", "about.py")) as f:
+    package_info = {}
+    info = f.read()
+    exec(info, package_info)
+
+setup(
+    name=package_info["__title__"],
+    version=package_info["__version__"],
+    url=package_info["__uri__"],
+    author=package_info["__author__"],
+    description=package_info["__description__"],
+    keywords="botframework azure botbuilder",
+    long_description=package_info["__summary__"],
+    license=package_info["__license__"],
+    packages=[
+        "model_corebot101.bert.train",
+        "model_corebot101.bert.common",
+        "model_corebot101.bert.model_runtime",
+        "model_corebot101.bidaf.model_runtime",
+    ],
+    install_requires=REQUIRES,
+    dependency_links=["https://github.com/pytorch/pytorch"],
+    include_package_data=True,
+    classifiers=[
+        "Programming Language :: Python :: 3.6",
+        "Intended Audience :: Developers",
+        "License :: OSI Approved :: MIT License",
+        "Operating System :: OS Independent",
+        "Development Status :: 3 - Alpha",
+        "Topic :: Scientific/Engineering :: Artificial Intelligence",
+    ],
+)
diff --git a/samples/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/__init__.py b/tests/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/__init__.py
similarity index 96%
rename from samples/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/__init__.py
rename to tests/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/__init__.py
index d3a549063..c702f213e 100644
--- a/samples/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/__init__.py
+++ b/tests/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/__init__.py
@@ -1,6 +1,6 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""Model Runtime."""
-from .model_cache import ModelCache
-
-__all__ = ["ModelCache"]
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+"""Model Runtime."""
+from .model_cache import ModelCache
+
+__all__ = ["ModelCache"]
diff --git a/samples/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/about.py b/tests/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/about.py
similarity index 97%
rename from samples/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/about.py
rename to tests/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/about.py
index 7fb9c163e..ce4ebf0e1 100644
--- a/samples/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/about.py
+++ b/tests/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/about.py
@@ -1,14 +1,14 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import os
-
-__title__ = "model_runtime_svc_corebot101"
-__version__ = (
-    os.environ["packageVersion"] if "packageVersion" in os.environ else "0.0.1"
-)
-__uri__ = "https://www.github.com/Microsoft/botbuilder-python"
-__author__ = "Microsoft"
-__description__ = "Microsoft Bot Framework Bot Builder"
-__summary__ = "Microsoft Bot Framework Bot Builder SDK for Python."
-__license__ = "MIT"
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+
+__title__ = "model_runtime_svc_corebot101"
+__version__ = (
+    os.environ["packageVersion"] if "packageVersion" in os.environ else "0.0.1"
+)
+__uri__ = "https://www.github.com/Microsoft/botbuilder-python"
+__author__ = "Microsoft"
+__description__ = "Microsoft Bot Framework Bot Builder"
+__summary__ = "Microsoft Bot Framework Bot Builder SDK for Python."
+__license__ = "MIT"
diff --git a/samples/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/docker_init.py b/tests/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/docker_init.py
similarity index 97%
rename from samples/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/docker_init.py
rename to tests/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/docker_init.py
index 1e285cc3e..c59fde586 100644
--- a/samples/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/docker_init.py
+++ b/tests/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/docker_init.py
@@ -1,19 +1,19 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""Docker initialization.
-This is called from the Dockerfile when creating the model runtime service API
-container.
-"""
-import os
-from pathlib import Path
-from model_corebot101.language_helper import LanguageHelper
-
-# Initialize the models
-LH = LanguageHelper()
-HOME_DIR = str(Path.home())
-BERT_MODEL_DIR_DEFAULT = os.path.abspath(os.path.join(HOME_DIR, "models/bert"))
-BIDAF_MODEL_DIR_DEFAULT = os.path.abspath(os.path.join(HOME_DIR, "models/bidaf"))
-
-LH.initialize_models(
-    bert_model_dir=BERT_MODEL_DIR_DEFAULT, bidaf_model_dir=BIDAF_MODEL_DIR_DEFAULT
-)
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+"""Docker initialization.
+This is called from the Dockerfile when creating the model runtime service API
+container.
+"""
+import os
+from pathlib import Path
+from model_corebot101.language_helper import LanguageHelper
+
+# Initialize the models
+LH = LanguageHelper()
+HOME_DIR = str(Path.home())
+BERT_MODEL_DIR_DEFAULT = os.path.abspath(os.path.join(HOME_DIR, "models/bert"))
+BIDAF_MODEL_DIR_DEFAULT = os.path.abspath(os.path.join(HOME_DIR, "models/bidaf"))
+
+LH.initialize_models(
+    bert_model_dir=BERT_MODEL_DIR_DEFAULT, bidaf_model_dir=BIDAF_MODEL_DIR_DEFAULT
+)
diff --git a/samples/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/handlers/__init__.py b/tests/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/handlers/__init__.py
similarity index 97%
rename from samples/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/handlers/__init__.py
rename to tests/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/handlers/__init__.py
index 5b7f7a925..d7fe4b228 100644
--- a/samples/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/handlers/__init__.py
+++ b/tests/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/handlers/__init__.py
@@ -1,2 +1,2 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
diff --git a/samples/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/handlers/model_handler.py b/tests/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/handlers/model_handler.py
similarity index 97%
rename from samples/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/handlers/model_handler.py
rename to tests/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/handlers/model_handler.py
index 32a3ff1bb..ef6e78a86 100644
--- a/samples/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/handlers/model_handler.py
+++ b/tests/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/handlers/model_handler.py
@@ -1,52 +1,52 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""Tornado handler to access the model runtime.
-
-To invoke:
-  /v1.0/model?q=
-"""
-
-import logging
-import json
-from tornado.web import RequestHandler
-from model_corebot101.language_helper import LanguageHelper
-
-# pylint:disable=abstract-method
-class ModelHandler(RequestHandler):
-    """Model Handler implementation to access the model runtime."""
-
-    _handler_routes = ["/v1.0/model/$", "/v1.0/model$"]
-
-    @classmethod
-    def build_config(cls, ref_obj: dict):
-        """Build the Tornado configuration for this handler."""
-        return [(route, ModelHandler, ref_obj) for route in cls._handler_routes]
-
-    def set_default_headers(self):
-        """Set the default HTTP headers."""
-        RequestHandler.set_default_headers(self)
-        self.set_header("Content-Type", "application/json")
-        self.set_header("Access-Control-Allow-Origin", "*")
-        self.set_header("Access-Control-Allow-Headers", "Origin, Content-Type, Accept")
-        self.set_header("Access-Control-Allow-Methods", "OPTIONS, GET")
-
-    # pylint:disable=attribute-defined-outside-init
-    def initialize(self, language_helper: LanguageHelper):
-        """Initialize the handler."""
-        RequestHandler.initialize(self)
-        self._language_helper = language_helper
-        self._logger = logging.getLogger("MODEL_HANDLER")
-
-    async def get(self):
-        """Handle HTTP GET request."""
-        text = self.get_argument("q", None, True)
-        if not text:
-            return (404, "Missing the q query string with the text")
-
-        response = {}
-        intent = self._language_helper.intents.serve(text)
-        response["intent"] = intent if intent else "None"
-        entities = self._language_helper.entities.serve(text)
-        response["entities"] = entities if entities else "None"
-        self.write(json.dumps(response))
-        return (200, "Complete")
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+"""Tornado handler to access the model runtime.
+
+To invoke:
+  /v1.0/model?q=
+"""
+
+import logging
+import json
+from tornado.web import RequestHandler
+from model_corebot101.language_helper import LanguageHelper
+
+# pylint:disable=abstract-method
+class ModelHandler(RequestHandler):
+    """Model Handler implementation to access the model runtime."""
+
+    _handler_routes = ["/v1.0/model/$", "/v1.0/model$"]
+
+    @classmethod
+    def build_config(cls, ref_obj: dict):
+        """Build the Tornado configuration for this handler."""
+        return [(route, ModelHandler, ref_obj) for route in cls._handler_routes]
+
+    def set_default_headers(self):
+        """Set the default HTTP headers."""
+        RequestHandler.set_default_headers(self)
+        self.set_header("Content-Type", "application/json")
+        self.set_header("Access-Control-Allow-Origin", "*")
+        self.set_header("Access-Control-Allow-Headers", "Origin, Content-Type, Accept")
+        self.set_header("Access-Control-Allow-Methods", "OPTIONS, GET")
+
+    # pylint:disable=attribute-defined-outside-init
+    def initialize(self, language_helper: LanguageHelper):
+        """Initialize the handler."""
+        RequestHandler.initialize(self)
+        self._language_helper = language_helper
+        self._logger = logging.getLogger("MODEL_HANDLER")
+
+    async def get(self):
+        """Handle HTTP GET request."""
+        text = self.get_argument("q", None, True)
+        if not text:
+            return (404, "Missing the q query string with the text")
+
+        response = {}
+        intent = self._language_helper.intents.serve(text)
+        response["intent"] = intent if intent else "None"
+        entities = self._language_helper.entities.serve(text)
+        response["entities"] = entities if entities else "None"
+        self.write(json.dumps(response))
+        return (200, "Complete")
diff --git a/samples/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/main.py b/tests/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/main.py
similarity index 96%
rename from samples/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/main.py
rename to tests/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/main.py
index 6d622f182..a8f6ba5ca 100644
--- a/samples/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/main.py
+++ b/tests/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/main.py
@@ -1,109 +1,109 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""Model Runtime.
-Entry point for the model runtime.
-"""
-import os
-import signal
-import logging
-from logging.handlers import RotatingFileHandler
-import tornado
-from tornado.options import define, options
-from pathlib import Path
-from model_corebot101.language_helper import LanguageHelper
-from handlers.model_handler import ModelHandler
-
-HOME_DIR = str(Path.home())
-
-# Define Tornado options
-define("port", default=8880, help="HTTP port for model runtime to listen on", type=int)
-define(
-    "bidaf_model_dir",
-    default=os.path.abspath(os.path.join(HOME_DIR, "models/bidaf")),
-    help="bidaf model directory",
-)
-define(
-    "bert_model_dir",
-    default=os.path.abspath(os.path.join(HOME_DIR, "models/bert")),
-    help="bert model directory",
-)
-
-
-def setup_logging():
-    """Set up logging."""
-    logging.info("Setting up logging infrastructure")
-
-    # Create the rotating log handler
-    if not os.path.exists("logs"):
-        os.mkdir("logs")
-    handler = RotatingFileHandler(
-        os.path.join("./logs", "model-runtime.log"),
-        maxBytes=5 * 1024 ** 2,  # 5 MB chunks,
-        backupCount=5,  # limit to 25 MB logs max
-    )
-
-    # Set the formatter
-    handler.setFormatter(
-        logging.Formatter("%(asctime)s %(name)-12s %(levelname)-8s %(message)s")
-    )
-
-    # Setup the root logging with the necessary handlers
-    log = logging.getLogger()
-    log.addHandler(handler)
-
-    # Set to info for normal processing
-    log.setLevel(logging.INFO)
-
-
-# pylint:disable=unused-argument
-def signal_handler(sig_num, frame):
-    """Stop activity on signal."""
-    tornado.ioloop.IOLoop.instance().stop()
-
-
-def run():
-    """Main entry point for model runtime api."""
-
-    # Register signal handlers.
-    logging.info("Preparing signal handlers..")
-    signal.signal(signal.SIGINT, signal_handler)
-    signal.signal(signal.SIGTERM, signal_handler)
-
-    # Set up model cache.
-    # If containerizing, suggest initializing the directories (and associated
-    # file downloads) be performed during container build time.
-    logging.info("Initializing model directories:")
-    logging.info("    bert  : %s", options.bert_model_dir)
-    logging.info("    bidaf : %s", options.bidaf_model_dir)
-
-    language_helper = LanguageHelper()
-    if (
-        language_helper.initialize_models(
-            options.bert_model_dir, options.bidaf_model_dir
-        )
-        is False
-    ):
-        logging.error("Could not initilize model directories.  Exiting..")
-        return
-
-    # Build the configuration
-    logging.info("Building config..")
-    ref_obj = {"language_helper": language_helper}
-    app_config = ModelHandler.build_config(ref_obj)
-
-    logging.info("Starting Tornado model runtime service..")
-    application = tornado.web.Application(app_config)
-    application.listen(options.port)
-
-    # Protect the loop with a try/catch
-    try:
-        # Start the app and wait for a close
-        tornado.ioloop.IOLoop.instance().start()
-    finally:
-        # handle error with shutting down loop
-        tornado.ioloop.IOLoop.instance().stop()
-
-
-if __name__ == "__main__":
-    setup_logging()
-    run()
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+"""Model Runtime.
+Entry point for the model runtime.
+"""
+import os
+import signal
+import logging
+from logging.handlers import RotatingFileHandler
+import tornado
+from tornado.options import define, options
+from pathlib import Path
+from model_corebot101.language_helper import LanguageHelper
+from handlers.model_handler import ModelHandler
+
+HOME_DIR = str(Path.home())
+
+# Define Tornado options
+define("port", default=8880, help="HTTP port for model runtime to listen on", type=int)
+define(
+    "bidaf_model_dir",
+    default=os.path.abspath(os.path.join(HOME_DIR, "models/bidaf")),
+    help="bidaf model directory",
+)
+define(
+    "bert_model_dir",
+    default=os.path.abspath(os.path.join(HOME_DIR, "models/bert")),
+    help="bert model directory",
+)
+
+
+def setup_logging():
+    """Set up logging."""
+    logging.info("Setting up logging infrastructure")
+
+    # Create the rotating log handler
+    if not os.path.exists("logs"):
+        os.mkdir("logs")
+    handler = RotatingFileHandler(
+        os.path.join("./logs", "model-runtime.log"),
+        maxBytes=5 * 1024 ** 2,  # 5 MB chunks,
+        backupCount=5,  # limit to 25 MB logs max
+    )
+
+    # Set the formatter
+    handler.setFormatter(
+        logging.Formatter("%(asctime)s %(name)-12s %(levelname)-8s %(message)s")
+    )
+
+    # Setup the root logging with the necessary handlers
+    log = logging.getLogger()
+    log.addHandler(handler)
+
+    # Set to info for normal processing
+    log.setLevel(logging.INFO)
+
+
+# pylint:disable=unused-argument
+def signal_handler(sig_num, frame):
+    """Stop activity on signal."""
+    tornado.ioloop.IOLoop.instance().stop()
+
+
+def run():
+    """Main entry point for model runtime api."""
+
+    # Register signal handlers.
+    logging.info("Preparing signal handlers..")
+    signal.signal(signal.SIGINT, signal_handler)
+    signal.signal(signal.SIGTERM, signal_handler)
+
+    # Set up model cache.
+    # If containerizing, suggest initializing the directories (and associated
+    # file downloads) be performed during container build time.
+    logging.info("Initializing model directories:")
+    logging.info("    bert  : %s", options.bert_model_dir)
+    logging.info("    bidaf : %s", options.bidaf_model_dir)
+
+    language_helper = LanguageHelper()
+    if (
+        language_helper.initialize_models(
+            options.bert_model_dir, options.bidaf_model_dir
+        )
+        is False
+    ):
+        logging.error("Could not initilize model directories.  Exiting..")
+        return
+
+    # Build the configuration
+    logging.info("Building config..")
+    ref_obj = {"language_helper": language_helper}
+    app_config = ModelHandler.build_config(ref_obj)
+
+    logging.info("Starting Tornado model runtime service..")
+    application = tornado.web.Application(app_config)
+    application.listen(options.port)
+
+    # Protect the loop with a try/catch
+    try:
+        # Start the app and wait for a close
+        tornado.ioloop.IOLoop.instance().start()
+    finally:
+        # handle error with shutting down loop
+        tornado.ioloop.IOLoop.instance().stop()
+
+
+if __name__ == "__main__":
+    setup_logging()
+    run()
diff --git a/samples/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/model_cache.py b/tests/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/model_cache.py
similarity index 97%
rename from samples/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/model_cache.py
rename to tests/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/model_cache.py
index 2c9d61c87..4b989a821 100644
--- a/samples/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/model_cache.py
+++ b/tests/experimental/101.corebot-bert-bidaf/model_runtime_svc/model_runtime_svc_corebot101/model_cache.py
@@ -1,67 +1,67 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""Model Cache.
-Simple container for bidaf/bert models.
-"""
-import os
-import logging
-
-from model_corebot101.bidaf.model_runtime import BidafModelRuntime
-from model_corebot101.bert.model_runtime import BertModelRuntime
-
-# pylint:disable=line-too-long,bad-continuation
-class DeprecateModelCache(object):
-    """Model Cache implementation."""
-
-    def __init__(self):
-        self._logger = logging.getLogger("ModelCache")
-        self._bert_model_dir = None
-        self._bidaf_model_dir = None
-        self._bert_intents = None
-        self._bidaf_entities = None
-
-    def init_model_dir(self, bidaf_model_dir: str, bert_model_dir: str) -> bool:
-        """ Initialize models """
-        if not os.path.exists(bidaf_model_dir):
-            # BiDAF needs no training, just download
-            if not BidafModelRuntime.init_bidaf(bidaf_model_dir, True):
-                self._logger.error(
-                    "bidaf model creation failed at model directory %s..",
-                    bidaf_model_dir,
-                )
-                return False
-
-        if not os.path.exists(bert_model_dir):
-            self._logger.error(
-                'BERT model directory does not exist "%s"', bert_model_dir
-            )
-            return False
-
-        self._bert_model_dir = os.path.normpath(bert_model_dir)
-        self._bidaf_model_dir = os.path.normpath(bidaf_model_dir)
-
-        self._bert_intents = BertModelRuntime(
-            model_dir=self._bert_model_dir, label_list=["Book flight", "Cancel"]
-        )
-        self._bidaf_entities = BidafModelRuntime(
-            targets=["from", "to", "date"],
-            queries={
-                "from": "which city will you travel from?",
-                "to": "which city will you travel to?",
-                "date": "which date will you travel?",
-            },
-            model_dir=self._bidaf_model_dir,
-        )
-        self._logger.info("bidaf entities model created : %s..", self._bidaf_model_dir)
-
-        return True
-
-    @property
-    def entities(self):
-        """Get the model that detect entities: bidaf."""
-        return self._bidaf_entities
-
-    @property
-    def intents(self):
-        """Get the model that detect intents: bert."""
-        return self._bert_intents
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+"""Model Cache.
+Simple container for bidaf/bert models.
+"""
+import os
+import logging
+
+from model_corebot101.bidaf.model_runtime import BidafModelRuntime
+from model_corebot101.bert.model_runtime import BertModelRuntime
+
+# pylint:disable=line-too-long,bad-continuation
+class DeprecateModelCache(object):
+    """Model Cache implementation."""
+
+    def __init__(self):
+        self._logger = logging.getLogger("ModelCache")
+        self._bert_model_dir = None
+        self._bidaf_model_dir = None
+        self._bert_intents = None
+        self._bidaf_entities = None
+
+    def init_model_dir(self, bidaf_model_dir: str, bert_model_dir: str) -> bool:
+        """ Initialize models """
+        if not os.path.exists(bidaf_model_dir):
+            # BiDAF needs no training, just download
+            if not BidafModelRuntime.init_bidaf(bidaf_model_dir, True):
+                self._logger.error(
+                    "bidaf model creation failed at model directory %s..",
+                    bidaf_model_dir,
+                )
+                return False
+
+        if not os.path.exists(bert_model_dir):
+            self._logger.error(
+                'BERT model directory does not exist "%s"', bert_model_dir
+            )
+            return False
+
+        self._bert_model_dir = os.path.normpath(bert_model_dir)
+        self._bidaf_model_dir = os.path.normpath(bidaf_model_dir)
+
+        self._bert_intents = BertModelRuntime(
+            model_dir=self._bert_model_dir, label_list=["Book flight", "Cancel"]
+        )
+        self._bidaf_entities = BidafModelRuntime(
+            targets=["from", "to", "date"],
+            queries={
+                "from": "which city will you travel from?",
+                "to": "which city will you travel to?",
+                "date": "which date will you travel?",
+            },
+            model_dir=self._bidaf_model_dir,
+        )
+        self._logger.info("bidaf entities model created : %s..", self._bidaf_model_dir)
+
+        return True
+
+    @property
+    def entities(self):
+        """Get the model that detect entities: bidaf."""
+        return self._bidaf_entities
+
+    @property
+    def intents(self):
+        """Get the model that detect intents: bert."""
+        return self._bert_intents
diff --git a/samples/experimental/101.corebot-bert-bidaf/model_runtime_svc/setup.py b/tests/experimental/101.corebot-bert-bidaf/model_runtime_svc/setup.py
similarity index 96%
rename from samples/experimental/101.corebot-bert-bidaf/model_runtime_svc/setup.py
rename to tests/experimental/101.corebot-bert-bidaf/model_runtime_svc/setup.py
index 35494dacc..95958734c 100644
--- a/samples/experimental/101.corebot-bert-bidaf/model_runtime_svc/setup.py
+++ b/tests/experimental/101.corebot-bert-bidaf/model_runtime_svc/setup.py
@@ -1,42 +1,42 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import os
-from setuptools import setup
-
-REQUIRES = [
-    "scikit-learn>=0.21.2",
-    "scipy>=1.3.0",
-    "tornado>=6.0.2",
-    "model_corebot101>=0.0.1",
-]
-
-root = os.path.abspath(os.path.dirname(__file__))
-
-with open(os.path.join(root, "model_runtime_svc_corebot101", "about.py")) as f:
-    package_info = {}
-    info = f.read()
-    exec(info, package_info)
-
-setup(
-    name=package_info["__title__"],
-    version=package_info["__version__"],
-    url=package_info["__uri__"],
-    author=package_info["__author__"],
-    description=package_info["__description__"],
-    keywords="botframework azure botbuilder",
-    long_description=package_info["__summary__"],
-    license=package_info["__license__"],
-    packages=["model_runtime_svc_corebot101", "model_runtime_svc_corebot101.handlers"],
-    install_requires=REQUIRES,
-    dependency_links=["https://github.com/pytorch/pytorch"],
-    include_package_data=True,
-    classifiers=[
-        "Programming Language :: Python :: 3.6",
-        "Intended Audience :: Developers",
-        "License :: OSI Approved :: MIT License",
-        "Operating System :: OS Independent",
-        "Development Status :: 3 - Alpha",
-        "Topic :: Scientific/Engineering :: Artificial Intelligence",
-    ],
-)
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+from setuptools import setup
+
+REQUIRES = [
+    "scikit-learn>=0.21.2",
+    "scipy>=1.3.0",
+    "tornado>=6.0.2",
+    "model_corebot101>=0.0.1",
+]
+
+root = os.path.abspath(os.path.dirname(__file__))
+
+with open(os.path.join(root, "model_runtime_svc_corebot101", "about.py")) as f:
+    package_info = {}
+    info = f.read()
+    exec(info, package_info)
+
+setup(
+    name=package_info["__title__"],
+    version=package_info["__version__"],
+    url=package_info["__uri__"],
+    author=package_info["__author__"],
+    description=package_info["__description__"],
+    keywords="botframework azure botbuilder",
+    long_description=package_info["__summary__"],
+    license=package_info["__license__"],
+    packages=["model_runtime_svc_corebot101", "model_runtime_svc_corebot101.handlers"],
+    install_requires=REQUIRES,
+    dependency_links=["https://github.com/pytorch/pytorch"],
+    include_package_data=True,
+    classifiers=[
+        "Programming Language :: Python :: 3.6",
+        "Intended Audience :: Developers",
+        "License :: OSI Approved :: MIT License",
+        "Operating System :: OS Independent",
+        "Development Status :: 3 - Alpha",
+        "Topic :: Scientific/Engineering :: Artificial Intelligence",
+    ],
+)
diff --git a/samples/experimental/101.corebot-bert-bidaf/notebooks/bert_model_runtime.ipynb b/tests/experimental/101.corebot-bert-bidaf/notebooks/bert_model_runtime.ipynb
similarity index 95%
rename from samples/experimental/101.corebot-bert-bidaf/notebooks/bert_model_runtime.ipynb
rename to tests/experimental/101.corebot-bert-bidaf/notebooks/bert_model_runtime.ipynb
index 4dd87e70d..fc32cd77e 100644
--- a/samples/experimental/101.corebot-bert-bidaf/notebooks/bert_model_runtime.ipynb
+++ b/tests/experimental/101.corebot-bert-bidaf/notebooks/bert_model_runtime.ipynb
@@ -1,323 +1,323 @@
-{
- "cells": [
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "# Test the intent classifier\n",
-    "This notebook uses the model trained in [bert_train.ipynb notebook](bert_train.ipynb). See the [README.md](../README.md) for instructions on how to use that notebook.\n",
-    "\n",
-    "## `model_corebot101` package\n",
-    "This sample creates a separate python package (`model_corebot101`) which contains code to train (tune), evaluate and infer intent classifiers for this sample.\n",
-    "\n",
-    "\n",
-    "## See also:\n",
-    "- [bert_train.ipynb](bert_train.ipynb) to train the intent classifier model.\n",
-    "- [bidaf_model_runtime.ipynb](bidaf_model_runtime.ipynb) to test the associated BiDAF model to test the entity classifier model.\n",
-    "- [The model runtime](model_runtime.ipynb) to test the both the BERT and BiDAF model together."
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 1,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "from model_corebot101.bert.model_runtime.bert_model_runtime import BertModelRuntime"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "## `BertModelRuntime` class\n",
-    "The `BertModelRuntime` class is used to perform the inferences against the bot utterances.\n",
-    "\n",
-    "The model is placed (during training) in the `/models/bert` directory which is packaged with the bot.\n",
-    "\n",
-    "The `label_list` is an array of intents."
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 2,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "import os\n",
-    "from pathlib import Path\n",
-    "bert_model_dir = os.path.abspath(os.path.join(Path.home(), \"models/bert\"))\n",
-    "s = BertModelRuntime(model_dir=bert_model_dir, label_list=[\"Book flight\", \"Cancel\"])"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "## `BertModelRuntime.serve` method\n",
-    "The `BertModelRuntime.serve` method is used to perform the classification against the bot utterances."
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 3,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "'Book flight'"
-      ]
-     },
-     "execution_count": 3,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "s.serve('i want to travel from new york to berlin')"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 4,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "'Book flight'"
-      ]
-     },
-     "execution_count": 4,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "s.serve(\"please book a flight for me\")"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 5,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "'Book flight'"
-      ]
-     },
-     "execution_count": 5,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "s.serve(\"from seattle to san\")"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 6,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "'Cancel'"
-      ]
-     },
-     "execution_count": 6,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "s.serve(\"random random random 42\")"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 7,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "'Cancel'"
-      ]
-     },
-     "execution_count": 7,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "s.serve(\"any\")"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 8,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "'Book flight'"
-      ]
-     },
-     "execution_count": 8,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "s.serve(\"take me to New York\")"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 9,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "'Book flight'"
-      ]
-     },
-     "execution_count": 9,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "s.serve(\"we'd like to go to seattle\")"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 10,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "'Cancel'"
-      ]
-     },
-     "execution_count": 10,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "s.serve(\"not this one\")"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 11,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "'Cancel'"
-      ]
-     },
-     "execution_count": 11,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "s.serve(\"I don't care about this one\")"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 12,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "'Cancel'"
-      ]
-     },
-     "execution_count": 12,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "s.serve(\"I don't want to see that\")"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 13,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "'Cancel'"
-      ]
-     },
-     "execution_count": 13,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "s.serve(\"boring\")"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 14,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "'Book flight'"
-      ]
-     },
-     "execution_count": 14,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "s.serve(\"you have no clue how to book a flight\")"
-   ]
-  }
- ],
- "metadata": {
-  "kernelspec": {
-   "display_name": "botsample",
-   "language": "python",
-   "name": "botsample"
-  },
-  "language_info": {
-   "codemirror_mode": {
-    "name": "ipython",
-    "version": 3
-   },
-   "file_extension": ".py",
-   "mimetype": "text/x-python",
-   "name": "python",
-   "nbconvert_exporter": "python",
-   "pygments_lexer": "ipython3",
-   "version": "3.6.8"
-  }
- },
- "nbformat": 4,
- "nbformat_minor": 2
-}
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Test the intent classifier\n",
+    "This notebook uses the model trained in [bert_train.ipynb notebook](bert_train.ipynb). See the [README.md](../README.md) for instructions on how to use that notebook.\n",
+    "\n",
+    "## `model_corebot101` package\n",
+    "This sample creates a separate python package (`model_corebot101`) which contains code to train (tune), evaluate and infer intent classifiers for this sample.\n",
+    "\n",
+    "\n",
+    "## See also:\n",
+    "- [bert_train.ipynb](bert_train.ipynb) to train the intent classifier model.\n",
+    "- [bidaf_model_runtime.ipynb](bidaf_model_runtime.ipynb) to test the associated BiDAF model to test the entity classifier model.\n",
+    "- [The model runtime](model_runtime.ipynb) to test the both the BERT and BiDAF model together."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from model_corebot101.bert.model_runtime.bert_model_runtime import BertModelRuntime"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## `BertModelRuntime` class\n",
+    "The `BertModelRuntime` class is used to perform the inferences against the bot utterances.\n",
+    "\n",
+    "The model is placed (during training) in the `/models/bert` directory which is packaged with the bot.\n",
+    "\n",
+    "The `label_list` is an array of intents."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import os\n",
+    "from pathlib import Path\n",
+    "bert_model_dir = os.path.abspath(os.path.join(Path.home(), \"models/bert\"))\n",
+    "s = BertModelRuntime(model_dir=bert_model_dir, label_list=[\"Book flight\", \"Cancel\"])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## `BertModelRuntime.serve` method\n",
+    "The `BertModelRuntime.serve` method is used to perform the classification against the bot utterances."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'Book flight'"
+      ]
+     },
+     "execution_count": 3,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "s.serve('i want to travel from new york to berlin')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'Book flight'"
+      ]
+     },
+     "execution_count": 4,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "s.serve(\"please book a flight for me\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'Book flight'"
+      ]
+     },
+     "execution_count": 5,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "s.serve(\"from seattle to san\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'Cancel'"
+      ]
+     },
+     "execution_count": 6,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "s.serve(\"random random random 42\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'Cancel'"
+      ]
+     },
+     "execution_count": 7,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "s.serve(\"any\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'Book flight'"
+      ]
+     },
+     "execution_count": 8,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "s.serve(\"take me to New York\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 9,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'Book flight'"
+      ]
+     },
+     "execution_count": 9,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "s.serve(\"we'd like to go to seattle\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 10,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'Cancel'"
+      ]
+     },
+     "execution_count": 10,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "s.serve(\"not this one\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 11,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'Cancel'"
+      ]
+     },
+     "execution_count": 11,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "s.serve(\"I don't care about this one\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 12,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'Cancel'"
+      ]
+     },
+     "execution_count": 12,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "s.serve(\"I don't want to see that\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 13,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'Cancel'"
+      ]
+     },
+     "execution_count": 13,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "s.serve(\"boring\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 14,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'Book flight'"
+      ]
+     },
+     "execution_count": 14,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "s.serve(\"you have no clue how to book a flight\")"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "botsample",
+   "language": "python",
+   "name": "botsample"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.6.8"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/samples/experimental/101.corebot-bert-bidaf/notebooks/bert_train.ipynb b/tests/experimental/101.corebot-bert-bidaf/notebooks/bert_train.ipynb
similarity index 99%
rename from samples/experimental/101.corebot-bert-bidaf/notebooks/bert_train.ipynb
rename to tests/experimental/101.corebot-bert-bidaf/notebooks/bert_train.ipynb
index a9296240e..fe95eb688 100644
--- a/samples/experimental/101.corebot-bert-bidaf/notebooks/bert_train.ipynb
+++ b/tests/experimental/101.corebot-bert-bidaf/notebooks/bert_train.ipynb
@@ -1,281 +1,281 @@
-{
- "cells": [
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "# Train the intent classifier using pretrained BERT model as featurizer\n",
-    "This notebook creates the BERT language classifier model.  See the [README.md](../README.md) for instructions on how to run this sample.\n",
-    "The resulting model is placed in the `/models/bert` directory which is packaged with the bot.\n",
-    "\n",
-    "## `model_corebot101` package\n",
-    "This sample creates a separate python package (`model_corebot101`) which contains all the code to train, evaluate and infer intent classifiers for this sample.\n",
-    "\n",
-    "## See also:\n",
-    "- [The BERT runtime model](bert_model_runtime.ipynb) to test the resulting intent classifier model.\n",
-    "- [The BiDAF runtime model](bidaf_model_runtime.ipynb) to test the associated BiDAF model to test the entity classifier model.\n",
-    "- [The model runtime](model_runtime.ipynb) to test the both the BERT and BiDAF model together.\n",
-    "\n"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 1,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "from model_corebot101.bert.train import BertTrainEval"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "## `BertTrainEvan.train_eval` method\n",
-    "This method performs all the training and performs evaluation that's listed at the bottom of the output.  Training may take several minutes to complete.\n",
-    "\n",
-    "The evaluation output should look something like the following:\n",
-    "```bash\n",
-    "06/02/2019 19:46:52 - INFO - model_corebot101.bert.train.bert_train_eval -   ***** Eval results *****\n",
-    "06/02/2019 19:46:52 - INFO - model_corebot101.bert.train.bert_train_eval -     acc = 1.0\n",
-    "06/02/2019 19:46:52 - INFO - model_corebot101.bert.train.bert_train_eval -     acc_and_f1 = 1.0\n",
-    "06/02/2019 19:46:52 - INFO - model_corebot101.bert.train.bert_train_eval -     eval_loss = 0.06498947739601135\n",
-    "06/02/2019 19:46:52 - INFO - model_corebot101.bert.train.bert_train_eval -     f1 = 1.0\n",
-    "06/02/2019 19:46:52 - INFO - model_corebot101.bert.train.bert_train_eval -     global_step = 12\n",
-    "06/02/2019 19:46:52 - INFO - model_corebot101.bert.train.bert_train_eval -     loss = 0.02480666587750117\n",
-    "```"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 2,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stderr",
-     "output_type": "stream",
-     "text": [
-      "Bert Model training_data_dir is set to d:\\python\\daveta-docker-wizard\\apub\\samples\\flask\\101.corebot-bert-bidaf\\model\\model_corebot101\\bert\\training_data\n",
-      "Bert Model model_dir is set to C:\\Users\\daveta\\models\\bert\n",
-      "07/02/2019 07:16:09 - INFO - model_corebot101.bert.train.bert_train_eval -   device: cpu n_gpu: 0, distributed training: False, 16-bits training: None\n",
-      "07/02/2019 07:16:09 - INFO - pytorch_pretrained_bert.tokenization -   loading vocabulary file https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-uncased-vocab.txt from cache at C:\\Users\\daveta\\.pytorch_pretrained_bert\\26bc1ad6c0ac742e9b52263248f6d0f00068293b33709fae12320c0e35ccfbbb.542ce4285a40d23a559526243235df47c5f75c197f04f37d1a0c124c32c9a084\n",
-      "07/02/2019 07:16:10 - INFO - pytorch_pretrained_bert.modeling -   loading archive file https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-uncased.tar.gz from cache at C:\\Users\\daveta\\.pytorch_pretrained_bert\\distributed_-1\\9c41111e2de84547a463fd39217199738d1e3deb72d4fec4399e6e241983c6f0.ae3cef932725ca7a30cdcb93fc6e09150a55e2a130ec7af63975a16c153ae2ba\n",
-      "07/02/2019 07:16:10 - INFO - pytorch_pretrained_bert.modeling -   extracting archive file C:\\Users\\daveta\\.pytorch_pretrained_bert\\distributed_-1\\9c41111e2de84547a463fd39217199738d1e3deb72d4fec4399e6e241983c6f0.ae3cef932725ca7a30cdcb93fc6e09150a55e2a130ec7af63975a16c153ae2ba to temp dir C:\\Users\\daveta\\AppData\\Local\\Temp\\tmp9hepebcl\n",
-      "07/02/2019 07:16:13 - INFO - pytorch_pretrained_bert.modeling -   Model config {\n",
-      "  \"attention_probs_dropout_prob\": 0.1,\n",
-      "  \"hidden_act\": \"gelu\",\n",
-      "  \"hidden_dropout_prob\": 0.1,\n",
-      "  \"hidden_size\": 768,\n",
-      "  \"initializer_range\": 0.02,\n",
-      "  \"intermediate_size\": 3072,\n",
-      "  \"max_position_embeddings\": 512,\n",
-      "  \"num_attention_heads\": 12,\n",
-      "  \"num_hidden_layers\": 12,\n",
-      "  \"type_vocab_size\": 2,\n",
-      "  \"vocab_size\": 30522\n",
-      "}\n",
-      "\n",
-      "07/02/2019 07:16:16 - INFO - pytorch_pretrained_bert.modeling -   Weights of BertForSequenceClassification not initialized from pretrained model: ['classifier.weight', 'classifier.bias']\n",
-      "07/02/2019 07:16:16 - INFO - pytorch_pretrained_bert.modeling -   Weights from pretrained model not used in BertForSequenceClassification: ['cls.predictions.bias', 'cls.predictions.transform.dense.weight', 'cls.predictions.transform.dense.bias', 'cls.predictions.decoder.weight', 'cls.seq_relationship.weight', 'cls.seq_relationship.bias', 'cls.predictions.transform.LayerNorm.weight', 'cls.predictions.transform.LayerNorm.bias']\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   Writing example 0 of 16\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   *** Example ***\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   guid: train-0\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   tokens: [CLS] book flight from london to paris on feb 14th [SEP]\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   input_ids: 101 2338 3462 2013 2414 2000 3000 2006 13114 6400 102 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   input_mask: 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   segment_ids: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   label: Book flight (id = 0)\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   *** Example ***\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   guid: train-1\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   tokens: [CLS] book flight to berlin on feb 14th [SEP]\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   input_ids: 101 2338 3462 2000 4068 2006 13114 6400 102 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   input_mask: 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   segment_ids: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   label: Book flight (id = 0)\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   *** Example ***\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   guid: train-2\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   tokens: [CLS] book me a flight from london to paris [SEP]\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   input_ids: 101 2338 2033 1037 3462 2013 2414 2000 3000 102 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   input_mask: 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   segment_ids: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   label: Book flight (id = 0)\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   *** Example ***\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   guid: train-3\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   tokens: [CLS] bye [SEP]\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   input_ids: 101 9061 102 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   input_mask: 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   segment_ids: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   label: Cancel (id = 1)\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   *** Example ***\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   guid: train-4\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   tokens: [CLS] cancel booking [SEP]\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   input_ids: 101 17542 21725 102 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   input_mask: 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   segment_ids: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   label: Cancel (id = 1)\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.train.bert_train_eval -   ***** Running training *****\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.train.bert_train_eval -     Num examples = 16\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.train.bert_train_eval -     Batch size = 4\n",
-      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.train.bert_train_eval -     Num steps = 12\n",
-      "Epoch:   0%|                                                                                     | 0/3 [00:00, ?it/s]\n",
-      "Iteration:   0%|                                                                                 | 0/4 [00:00, ?it/s]\n",
-      "Iteration:  25%|██████████████████▎                                                      | 1/4 [00:05<00:16,  5.40s/it]\n",
-      "Iteration:  50%|████████████████████████████████████▌                                    | 2/4 [00:11<00:11,  5.60s/it]\n",
-      "Iteration:  75%|██████████████████████████████████████████████████████▊                  | 3/4 [00:17<00:05,  5.63s/it]\n",
-      "Epoch:  33%|█████████████████████████▋                                                   | 1/3 [00:22<00:45, 22.85s/it]\n",
-      "Iteration:   0%|                                                                                 | 0/4 [00:00, ?it/s]\n",
-      "Iteration:  25%|██████████████████▎                                                      | 1/4 [00:05<00:17,  5.83s/it]\n",
-      "Iteration:  50%|████████████████████████████████████▌                                    | 2/4 [00:11<00:11,  5.78s/it]\n",
-      "Iteration:  75%|██████████████████████████████████████████████████████▊                  | 3/4 [00:17<00:05,  5.73s/it]\n",
-      "Epoch:  67%|███████████████████████████████████████████████████▎                         | 2/3 [00:45<00:22, 22.85s/it]\n",
-      "Iteration:   0%|                                                                                 | 0/4 [00:00, ?it/s]\n",
-      "Iteration:  25%|██████████████████▎                                                      | 1/4 [00:05<00:16,  5.50s/it]\n",
-      "Iteration:  50%|████████████████████████████████████▌                                    | 2/4 [00:11<00:11,  5.51s/it]\n",
-      "Iteration:  75%|██████████████████████████████████████████████████████▊                  | 3/4 [00:16<00:05,  5.47s/it]\n",
-      "Epoch: 100%|█████████████████████████████████████████████████████████████████████████████| 3/3 [01:07<00:00, 22.61s/it]\n",
-      "07/02/2019 07:17:24 - INFO - pytorch_pretrained_bert.modeling -   loading archive file C:\\Users\\daveta\\models\\bert\n",
-      "07/02/2019 07:17:24 - INFO - pytorch_pretrained_bert.modeling -   Model config {\n",
-      "  \"attention_probs_dropout_prob\": 0.1,\n",
-      "  \"hidden_act\": \"gelu\",\n",
-      "  \"hidden_dropout_prob\": 0.1,\n",
-      "  \"hidden_size\": 768,\n",
-      "  \"initializer_range\": 0.02,\n",
-      "  \"intermediate_size\": 3072,\n",
-      "  \"max_position_embeddings\": 512,\n",
-      "  \"num_attention_heads\": 12,\n",
-      "  \"num_hidden_layers\": 12,\n",
-      "  \"type_vocab_size\": 2,\n",
-      "  \"vocab_size\": 30522\n",
-      "}\n",
-      "\n",
-      "07/02/2019 07:17:26 - INFO - pytorch_pretrained_bert.tokenization -   loading vocabulary file C:\\Users\\daveta\\models\\bert\\vocab.txt\n",
-      "07/02/2019 07:17:26 - INFO - model_corebot101.bert.train.bert_train_eval -   DONE TRAINING.\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   Writing example 0 of 16\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   *** Example ***\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   guid: dev-0\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   tokens: [CLS] book flight from london to paris on feb 14th [SEP]\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   input_ids: 101 2338 3462 2013 2414 2000 3000 2006 13114 6400 102 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   input_mask: 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   segment_ids: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   label: Book flight (id = 0)\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   *** Example ***\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   guid: dev-1\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   tokens: [CLS] book flight to berlin on feb 14th [SEP]\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   input_ids: 101 2338 3462 2000 4068 2006 13114 6400 102 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   input_mask: 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   segment_ids: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   label: Book flight (id = 0)\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   *** Example ***\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   guid: dev-2\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   tokens: [CLS] book me a flight from london to paris [SEP]\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   input_ids: 101 2338 2033 1037 3462 2013 2414 2000 3000 102 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   input_mask: 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   segment_ids: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   label: Book flight (id = 0)\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   *** Example ***\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   guid: dev-3\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   tokens: [CLS] bye [SEP]\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   input_ids: 101 9061 102 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   input_mask: 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   segment_ids: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   label: Cancel (id = 1)\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   *** Example ***\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   guid: dev-4\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   tokens: [CLS] cancel booking [SEP]\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   input_ids: 101 17542 21725 102 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   input_mask: 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   segment_ids: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   label: Cancel (id = 1)\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.train.bert_train_eval -   ***** Running evaluation *****\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.train.bert_train_eval -     Num examples = 16\n",
-      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.train.bert_train_eval -     Batch size = 8\n",
-      "Evaluating: 100%|████████████████████████████████████████████████████████████████████████| 2/2 [00:04<00:00,  2.46s/it]\n",
-      "07/02/2019 07:17:32 - INFO - model_corebot101.bert.train.bert_train_eval -   ***** Eval results *****\n",
-      "07/02/2019 07:17:32 - INFO - model_corebot101.bert.train.bert_train_eval -     acc = 1.0\n",
-      "07/02/2019 07:17:32 - INFO - model_corebot101.bert.train.bert_train_eval -     acc_and_f1 = 1.0\n",
-      "07/02/2019 07:17:32 - INFO - model_corebot101.bert.train.bert_train_eval -     eval_loss = 0.026343628764152527\n",
-      "07/02/2019 07:17:32 - INFO - model_corebot101.bert.train.bert_train_eval -     f1 = 1.0\n",
-      "07/02/2019 07:17:32 - INFO - model_corebot101.bert.train.bert_train_eval -     global_step = 12\n",
-      "07/02/2019 07:17:32 - INFO - model_corebot101.bert.train.bert_train_eval -     loss = 0.01322490597764651\n",
-      "07/02/2019 07:17:32 - INFO - model_corebot101.bert.train.bert_train_eval -   DONE EVALUATING.\n"
-     ]
-    }
-   ],
-   "source": [
-    "BertTrainEval.train_eval(cleanup_output_dir=True)"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "## Verify the output directory"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 4,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "application/vnd.jupyter.widget-view+json": {
-       "model_id": "0cd881fcb42b4d76a877907936a8d245",
-       "version_major": 2,
-       "version_minor": 0
-      },
-      "text/plain": [
-       "HBox(children=(IntProgress(value=0, description='Verify Output', max=4, style=ProgressStyle(description_width=…"
-      ]
-     },
-     "metadata": {},
-     "output_type": "display_data"
-    },
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "\n"
-     ]
-    }
-   ],
-   "source": [
-    "import os\n",
-    "from pathlib import Path\n",
-    "from tqdm import tqdm_notebook\n",
-    "home_dir = str(Path.home())\n",
-    "path = os.path.abspath(os.path.join(home_dir, \"models/bert\"))\n",
-    "files_with_size = {file:os.path.getsize(os.path.join(path, file)) for file in os.listdir(path)}\n",
-    "expected = {'config.json':326, 'eval_results.txt':119, 'pytorch_model.bin':437982182, 'vocab.txt':262030}\n",
-    "for f in tqdm_notebook(expected.keys(), desc='Verify Output'):\n",
-    "    if f in files_with_size:\n",
-    "        delta = abs(expected[f] - files_with_size[f]) / expected[f]\n",
-    "        if delta > float(.30):\n",
-    "            raise Exception(f'Size of output file {f} is out of range of expected.')\n",
-    "    else:\n",
-    "        raise Exception(f'Expected file {f} missing from output.')"
-   ]
-  }
- ],
- "metadata": {
-  "kernelspec": {
-   "display_name": "botsample",
-   "language": "python",
-   "name": "botsample"
-  },
-  "language_info": {
-   "codemirror_mode": {
-    "name": "ipython",
-    "version": 3
-   },
-   "file_extension": ".py",
-   "mimetype": "text/x-python",
-   "name": "python",
-   "nbconvert_exporter": "python",
-   "pygments_lexer": "ipython3",
-   "version": "3.6.8"
-  }
- },
- "nbformat": 4,
- "nbformat_minor": 2
-}
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Train the intent classifier using pretrained BERT model as featurizer\n",
+    "This notebook creates the BERT language classifier model.  See the [README.md](../README.md) for instructions on how to run this sample.\n",
+    "The resulting model is placed in the `/models/bert` directory which is packaged with the bot.\n",
+    "\n",
+    "## `model_corebot101` package\n",
+    "This sample creates a separate python package (`model_corebot101`) which contains all the code to train, evaluate and infer intent classifiers for this sample.\n",
+    "\n",
+    "## See also:\n",
+    "- [The BERT runtime model](bert_model_runtime.ipynb) to test the resulting intent classifier model.\n",
+    "- [The BiDAF runtime model](bidaf_model_runtime.ipynb) to test the associated BiDAF model to test the entity classifier model.\n",
+    "- [The model runtime](model_runtime.ipynb) to test the both the BERT and BiDAF model together.\n",
+    "\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from model_corebot101.bert.train import BertTrainEval"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## `BertTrainEvan.train_eval` method\n",
+    "This method performs all the training and performs evaluation that's listed at the bottom of the output.  Training may take several minutes to complete.\n",
+    "\n",
+    "The evaluation output should look something like the following:\n",
+    "```bash\n",
+    "06/02/2019 19:46:52 - INFO - model_corebot101.bert.train.bert_train_eval -   ***** Eval results *****\n",
+    "06/02/2019 19:46:52 - INFO - model_corebot101.bert.train.bert_train_eval -     acc = 1.0\n",
+    "06/02/2019 19:46:52 - INFO - model_corebot101.bert.train.bert_train_eval -     acc_and_f1 = 1.0\n",
+    "06/02/2019 19:46:52 - INFO - model_corebot101.bert.train.bert_train_eval -     eval_loss = 0.06498947739601135\n",
+    "06/02/2019 19:46:52 - INFO - model_corebot101.bert.train.bert_train_eval -     f1 = 1.0\n",
+    "06/02/2019 19:46:52 - INFO - model_corebot101.bert.train.bert_train_eval -     global_step = 12\n",
+    "06/02/2019 19:46:52 - INFO - model_corebot101.bert.train.bert_train_eval -     loss = 0.02480666587750117\n",
+    "```"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "Bert Model training_data_dir is set to d:\\python\\daveta-docker-wizard\\apub\\samples\\flask\\101.corebot-bert-bidaf\\model\\model_corebot101\\bert\\training_data\n",
+      "Bert Model model_dir is set to C:\\Users\\daveta\\models\\bert\n",
+      "07/02/2019 07:16:09 - INFO - model_corebot101.bert.train.bert_train_eval -   device: cpu n_gpu: 0, distributed training: False, 16-bits training: None\n",
+      "07/02/2019 07:16:09 - INFO - pytorch_pretrained_bert.tokenization -   loading vocabulary file https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-uncased-vocab.txt from cache at C:\\Users\\daveta\\.pytorch_pretrained_bert\\26bc1ad6c0ac742e9b52263248f6d0f00068293b33709fae12320c0e35ccfbbb.542ce4285a40d23a559526243235df47c5f75c197f04f37d1a0c124c32c9a084\n",
+      "07/02/2019 07:16:10 - INFO - pytorch_pretrained_bert.modeling -   loading archive file https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-uncased.tar.gz from cache at C:\\Users\\daveta\\.pytorch_pretrained_bert\\distributed_-1\\9c41111e2de84547a463fd39217199738d1e3deb72d4fec4399e6e241983c6f0.ae3cef932725ca7a30cdcb93fc6e09150a55e2a130ec7af63975a16c153ae2ba\n",
+      "07/02/2019 07:16:10 - INFO - pytorch_pretrained_bert.modeling -   extracting archive file C:\\Users\\daveta\\.pytorch_pretrained_bert\\distributed_-1\\9c41111e2de84547a463fd39217199738d1e3deb72d4fec4399e6e241983c6f0.ae3cef932725ca7a30cdcb93fc6e09150a55e2a130ec7af63975a16c153ae2ba to temp dir C:\\Users\\daveta\\AppData\\Local\\Temp\\tmp9hepebcl\n",
+      "07/02/2019 07:16:13 - INFO - pytorch_pretrained_bert.modeling -   Model config {\n",
+      "  \"attention_probs_dropout_prob\": 0.1,\n",
+      "  \"hidden_act\": \"gelu\",\n",
+      "  \"hidden_dropout_prob\": 0.1,\n",
+      "  \"hidden_size\": 768,\n",
+      "  \"initializer_range\": 0.02,\n",
+      "  \"intermediate_size\": 3072,\n",
+      "  \"max_position_embeddings\": 512,\n",
+      "  \"num_attention_heads\": 12,\n",
+      "  \"num_hidden_layers\": 12,\n",
+      "  \"type_vocab_size\": 2,\n",
+      "  \"vocab_size\": 30522\n",
+      "}\n",
+      "\n",
+      "07/02/2019 07:16:16 - INFO - pytorch_pretrained_bert.modeling -   Weights of BertForSequenceClassification not initialized from pretrained model: ['classifier.weight', 'classifier.bias']\n",
+      "07/02/2019 07:16:16 - INFO - pytorch_pretrained_bert.modeling -   Weights from pretrained model not used in BertForSequenceClassification: ['cls.predictions.bias', 'cls.predictions.transform.dense.weight', 'cls.predictions.transform.dense.bias', 'cls.predictions.decoder.weight', 'cls.seq_relationship.weight', 'cls.seq_relationship.bias', 'cls.predictions.transform.LayerNorm.weight', 'cls.predictions.transform.LayerNorm.bias']\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   Writing example 0 of 16\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   *** Example ***\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   guid: train-0\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   tokens: [CLS] book flight from london to paris on feb 14th [SEP]\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   input_ids: 101 2338 3462 2013 2414 2000 3000 2006 13114 6400 102 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   input_mask: 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   segment_ids: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   label: Book flight (id = 0)\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   *** Example ***\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   guid: train-1\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   tokens: [CLS] book flight to berlin on feb 14th [SEP]\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   input_ids: 101 2338 3462 2000 4068 2006 13114 6400 102 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   input_mask: 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   segment_ids: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   label: Book flight (id = 0)\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   *** Example ***\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   guid: train-2\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   tokens: [CLS] book me a flight from london to paris [SEP]\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   input_ids: 101 2338 2033 1037 3462 2013 2414 2000 3000 102 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   input_mask: 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   segment_ids: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   label: Book flight (id = 0)\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   *** Example ***\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   guid: train-3\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   tokens: [CLS] bye [SEP]\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   input_ids: 101 9061 102 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   input_mask: 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   segment_ids: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   label: Cancel (id = 1)\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   *** Example ***\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   guid: train-4\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   tokens: [CLS] cancel booking [SEP]\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   input_ids: 101 17542 21725 102 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   input_mask: 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   segment_ids: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.common.bert_util -   label: Cancel (id = 1)\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.train.bert_train_eval -   ***** Running training *****\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.train.bert_train_eval -     Num examples = 16\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.train.bert_train_eval -     Batch size = 4\n",
+      "07/02/2019 07:16:16 - INFO - model_corebot101.bert.train.bert_train_eval -     Num steps = 12\n",
+      "Epoch:   0%|                                                                                     | 0/3 [00:00, ?it/s]\n",
+      "Iteration:   0%|                                                                                 | 0/4 [00:00, ?it/s]\n",
+      "Iteration:  25%|██████████████████▎                                                      | 1/4 [00:05<00:16,  5.40s/it]\n",
+      "Iteration:  50%|████████████████████████████████████▌                                    | 2/4 [00:11<00:11,  5.60s/it]\n",
+      "Iteration:  75%|██████████████████████████████████████████████████████▊                  | 3/4 [00:17<00:05,  5.63s/it]\n",
+      "Epoch:  33%|█████████████████████████▋                                                   | 1/3 [00:22<00:45, 22.85s/it]\n",
+      "Iteration:   0%|                                                                                 | 0/4 [00:00, ?it/s]\n",
+      "Iteration:  25%|██████████████████▎                                                      | 1/4 [00:05<00:17,  5.83s/it]\n",
+      "Iteration:  50%|████████████████████████████████████▌                                    | 2/4 [00:11<00:11,  5.78s/it]\n",
+      "Iteration:  75%|██████████████████████████████████████████████████████▊                  | 3/4 [00:17<00:05,  5.73s/it]\n",
+      "Epoch:  67%|███████████████████████████████████████████████████▎                         | 2/3 [00:45<00:22, 22.85s/it]\n",
+      "Iteration:   0%|                                                                                 | 0/4 [00:00, ?it/s]\n",
+      "Iteration:  25%|██████████████████▎                                                      | 1/4 [00:05<00:16,  5.50s/it]\n",
+      "Iteration:  50%|████████████████████████████████████▌                                    | 2/4 [00:11<00:11,  5.51s/it]\n",
+      "Iteration:  75%|██████████████████████████████████████████████████████▊                  | 3/4 [00:16<00:05,  5.47s/it]\n",
+      "Epoch: 100%|█████████████████████████████████████████████████████████████████████████████| 3/3 [01:07<00:00, 22.61s/it]\n",
+      "07/02/2019 07:17:24 - INFO - pytorch_pretrained_bert.modeling -   loading archive file C:\\Users\\daveta\\models\\bert\n",
+      "07/02/2019 07:17:24 - INFO - pytorch_pretrained_bert.modeling -   Model config {\n",
+      "  \"attention_probs_dropout_prob\": 0.1,\n",
+      "  \"hidden_act\": \"gelu\",\n",
+      "  \"hidden_dropout_prob\": 0.1,\n",
+      "  \"hidden_size\": 768,\n",
+      "  \"initializer_range\": 0.02,\n",
+      "  \"intermediate_size\": 3072,\n",
+      "  \"max_position_embeddings\": 512,\n",
+      "  \"num_attention_heads\": 12,\n",
+      "  \"num_hidden_layers\": 12,\n",
+      "  \"type_vocab_size\": 2,\n",
+      "  \"vocab_size\": 30522\n",
+      "}\n",
+      "\n",
+      "07/02/2019 07:17:26 - INFO - pytorch_pretrained_bert.tokenization -   loading vocabulary file C:\\Users\\daveta\\models\\bert\\vocab.txt\n",
+      "07/02/2019 07:17:26 - INFO - model_corebot101.bert.train.bert_train_eval -   DONE TRAINING.\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   Writing example 0 of 16\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   *** Example ***\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   guid: dev-0\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   tokens: [CLS] book flight from london to paris on feb 14th [SEP]\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   input_ids: 101 2338 3462 2013 2414 2000 3000 2006 13114 6400 102 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   input_mask: 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   segment_ids: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   label: Book flight (id = 0)\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   *** Example ***\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   guid: dev-1\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   tokens: [CLS] book flight to berlin on feb 14th [SEP]\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   input_ids: 101 2338 3462 2000 4068 2006 13114 6400 102 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   input_mask: 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   segment_ids: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   label: Book flight (id = 0)\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   *** Example ***\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   guid: dev-2\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   tokens: [CLS] book me a flight from london to paris [SEP]\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   input_ids: 101 2338 2033 1037 3462 2013 2414 2000 3000 102 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   input_mask: 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   segment_ids: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   label: Book flight (id = 0)\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   *** Example ***\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   guid: dev-3\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   tokens: [CLS] bye [SEP]\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   input_ids: 101 9061 102 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   input_mask: 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   segment_ids: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   label: Cancel (id = 1)\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   *** Example ***\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   guid: dev-4\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   tokens: [CLS] cancel booking [SEP]\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   input_ids: 101 17542 21725 102 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   input_mask: 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   segment_ids: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.common.bert_util -   label: Cancel (id = 1)\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.train.bert_train_eval -   ***** Running evaluation *****\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.train.bert_train_eval -     Num examples = 16\n",
+      "07/02/2019 07:17:27 - INFO - model_corebot101.bert.train.bert_train_eval -     Batch size = 8\n",
+      "Evaluating: 100%|████████████████████████████████████████████████████████████████████████| 2/2 [00:04<00:00,  2.46s/it]\n",
+      "07/02/2019 07:17:32 - INFO - model_corebot101.bert.train.bert_train_eval -   ***** Eval results *****\n",
+      "07/02/2019 07:17:32 - INFO - model_corebot101.bert.train.bert_train_eval -     acc = 1.0\n",
+      "07/02/2019 07:17:32 - INFO - model_corebot101.bert.train.bert_train_eval -     acc_and_f1 = 1.0\n",
+      "07/02/2019 07:17:32 - INFO - model_corebot101.bert.train.bert_train_eval -     eval_loss = 0.026343628764152527\n",
+      "07/02/2019 07:17:32 - INFO - model_corebot101.bert.train.bert_train_eval -     f1 = 1.0\n",
+      "07/02/2019 07:17:32 - INFO - model_corebot101.bert.train.bert_train_eval -     global_step = 12\n",
+      "07/02/2019 07:17:32 - INFO - model_corebot101.bert.train.bert_train_eval -     loss = 0.01322490597764651\n",
+      "07/02/2019 07:17:32 - INFO - model_corebot101.bert.train.bert_train_eval -   DONE EVALUATING.\n"
+     ]
+    }
+   ],
+   "source": [
+    "BertTrainEval.train_eval(cleanup_output_dir=True)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Verify the output directory"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "0cd881fcb42b4d76a877907936a8d245",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "HBox(children=(IntProgress(value=0, description='Verify Output', max=4, style=ProgressStyle(description_width=…"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\n"
+     ]
+    }
+   ],
+   "source": [
+    "import os\n",
+    "from pathlib import Path\n",
+    "from tqdm import tqdm_notebook\n",
+    "home_dir = str(Path.home())\n",
+    "path = os.path.abspath(os.path.join(home_dir, \"models/bert\"))\n",
+    "files_with_size = {file:os.path.getsize(os.path.join(path, file)) for file in os.listdir(path)}\n",
+    "expected = {'config.json':326, 'eval_results.txt':119, 'pytorch_model.bin':437982182, 'vocab.txt':262030}\n",
+    "for f in tqdm_notebook(expected.keys(), desc='Verify Output'):\n",
+    "    if f in files_with_size:\n",
+    "        delta = abs(expected[f] - files_with_size[f]) / expected[f]\n",
+    "        if delta > float(.30):\n",
+    "            raise Exception(f'Size of output file {f} is out of range of expected.')\n",
+    "    else:\n",
+    "        raise Exception(f'Expected file {f} missing from output.')"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "botsample",
+   "language": "python",
+   "name": "botsample"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.6.8"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/samples/experimental/101.corebot-bert-bidaf/notebooks/bidaf_model_runtime.ipynb b/tests/experimental/101.corebot-bert-bidaf/notebooks/bidaf_model_runtime.ipynb
similarity index 96%
rename from samples/experimental/101.corebot-bert-bidaf/notebooks/bidaf_model_runtime.ipynb
rename to tests/experimental/101.corebot-bert-bidaf/notebooks/bidaf_model_runtime.ipynb
index 682df02c8..1b145d4af 100644
--- a/samples/experimental/101.corebot-bert-bidaf/notebooks/bidaf_model_runtime.ipynb
+++ b/tests/experimental/101.corebot-bert-bidaf/notebooks/bidaf_model_runtime.ipynb
@@ -1,228 +1,228 @@
-{
- "cells": [
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "# Test the BiDAF runtime model\n",
-    "This notebook uses the BiDAF language entitiy recognizer model.  See the [README.md](../README.md) for instructions on how to run this sample.\n",
-    "\n",
-    "## `model_corebot101` package\n",
-    "This sample creates a python package (`model_corebot101`) which contains all the code to train, evaluate and infer intent classifier for this sample.\n",
-    "\n",
-    "## See also:\n",
-    "- [The BERT training model](bert_train.ipynb) to train the intent classifier model.\n",
-    "- [The BERT runtime model](bert_model_runtime.ipynb) to test the BERT model to test the intent classifier model.\n",
-    "- [The model runtime](model_runtime.ipynb) to test the both the BERT and BiDAF model together."
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 1,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "from model_corebot101.bidaf.model_runtime.bidaf_model_runtime import BidafModelRuntime\n",
-    "import os\n",
-    "from pathlib import Path\n",
-    "from IPython.display import display\n",
-    "\n",
-    "bidaf_model_dir = os.path.abspath(os.path.join(Path.home(), \"models/bidaf\"))"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "## `BidafModelRuntime` class\n",
-    "The `BidafModelRuntime` class is used to perform the classification for entities on the bot utterances.\n",
-    "\n",
-    "The model is completely is downloaded and placed in the `/models/bidaf` directory.\n",
-    "\n",
-    "## `BidafModelRuntime.init_bidaf` method\n",
-    "The `BidafModelRuntime.init_bidaf` method downloads the necessary ONNX model.\n",
-    "\n",
-    "Output should look like the following: \n",
-    "\n",
-    "```bash\n",
-    "Creating bidaf model directory..\n",
-    "Checking file ../../bot/cognitiveModels/bidaf\\bidaf.onnx..\n",
-    "Downloading bidaf.onnx...\n",
-    "```"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 2,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stderr",
-     "output_type": "stream",
-     "text": [
-      "bidaf model directory already present..\n",
-      "[nltk_data] Downloading package punkt to\n",
-      "[nltk_data]     C:\\Users\\daveta\\models\\bidaf...\n",
-      "[nltk_data]   Package punkt is already up-to-date!\n",
-      "[nltk_data] Downloading package punkt to\n",
-      "[nltk_data]     C:\\Users\\daveta\\AppData\\Roaming\\nltk_data...\n",
-      "[nltk_data]   Package punkt is already up-to-date!\n",
-      "Checking file C:\\Users\\daveta\\models\\bidaf\\bidaf.onnx..\n",
-      "bidaf.onnx downloaded already!\n"
-     ]
-    },
-    {
-     "data": {
-      "text/plain": [
-       "'The BiDAF model successfully downloaded.'"
-      ]
-     },
-     "metadata": {},
-     "output_type": "display_data"
-    }
-   ],
-   "source": [
-    "if not BidafModelRuntime.init_bidaf(bidaf_model_dir, True):\n",
-    "    display('The BiDAF model was not downloaded successfully.  See output below for more details.')\n",
-    "else:\n",
-    "    display('The BiDAF model successfully downloaded.')"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "## `BidafModelRuntime` class\n",
-    "The `BidafModelRuntime` class is used to perform the classification against the bot utterances.\n",
-    "\n",
-    "- `targets` : an array of entities to classify.\n",
-    "- `queries` : examples passed to assist the classifier\n",
-    "- `model_dir` : path to the model\n",
-    "\n",
-    "The output should resemble the following:\n",
-    "\n",
-    "```bash\n",
-    "Loading Inference session from C:\\Users\\<>\\models\\bidaf\\bidaf.onnx..\n",
-    "Inference session loaded..\n",
-    "Processed queries..\n",
-    "```\n",
-    "\n"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 3,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stderr",
-     "output_type": "stream",
-     "text": [
-      "Loading Inference session from C:\\Users\\daveta\\models\\bidaf\\bidaf.onnx..\n",
-      "Inference session loaded..\n",
-      "Processed queries..\n"
-     ]
-    }
-   ],
-   "source": [
-    "s = BidafModelRuntime(\n",
-    "    targets=[\"from\", \"to\", \"date\"],\n",
-    "    queries={\n",
-    "        \"from\": \"which city will you travel from?\",\n",
-    "        \"to\": \"which city will you travel to?\",\n",
-    "        \"date\": \"which date will you travel?\",\n",
-    "    },\n",
-    "    model_dir = bidaf_model_dir\n",
-    ")"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 4,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "{'from': 'london', 'to': 'paris', 'date': 'feb 14th'}"
-      ]
-     },
-     "execution_count": 4,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "s.serve(\"flight to paris from london on feb 14th\")"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 5,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "{'from': 'london', 'to': 'paris', 'date': 'feb 14th'}"
-      ]
-     },
-     "execution_count": 5,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "s.serve(\"book flight from london to paris on feb 14th\")"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 6,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "{'from': 'berlin', 'to': 'berlin', 'date': '5th'}"
-      ]
-     },
-     "execution_count": 6,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "s.serve(\"fly from berlin to paris on may 5th\")"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": []
-  }
- ],
- "metadata": {
-  "kernelspec": {
-   "display_name": "botsample",
-   "language": "python",
-   "name": "botsample"
-  },
-  "language_info": {
-   "codemirror_mode": {
-    "name": "ipython",
-    "version": 3
-   },
-   "file_extension": ".py",
-   "mimetype": "text/x-python",
-   "name": "python",
-   "nbconvert_exporter": "python",
-   "pygments_lexer": "ipython3",
-   "version": "3.6.8"
-  }
- },
- "nbformat": 4,
- "nbformat_minor": 2
-}
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Test the BiDAF runtime model\n",
+    "This notebook uses the BiDAF language entitiy recognizer model.  See the [README.md](../README.md) for instructions on how to run this sample.\n",
+    "\n",
+    "## `model_corebot101` package\n",
+    "This sample creates a python package (`model_corebot101`) which contains all the code to train, evaluate and infer intent classifier for this sample.\n",
+    "\n",
+    "## See also:\n",
+    "- [The BERT training model](bert_train.ipynb) to train the intent classifier model.\n",
+    "- [The BERT runtime model](bert_model_runtime.ipynb) to test the BERT model to test the intent classifier model.\n",
+    "- [The model runtime](model_runtime.ipynb) to test the both the BERT and BiDAF model together."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from model_corebot101.bidaf.model_runtime.bidaf_model_runtime import BidafModelRuntime\n",
+    "import os\n",
+    "from pathlib import Path\n",
+    "from IPython.display import display\n",
+    "\n",
+    "bidaf_model_dir = os.path.abspath(os.path.join(Path.home(), \"models/bidaf\"))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## `BidafModelRuntime` class\n",
+    "The `BidafModelRuntime` class is used to perform the classification for entities on the bot utterances.\n",
+    "\n",
+    "The model is completely is downloaded and placed in the `/models/bidaf` directory.\n",
+    "\n",
+    "## `BidafModelRuntime.init_bidaf` method\n",
+    "The `BidafModelRuntime.init_bidaf` method downloads the necessary ONNX model.\n",
+    "\n",
+    "Output should look like the following: \n",
+    "\n",
+    "```bash\n",
+    "Creating bidaf model directory..\n",
+    "Checking file ../../bot/cognitiveModels/bidaf\\bidaf.onnx..\n",
+    "Downloading bidaf.onnx...\n",
+    "```"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "bidaf model directory already present..\n",
+      "[nltk_data] Downloading package punkt to\n",
+      "[nltk_data]     C:\\Users\\daveta\\models\\bidaf...\n",
+      "[nltk_data]   Package punkt is already up-to-date!\n",
+      "[nltk_data] Downloading package punkt to\n",
+      "[nltk_data]     C:\\Users\\daveta\\AppData\\Roaming\\nltk_data...\n",
+      "[nltk_data]   Package punkt is already up-to-date!\n",
+      "Checking file C:\\Users\\daveta\\models\\bidaf\\bidaf.onnx..\n",
+      "bidaf.onnx downloaded already!\n"
+     ]
+    },
+    {
+     "data": {
+      "text/plain": [
+       "'The BiDAF model successfully downloaded.'"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "if not BidafModelRuntime.init_bidaf(bidaf_model_dir, True):\n",
+    "    display('The BiDAF model was not downloaded successfully.  See output below for more details.')\n",
+    "else:\n",
+    "    display('The BiDAF model successfully downloaded.')"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## `BidafModelRuntime` class\n",
+    "The `BidafModelRuntime` class is used to perform the classification against the bot utterances.\n",
+    "\n",
+    "- `targets` : an array of entities to classify.\n",
+    "- `queries` : examples passed to assist the classifier\n",
+    "- `model_dir` : path to the model\n",
+    "\n",
+    "The output should resemble the following:\n",
+    "\n",
+    "```bash\n",
+    "Loading Inference session from C:\\Users\\<>\\models\\bidaf\\bidaf.onnx..\n",
+    "Inference session loaded..\n",
+    "Processed queries..\n",
+    "```\n",
+    "\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "Loading Inference session from C:\\Users\\daveta\\models\\bidaf\\bidaf.onnx..\n",
+      "Inference session loaded..\n",
+      "Processed queries..\n"
+     ]
+    }
+   ],
+   "source": [
+    "s = BidafModelRuntime(\n",
+    "    targets=[\"from\", \"to\", \"date\"],\n",
+    "    queries={\n",
+    "        \"from\": \"which city will you travel from?\",\n",
+    "        \"to\": \"which city will you travel to?\",\n",
+    "        \"date\": \"which date will you travel?\",\n",
+    "    },\n",
+    "    model_dir = bidaf_model_dir\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "{'from': 'london', 'to': 'paris', 'date': 'feb 14th'}"
+      ]
+     },
+     "execution_count": 4,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "s.serve(\"flight to paris from london on feb 14th\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "{'from': 'london', 'to': 'paris', 'date': 'feb 14th'}"
+      ]
+     },
+     "execution_count": 5,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "s.serve(\"book flight from london to paris on feb 14th\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "{'from': 'berlin', 'to': 'berlin', 'date': '5th'}"
+      ]
+     },
+     "execution_count": 6,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "s.serve(\"fly from berlin to paris on may 5th\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "botsample",
+   "language": "python",
+   "name": "botsample"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.6.8"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/samples/experimental/101.corebot-bert-bidaf/notebooks/model_runtime.ipynb b/tests/experimental/101.corebot-bert-bidaf/notebooks/model_runtime.ipynb
similarity index 95%
rename from samples/experimental/101.corebot-bert-bidaf/notebooks/model_runtime.ipynb
rename to tests/experimental/101.corebot-bert-bidaf/notebooks/model_runtime.ipynb
index 8adca2b30..4b6b71c60 100644
--- a/samples/experimental/101.corebot-bert-bidaf/notebooks/model_runtime.ipynb
+++ b/tests/experimental/101.corebot-bert-bidaf/notebooks/model_runtime.ipynb
@@ -1,206 +1,206 @@
-{
- "cells": [
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "# Test the intent classifier and entity extractor\n",
-    "This notebook uses the pretrained BiDAF model and BERT model tuned in [bert_train.ipynb notebook](bert_train.ipynb). See the [README.md](../README.md) for instructions on how to use that notebook.\n",
-    "\n",
-    "## See also:\n",
-    "- [bert_train.ipynb](bert_train.ipynb) to train the intent classifier model.\n",
-    "- [bert_model_runtime.ipynb](bert_model_runtime.ipynb) to test the BERT intent classifier model.\n",
-    "- [bidaf_model_runtime.ipynb](bidaf_model_runtime.ipynb) to test the associated BiDAF model to test the entity extractor model."
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 1,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "from model_corebot101.bert.model_runtime.bert_model_runtime import BertModelRuntime\n",
-    "from model_corebot101.bidaf.model_runtime.bidaf_model_runtime import BidafModelRuntime\n",
-    "import os\n",
-    "from pathlib import Path\n",
-    "bidaf_model_dir = os.path.abspath(os.path.join(Path.home(), \"models/bidaf\"))\n",
-    "bert_model_dir = os.path.abspath(os.path.join(Path.home(), \"models/bert\"))"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stderr",
-     "output_type": "stream",
-     "text": [
-      "bidaf model directory already present..\n"
-     ]
-    }
-   ],
-   "source": [
-    "BidafModelRuntime.init_bidaf(bidaf_model_dir, True)\n",
-    "bidaf = BidafModelRuntime(\n",
-    "    targets=[\"from\", \"to\", \"date\"],\n",
-    "    queries={\n",
-    "        \"from\": \"which city will you travel from?\",\n",
-    "        \"to\": \"which city will you travel to?\",\n",
-    "        \"date\": \"which date will you travel?\",\n",
-    "    },\n",
-    "    model_dir = bidaf_model_dir\n",
-    ")"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "bert = BertModelRuntime(model_dir=bert_model_dir, label_list=[\"Book flight\", \"Cancel\"])"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "def serve(utterance):\n",
-    "    intent = bert.serve(utterance)\n",
-    "    entities = bidaf.serve(utterance)\n",
-    "    return intent, entities"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "## `BertModelRuntime.serve` method\n",
-    "The `BertModelRuntime.serve` method is used to perform the classification against the bot utterances."
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "serve(\"flight to paris from london on feb 14th\")"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "serve(\"from seattle to san\")"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "serve(\"random random random 42\")"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "serve(\"any\")"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "serve(\"take me to New York\")"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "serve(\"we'd like to go to seattle\")"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "serve(\"not this one\")"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "serve(\"I don't care about this one\")"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "serve(\"I don't want to see that\")"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "serve(\"boring\")"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "serve(\"you have no clue how to book a flight\")"
-   ]
-  }
- ],
- "metadata": {
-  "kernelspec": {
-   "display_name": "Python 3",
-   "language": "python",
-   "name": "python3"
-  },
-  "language_info": {
-   "codemirror_mode": {
-    "name": "ipython",
-    "version": 3
-   },
-   "file_extension": ".py",
-   "mimetype": "text/x-python",
-   "name": "python",
-   "nbconvert_exporter": "python",
-   "pygments_lexer": "ipython3",
-   "version": "3.6.8"
-  }
- },
- "nbformat": 4,
- "nbformat_minor": 2
-}
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Test the intent classifier and entity extractor\n",
+    "This notebook uses the pretrained BiDAF model and BERT model tuned in [bert_train.ipynb notebook](bert_train.ipynb). See the [README.md](../README.md) for instructions on how to use that notebook.\n",
+    "\n",
+    "## See also:\n",
+    "- [bert_train.ipynb](bert_train.ipynb) to train the intent classifier model.\n",
+    "- [bert_model_runtime.ipynb](bert_model_runtime.ipynb) to test the BERT intent classifier model.\n",
+    "- [bidaf_model_runtime.ipynb](bidaf_model_runtime.ipynb) to test the associated BiDAF model to test the entity extractor model."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from model_corebot101.bert.model_runtime.bert_model_runtime import BertModelRuntime\n",
+    "from model_corebot101.bidaf.model_runtime.bidaf_model_runtime import BidafModelRuntime\n",
+    "import os\n",
+    "from pathlib import Path\n",
+    "bidaf_model_dir = os.path.abspath(os.path.join(Path.home(), \"models/bidaf\"))\n",
+    "bert_model_dir = os.path.abspath(os.path.join(Path.home(), \"models/bert\"))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "bidaf model directory already present..\n"
+     ]
+    }
+   ],
+   "source": [
+    "BidafModelRuntime.init_bidaf(bidaf_model_dir, True)\n",
+    "bidaf = BidafModelRuntime(\n",
+    "    targets=[\"from\", \"to\", \"date\"],\n",
+    "    queries={\n",
+    "        \"from\": \"which city will you travel from?\",\n",
+    "        \"to\": \"which city will you travel to?\",\n",
+    "        \"date\": \"which date will you travel?\",\n",
+    "    },\n",
+    "    model_dir = bidaf_model_dir\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "bert = BertModelRuntime(model_dir=bert_model_dir, label_list=[\"Book flight\", \"Cancel\"])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def serve(utterance):\n",
+    "    intent = bert.serve(utterance)\n",
+    "    entities = bidaf.serve(utterance)\n",
+    "    return intent, entities"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## `BertModelRuntime.serve` method\n",
+    "The `BertModelRuntime.serve` method is used to perform the classification against the bot utterances."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "serve(\"flight to paris from london on feb 14th\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "serve(\"from seattle to san\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "serve(\"random random random 42\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "serve(\"any\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "serve(\"take me to New York\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "serve(\"we'd like to go to seattle\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "serve(\"not this one\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "serve(\"I don't care about this one\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "serve(\"I don't want to see that\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "serve(\"boring\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "serve(\"you have no clue how to book a flight\")"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.6.8"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/samples/experimental/101.corebot-bert-bidaf/bot/requirements.txt b/tests/experimental/101.corebot-bert-bidaf/requirements.txt
similarity index 95%
rename from samples/experimental/101.corebot-bert-bidaf/bot/requirements.txt
rename to tests/experimental/101.corebot-bert-bidaf/requirements.txt
index 4b99b18d5..496696f2c 100644
--- a/samples/experimental/101.corebot-bert-bidaf/bot/requirements.txt
+++ b/tests/experimental/101.corebot-bert-bidaf/requirements.txt
@@ -1,41 +1,41 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-# Note: The model must be built first!
-#   cd model
-#   
-
-# The following are installed outside of requirements.txt
-#    conda install -c pytorch pytorch -y 
-#    pip install onnxruntime
-# Install python package dependencies with the following:
-# `pip install -r requirements.txt` 
-
-# Bot
-Flask>=1.0.2
-asyncio>=3.4.3
-requests>=2.18.1
-
-# Bot Framework
-botframework-connector>=4.4.0.b1
-botbuilder-schema>=4.4.0.b1
-botbuilder-core>=4.4.0.b1
-botbuilder-dialogs>=4.4.0.b1
-botbuilder-ai>=4.4.0.b1
-datatypes-date-time>=1.0.0.a1
-azure-cognitiveservices-language-luis>=0.2.0
-msrest>=0.6.6
-
-# Internal library - must be built first!
-model_corebot101>=0.0.1
-
-torch
-onnx
-onnxruntime
-tqdm>=4.32.1
-pytorch-pretrained-bert>=0.6.2
-nltk>=3.4.1
-numpy>=1.16.3
-scipy>=1.3.0
-scikit-learn>=0.21.2
-
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+# Note: The model must be built first!
+#   cd model
+#   
+
+# The following are installed outside of requirements.txt
+#    conda install -c pytorch pytorch -y 
+#    pip install onnxruntime
+# Install python package dependencies with the following:
+# `pip install -r requirements.txt` 
+
+# Bot
+Flask>=1.0.2
+asyncio>=3.4.3
+requests>=2.18.1
+
+# Bot Framework
+botframework-connector>=4.4.0.b1
+botbuilder-schema>=4.4.0.b1
+botbuilder-core>=4.4.0.b1
+botbuilder-dialogs>=4.4.0.b1
+botbuilder-ai>=4.4.0.b1
+datatypes-date-time>=1.0.0.a1
+azure-cognitiveservices-language-luis>=0.2.0
+msrest>=0.6.6
+
+# Internal library - must be built first!
+model_corebot101>=0.0.1
+
+torch
+onnx
+onnxruntime
+tqdm>=4.32.1
+pytorch-pretrained-bert>=0.6.2
+nltk>=3.4.1
+numpy>=1.16.3
+scipy>=1.3.0
+scikit-learn>=0.21.2
+
diff --git a/tests/experimental/sso/child/adapter_with_error_handler.py b/tests/experimental/sso/child/adapter_with_error_handler.py
new file mode 100644
index 000000000..6eb8e230b
--- /dev/null
+++ b/tests/experimental/sso/child/adapter_with_error_handler.py
@@ -0,0 +1,64 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+import sys
+import traceback
+from datetime import datetime
+
+from botbuilder.core import (
+    BotFrameworkAdapter,
+    BotFrameworkAdapterSettings,
+    ConversationState,
+    UserState,
+    TurnContext,
+)
+from botbuilder.schema import ActivityTypes, Activity
+
+
+class AdapterWithErrorHandler(BotFrameworkAdapter):
+    def __init__(
+        self,
+        settings: BotFrameworkAdapterSettings,
+        conversation_state: ConversationState,
+        user_state: UserState,
+    ):
+        super().__init__(settings)
+        self._conversation_state = conversation_state
+        self._user_state = user_state
+
+        # Catch-all for errors.
+        async def on_error(context: TurnContext, error: Exception):
+            # This check writes out errors to console log
+            # 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.")
+            await context.send_activity(
+                "To continue to run this bot, please fix the bot source code."
+            )
+            # Send a trace activity if we're talking to the Bot Framework Emulator
+            if context.activity.channel_id == "emulator":
+                # Create a trace activity that contains the error object
+                trace_activity = Activity(
+                    label="TurnError",
+                    name="on_turn_error Trace",
+                    timestamp=datetime.utcnow(),
+                    type=ActivityTypes.trace,
+                    value=f"{error}",
+                    value_type="https://www.botframework.com/schemas/error",
+                )
+                # Send a trace activity, which will be displayed in Bot Framework Emulator
+                await context.send_activity(trace_activity)
+
+            # Clear out state
+            nonlocal self
+            await self._conversation_state.delete(context)
+
+        self.on_turn_error = on_error
+    
+    async def send_activities(self, context, activities):
+        await self._conversation_state.save_changes(context)
+        await self._user_state.save_changes(context)
+        return await super().send_activities(context, activities)
\ No newline at end of file
diff --git a/tests/experimental/sso/child/app.py b/tests/experimental/sso/child/app.py
new file mode 100644
index 000000000..03774b27a
--- /dev/null
+++ b/tests/experimental/sso/child/app.py
@@ -0,0 +1,105 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import logging
+import sys
+import traceback
+from datetime import datetime
+
+from aiohttp import web
+from aiohttp.web import Request, Response
+from aiohttp.web_response import json_response
+from botbuilder.core import (
+    BotFrameworkAdapterSettings,
+    ConversationState,
+    MemoryStorage,
+    UserState,
+    TurnContext,
+    BotFrameworkAdapter,
+)
+from botbuilder.schema import Activity, ActivityTypes
+
+from adapter_with_error_handler import AdapterWithErrorHandler
+from bots import ChildBot
+from dialogs import MainDialog
+from config import DefaultConfig
+
+CONFIG = DefaultConfig()
+
+STORAGE = MemoryStorage()
+
+CONVERSATION_STATE = ConversationState(STORAGE)
+USER_STATE = UserState(STORAGE)
+
+# Create adapter.
+# See https://aka.ms/about-bot-adapter to learn more about how bots work.
+SETTINGS = BotFrameworkAdapterSettings(CONFIG.APP_ID, CONFIG.APP_PASSWORD)
+ADAPTER = AdapterWithErrorHandler(SETTINGS, CONVERSATION_STATE, USER_STATE)
+
+
+# Catch-all for errors.
+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.")
+    await context.send_activity(
+        "To continue to run this bot, please fix the bot source code."
+    )
+    # Send a trace activity if we're talking to the Bot Framework Emulator
+    if context.activity.channel_id == "emulator":
+        # Create a trace activity that contains the error object
+        trace_activity = Activity(
+            label="TurnError",
+            name="on_turn_error Trace",
+            timestamp=datetime.utcnow(),
+            type=ActivityTypes.trace,
+            value=f"{error}",
+            value_type="https://www.botframework.com/schemas/error",
+        )
+        # Send a trace activity, which will be displayed in Bot Framework Emulator
+        await context.send_activity(trace_activity)
+
+
+ADAPTER.on_turn_error = on_error
+
+DIALOG = MainDialog(CONFIG)
+
+# Create the Bot
+BOT = ChildBot(DIALOG, USER_STATE, CONVERSATION_STATE, CONFIG)
+
+# Listen for incoming requests on /api/messages
+async def messages(req: Request) -> Response:
+    # Main bot message handler.
+    if "application/json" in req.headers["Content-Type"]:
+        body = await req.json()
+    else:
+        return Response(status=415)
+
+    activity = Activity().deserialize(body)
+    auth_header = req.headers["Authorization"] if "Authorization" in req.headers else ""
+
+    try:
+        response = await ADAPTER.process_activity(activity, auth_header, BOT.on_turn)
+        if response:
+            return json_response(data=response.body, status=response.status)
+        return Response(status=201)
+    except Exception as exception:
+        raise exception
+
+"""async def options(req: Request) -> Response:
+    return Response(status=200)"""
+
+APP = web.Application()
+APP.router.add_post("/api/messages", messages)
+
+if __name__ == "__main__":
+    try:
+        logging.basicConfig(level=logging.DEBUG)
+        web.run_app(APP, host="localhost", port=CONFIG.PORT)
+    except Exception as error:
+        raise error
diff --git a/tests/experimental/sso/child/bots/__init__.py b/tests/experimental/sso/child/bots/__init__.py
new file mode 100644
index 000000000..aa82cac78
--- /dev/null
+++ b/tests/experimental/sso/child/bots/__init__.py
@@ -0,0 +1,4 @@
+from .child_bot import ChildBot
+
+
+__all__ = ["ChildBot"]
diff --git a/tests/experimental/sso/child/bots/child_bot.py b/tests/experimental/sso/child/bots/child_bot.py
new file mode 100644
index 000000000..df60e6fe1
--- /dev/null
+++ b/tests/experimental/sso/child/bots/child_bot.py
@@ -0,0 +1,73 @@
+from typing import List
+
+from botbuilder.core import (
+    ActivityHandler,
+    BotFrameworkAdapter,
+    ConversationState,
+    UserState,
+    MessageFactory,
+    TurnContext,
+)
+from botbuilder.dialogs import DialogState
+from botframework.connector.auth import MicrosoftAppCredentials
+
+from config import DefaultConfig
+from helpers.dialog_helper import DialogHelper
+from dialogs import MainDialog
+
+
+class ChildBot(ActivityHandler):
+    def __init__(
+        self,
+        dialog: MainDialog,
+        user_state: UserState,
+        conversation_state: ConversationState,
+        config: DefaultConfig,
+    ):
+        self._user_state = user_state
+        self._conversation_state = conversation_state
+        self._dialog = dialog
+        self._connection_name = config.CONNECTION_NAME
+        self._config = config
+
+    async def on_turn(self, turn_context: TurnContext):
+        await super().on_turn(turn_context)
+
+        await self._conversation_state.save_changes(turn_context)
+        await self._user_state.save_changes(turn_context)
+
+    async def on_sign_in_invoke(  # pylint: disable=unused-argument
+        self, turn_context: TurnContext
+    ):
+        await self._conversation_state.load(turn_context, True)
+        await self._user_state.load(turn_context, True)
+        await DialogHelper.run_dialog(
+            self._dialog,
+            turn_context,
+            self._conversation_state.create_property(DialogState.__name__)
+        )
+
+    async def on_message_activity(self, turn_context: TurnContext):
+        if turn_context.activity.channel_id != "emulator":
+            if "skill login" in turn_context.activity.text:
+                await self._conversation_state.load(turn_context, True)
+                await self._user_state.load(turn_context, True)
+                await DialogHelper.run_dialog(
+                    self._dialog,
+                    turn_context,
+                    self._conversation_state.create_property(DialogState.__name__)
+                )
+                return
+            elif "skill logout" in turn_context.activity.text:
+                adapter: BotFrameworkAdapter = turn_context.adapter
+                await adapter.sign_out_user(
+                    turn_context,
+                    self._connection_name,
+                    turn_context.activity.from_property.id,
+                    MicrosoftAppCredentials(self._config.APP_ID, self._config.APP_PASSWORD))
+                await turn_context.send_activity(MessageFactory.text("logout from child bot successful"))
+        else:
+            await turn_context.send_activity(MessageFactory.text("child: activity (1)"))
+            await turn_context.send_activity(MessageFactory.text("child: activity (2)"))
+            await turn_context.send_activity(MessageFactory.text("child: activity (3)"))
+            await turn_context.send_activity(MessageFactory.text(f"child: {turn_context.activity.text}"))
diff --git a/tests/experimental/sso/child/config.py b/tests/experimental/sso/child/config.py
new file mode 100644
index 000000000..e7e7e320f
--- /dev/null
+++ b/tests/experimental/sso/child/config.py
@@ -0,0 +1,16 @@
+#!/usr/bin/env python3
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+
+""" Bot Configuration """
+
+
+class DefaultConfig:
+    """ Bot Configuration """
+
+    PORT = 3979
+    APP_ID = os.environ.get("MicrosoftAppId", "")
+    APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "")
+    CONNECTION_NAME = ""
diff --git a/tests/experimental/sso/child/dialogs/__init__.py b/tests/experimental/sso/child/dialogs/__init__.py
new file mode 100644
index 000000000..9a834bd37
--- /dev/null
+++ b/tests/experimental/sso/child/dialogs/__init__.py
@@ -0,0 +1,5 @@
+from .main_dialog import MainDialog
+
+__all__ = [
+    "MainDialog"
+]
diff --git a/tests/experimental/sso/child/dialogs/main_dialog.py b/tests/experimental/sso/child/dialogs/main_dialog.py
new file mode 100644
index 000000000..d3f070ed5
--- /dev/null
+++ b/tests/experimental/sso/child/dialogs/main_dialog.py
@@ -0,0 +1,55 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.dialogs import (
+    ComponentDialog,
+    DialogTurnResult,
+    OAuthPrompt,
+    OAuthPromptSettings,
+    WaterfallDialog,
+    WaterfallStepContext
+)
+from botbuilder.schema import TokenResponse
+from botbuilder.core import MessageFactory
+from botframework.connector.auth import MicrosoftAppCredentials
+
+from config import DefaultConfig
+
+
+class MainDialog(ComponentDialog):
+    def __init__(self, config: DefaultConfig):
+        super(MainDialog, self).__init__(MainDialog.__name__)
+
+        self.connection_name = config.CONNECTION_NAME
+        self.add_dialog(
+            WaterfallDialog(
+                WaterfallDialog.__name__,
+                [self.sign_in_step, self.show_token_response]
+            )
+        )
+        self.add_dialog(
+            OAuthPrompt(
+                OAuthPrompt.__name__,
+                OAuthPromptSettings(
+                    connection_name=self.connection_name,
+                    text="Sign In to AAD",
+                    title="Sign In",
+                    oauth_app_credentials=MicrosoftAppCredentials(
+                        app_id=config.APP_ID,
+                        password=config.APP_PASSWORD
+                    )
+                )
+            )
+        )
+
+    async def sign_in_step(self, context: WaterfallStepContext) -> DialogTurnResult:
+        return await context.begin_dialog(OAuthPrompt.__name__)
+
+    async def show_token_response(self, context: WaterfallStepContext) -> DialogTurnResult:
+        result: TokenResponse = context.result
+        if not result:
+            await context.context.send_activity(MessageFactory.text("Skill: No token response from OAuthPrompt"))
+        else:
+            await context.context.send_activity(MessageFactory.text(f"Skill: Your token is {result.token}"))
+
+        return await context.end_dialog()
diff --git a/samples/06.using-cards/dialogs/resources/__init__.py b/tests/experimental/sso/child/helpers/__init__.py
similarity index 57%
rename from samples/06.using-cards/dialogs/resources/__init__.py
rename to tests/experimental/sso/child/helpers/__init__.py
index 7569a0e37..a824eb8f4 100644
--- a/samples/06.using-cards/dialogs/resources/__init__.py
+++ b/tests/experimental/sso/child/helpers/__init__.py
@@ -1,6 +1,6 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the MIT License.
 
-from . import adaptive_card_example
+from . import dialog_helper
 
-__all__ = ["adaptive_card_example"]
+__all__ = ["dialog_helper"]
diff --git a/samples/06.using-cards/helpers/dialog_helper.py b/tests/experimental/sso/child/helpers/dialog_helper.py
similarity index 100%
rename from samples/06.using-cards/helpers/dialog_helper.py
rename to tests/experimental/sso/child/helpers/dialog_helper.py
diff --git a/tests/experimental/sso/parent/ReadMeForSSOTesting.md b/tests/experimental/sso/parent/ReadMeForSSOTesting.md
new file mode 100644
index 000000000..a5009494d
--- /dev/null
+++ b/tests/experimental/sso/parent/ReadMeForSSOTesting.md
@@ -0,0 +1,37 @@
+This guide documents how to configure and test SSO by using the parent and child bot projects.
+## SetUp
+- Go to [App registrations page on Azure Portal](https://ms.portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps)
+- You need to create 2 AAD apps (one for the parent bot and one for the skill)
+### Parent bot AAD app
+- Click "New Registration"
+- Enter name, set "supported account types" as Single Tenant, Redirect URI as https://token.botframework.com/.auth/web/redirect
+- Go to "Expose an API". Click "Add a Scope". Enter a scope name (like "scope1"), set "who can consent" to Admins and users, display name, description and click "Add Scope" . Copy the value of the scope that you just added (should be like "api://{clientId}/scopename")
+- Go to "Manifest" tab and set `accessTokenAcceptedVersion` to 2
+- Go to "Certificates and secrets" , click "new client secret" and store the generated secret.
+
+### Configuring the Parent Bot Channel Registration
+- Create a new Bot Channel Registration. You can leave the messaging endpoint empty and later fill an ngrok endpoint for it.
+- Go to settings tab, click "Add Setting" and enter a name, set Service Provider to "Azure Active Directory v2".
+- Fill in ClientId, TenantId from the parent bot AAD app you created (look at the overview tab for these values)
+- Fill in the secret from the parent bot AAD app.
+- Fill in the scope that you copied earlier ("api://{clientId}/scopename") and enter it for "Scopes" on the OAuth connection. Click Save.
+
+### Child bot AAD app and BCR
+- Follow the steps in the [documentation](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-authentication?view=azure-bot-service-4.0&tabs=csharp) for creating an Azure AD v2 app and filling those values in a Bot Channel Registration.
+- Go to the Azure AD app that you created in the step above.
+- Go to "Manifest" tab and set `accessTokenAcceptedVersion` to 2
+- Go to "Expose an API". Click "Add a client application". Enter the clientId of the parent bot AAD app.
+- Go to "Expose an API". Click "Add a Scope". Enter a scope name (like "scope1"), set "who can consent" to Admins and users, display name, description and click "Add Scope" . Copy the value of the scope that you just added (should be like "api://{clientId}/scopename")
+- Go back to your BCR that you created for the child bot. Go to Auth Connections in the settings blade and click on the auth connection that you created earlier. For the "Token Exchange Uri" , set the scope value that you copied in the step above.
+
+### Running and Testing
+- Configure appid, passoword and connection names in the appsettings.json files for both parent and child bots. Run both the projects.
+- Set up ngrok to expose the url for the parent bot. (Child bot can run just locally, as long as it's on the same machine as the parent bot.)
+- Configure the messaging endpoint for the parent bot channel registration with the ngrok url and go to "test in webchat" tab.
+- Run the following commands and look at the outputs
+    - login - shows an oauth card. Click the oauth card to login into the parent bot.
+    - type "login" again - shows your JWT token.
+    - skill login - should do nothing (no oauth card shown).
+    - type "skill login" again - should show you a message from the skill with the token.
+    - logout - should give a message that you have been logged out from the parent bot.
+    - skill logout - should give a message that you have been logged out from the child bot.
diff --git a/tests/experimental/sso/parent/app.py b/tests/experimental/sso/parent/app.py
new file mode 100644
index 000000000..aea35bc34
--- /dev/null
+++ b/tests/experimental/sso/parent/app.py
@@ -0,0 +1,108 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import logging
+import sys
+import traceback
+from datetime import datetime
+
+from aiohttp import web
+from aiohttp.web import Request, Response
+from botbuilder.core import (
+    BotFrameworkAdapterSettings,
+    BotFrameworkHttpClient,
+    ConversationState,
+    MemoryStorage,
+    TurnContext,
+    UserState,
+    BotFrameworkAdapter,
+)
+from botbuilder.core.integration import (
+    aiohttp_error_middleware,
+)
+from botbuilder.schema import Activity, ActivityTypes
+from botframework.connector.auth import (
+    SimpleCredentialProvider,
+)
+from bots import ParentBot
+from config import DefaultConfig
+from dialogs import MainDialog
+
+CONFIG = DefaultConfig()
+
+STORAGE = MemoryStorage()
+
+CONVERSATION_STATE = ConversationState(STORAGE)
+USER_STATE = UserState(STORAGE)
+
+CREDENTIAL_PROVIDER = SimpleCredentialProvider(CONFIG.APP_ID, CONFIG.APP_PASSWORD)
+CLIENT = BotFrameworkHttpClient(CREDENTIAL_PROVIDER)
+
+# Create adapter.
+# See https://aka.ms/about-bot-adapter to learn more about how bots work.
+SETTINGS = BotFrameworkAdapterSettings(CONFIG.APP_ID, CONFIG.APP_PASSWORD)
+ADAPTER = BotFrameworkAdapter(SETTINGS)
+
+
+# Catch-all for errors.
+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.")
+    await context.send_activity(
+        "To continue to run this bot, please fix the bot source code."
+    )
+    # Send a trace activity if we're talking to the Bot Framework Emulator
+    if context.activity.channel_id == "emulator":
+        # Create a trace activity that contains the error object
+        trace_activity = Activity(
+            label="TurnError",
+            name="on_turn_error Trace",
+            timestamp=datetime.utcnow(),
+            type=ActivityTypes.trace,
+            value=f"{error}",
+            value_type="https://www.botframework.com/schemas/error",
+        )
+        # Send a trace activity, which will be displayed in Bot Framework Emulator
+        await context.send_activity(trace_activity)
+
+
+ADAPTER.on_turn_error = on_error
+
+DIALOG = MainDialog(CONFIG)
+# Create the Bot
+BOT = ParentBot(CLIENT, CONFIG, DIALOG, CONVERSATION_STATE, USER_STATE)
+
+
+# Listen for incoming requests on /api/messages
+async def messages(req: Request) -> Response:
+    # Main bot message handler.
+    if "application/json" in req.headers["Content-Type"]:
+        body = await req.json()
+    else:
+        return Response(status=415)
+
+    activity = Activity().deserialize(body)
+    auth_header = req.headers["Authorization"] if "Authorization" in req.headers else ""
+
+    try:
+        await ADAPTER.process_activity(activity, auth_header, BOT.on_turn)
+        return Response(status=201)
+    except Exception as exception:
+        raise exception
+
+
+APP = web.Application(middlewares=[aiohttp_error_middleware])
+APP.router.add_post("/api/messages", messages)
+
+if __name__ == "__main__":
+    try:
+        logging.basicConfig(level=logging.DEBUG)
+        web.run_app(APP, host="localhost", port=CONFIG.PORT)
+    except Exception as error:
+        raise error
diff --git a/tests/experimental/sso/parent/bots/__init__.py b/tests/experimental/sso/parent/bots/__init__.py
new file mode 100644
index 000000000..ab6c7b715
--- /dev/null
+++ b/tests/experimental/sso/parent/bots/__init__.py
@@ -0,0 +1,4 @@
+from .parent_bot import ParentBot
+
+
+__all__ = ["ParentBot"]
diff --git a/tests/experimental/sso/parent/bots/parent_bot.py b/tests/experimental/sso/parent/bots/parent_bot.py
new file mode 100644
index 000000000..fe9abbc2c
--- /dev/null
+++ b/tests/experimental/sso/parent/bots/parent_bot.py
@@ -0,0 +1,231 @@
+from uuid import uuid4
+
+from datetime import datetime
+from http import HTTPStatus
+from typing import List
+
+from botbuilder.core import (
+    ActivityHandler,
+    BotFrameworkAdapter,
+    BotFrameworkHttpClient,
+    CardFactory,
+    ConversationState,
+    UserState,
+    MessageFactory,
+    TurnContext,
+)
+from botbuilder.schema import (
+    Activity,
+    ActivityTypes,
+    ConversationAccount,
+    DeliveryModes,
+    ChannelAccount,
+    OAuthCard,
+    TokenExchangeInvokeRequest,
+)
+from botframework.connector.token_api.models import (
+    TokenExchangeResource,
+    TokenExchangeRequest,
+)
+
+from config import DefaultConfig
+from helpers.dialog_helper import DialogHelper
+from dialogs import MainDialog
+
+
+class ParentBot(ActivityHandler):
+    def __init__(
+        self,
+        skill_client: BotFrameworkHttpClient,
+        config: DefaultConfig,
+        dialog: MainDialog,
+        conversation_state: ConversationState,
+        user_state: UserState,
+    ):
+        self._client = skill_client
+        self._conversation_state = conversation_state
+        self._user_state = user_state
+        self._dialog = dialog
+        self._from_bot_id = config.APP_ID
+        self._to_bot_id = config.SKILL_MICROSOFT_APP_ID
+        self._connection_name = config.CONNECTION_NAME
+
+    async def on_turn(self, turn_context: TurnContext):
+        await super().on_turn(turn_context)
+
+        await self._conversation_state.save_changes(turn_context)
+        await self._user_state.save_changes(turn_context)
+
+    async def on_message_activity(self, turn_context: TurnContext):
+        # for signin, just use an oauth prompt to get the exchangeable token
+        # also ensure that the channelId is not emulator
+        if turn_context.activity.type != "emulator":
+            if (
+                turn_context.activity.text == "login"
+                or turn_context.activity.text.isdigit()
+            ):
+                await self._conversation_state.load(turn_context, True)
+                await self._user_state.load(turn_context, True)
+                await DialogHelper.run_dialog(
+                    self._dialog,
+                    turn_context,
+                    self._conversation_state.create_property("DialogState"),
+                )
+            elif turn_context.activity.text == "logout":
+                bot_adapter = turn_context.adapter
+                await bot_adapter.sign_out_user(turn_context, self._connection_name)
+                await turn_context.send_activity(
+                    MessageFactory.text("You have been signed out.")
+                )
+            elif turn_context.activity.text in ("skill login", "skill logout"):
+                # incoming activity needs to be cloned for buffered replies
+                clone_activity = MessageFactory.text(turn_context.activity.text)
+
+                TurnContext.apply_conversation_reference(
+                    clone_activity,
+                    TurnContext.get_conversation_reference(turn_context.activity),
+                    True,
+                )
+
+                clone_activity.delivery_mode = DeliveryModes.expect_replies
+
+                activities = await self._client.post_buffered_activity(
+                    self._from_bot_id,
+                    self._to_bot_id,
+                    "http://localhost:3979/api/messages",
+                    "http://tempuri.org/whatever",
+                    turn_context.activity.conversation.id,
+                    clone_activity,
+                )
+
+                if activities:
+                    if not await self._intercept_oauth_cards(
+                        activities, turn_context
+                    ):
+                        await turn_context.send_activities(activities)
+
+            return
+
+        await turn_context.send_activity(MessageFactory.text("parent: before child"))
+
+        activity = MessageFactory.text("parent: before child")
+        TurnContext.apply_conversation_reference(
+            activity,
+            TurnContext.get_conversation_reference(turn_context.activity),
+            True,
+        )
+        activity.delivery_mode = DeliveryModes.expect_replies
+
+        activities = await self._client.post_buffered_activity(
+            self._from_bot_id,
+            self._to_bot_id,
+            "http://localhost:3979/api/messages",
+            "http://tempuri.org/whatever",
+            str(uuid4()),
+            activity,
+        )
+
+        await turn_context.send_activities(activities)
+        await turn_context.send_activity(MessageFactory.text("parent: after child"))
+
+    async def on_members_added_activity(
+        self, members_added: List[ChannelAccount], turn_context: TurnContext
+    ):
+        for member in members_added:
+            if member.id != turn_context.activity.recipient.id:
+                await turn_context.send_activity(
+                    MessageFactory.text("Hello and welcome!")
+                )
+
+    async def _intercept_oauth_cards(
+        self, activities: List[Activity], turn_context: TurnContext,
+    ) -> bool:
+        if not activities:
+            return False
+        activity = activities[0]
+
+        if activity.attachments:
+            for attachment in filter(
+                lambda att: att.content_type == CardFactory.content_types.oauth_card,
+                activity.attachments,
+            ):
+                oauth_card: OAuthCard = OAuthCard().from_dict(attachment.content)
+                oauth_card.token_exchange_resource: TokenExchangeResource = TokenExchangeResource().from_dict(
+                    oauth_card.token_exchange_resource
+                )
+                if oauth_card.token_exchange_resource:
+                    token_exchange_provider: BotFrameworkAdapter = turn_context.adapter
+
+                    result = await token_exchange_provider.exchange_token(
+                        turn_context,
+                        self._connection_name,
+                        turn_context.activity.from_property.id,
+                        TokenExchangeRequest(
+                            uri=oauth_card.token_exchange_resource.uri
+                        ),
+                    )
+
+                    if result.token:
+                        return await self._send_token_exchange_invoke_to_skill(
+                            turn_context,
+                            activity,
+                            oauth_card.token_exchange_resource.id,
+                            result.token,
+                        )
+        return False
+
+    async def _send_token_exchange_invoke_to_skill(
+        self,
+        turn_context: TurnContext,
+        incoming_activity: Activity,
+        identifier: str,
+        token: str,
+    ) -> bool:
+        activity = self._create_reply(incoming_activity)
+        activity.type = ActivityTypes.invoke
+        activity.name = "signin/tokenExchange"
+        activity.value = TokenExchangeInvokeRequest(id=identifier, token=token,)
+
+        # route the activity to the skill
+        response = await self._client.post_activity(
+            self._from_bot_id,
+            self._to_bot_id,
+            "http://localhost:3979/api/messages",
+            "http://tempuri.org/whatever",
+            incoming_activity.conversation.id,
+            activity,
+        )
+
+        # Check response status: true if success, false if failure
+        is_success = int(HTTPStatus.OK) <= response.status <= 299
+        message = (
+            "Skill token exchange successful"
+            if is_success
+            else "Skill token exchange failed"
+        )
+
+        await turn_context.send_activity(MessageFactory.text(message))
+
+        return is_success
+
+    def _create_reply(self, activity) -> Activity:
+        return Activity(
+            type=ActivityTypes.message,
+            timestamp=datetime.utcnow(),
+            from_property=ChannelAccount(
+                id=activity.recipient.id, name=activity.recipient.name
+            ),
+            recipient=ChannelAccount(
+                id=activity.from_property.id, name=activity.from_property.name
+            ),
+            reply_to_id=activity.id,
+            service_url=activity.service_url,
+            channel_id=activity.channel_id,
+            conversation=ConversationAccount(
+                is_group=activity.conversation.is_group,
+                id=activity.conversation.id,
+                name=activity.conversation.name,
+            ),
+            text="",
+            locale=activity.locale,
+        )
diff --git a/samples/06.using-cards/config.py b/tests/experimental/sso/parent/config.py
similarity index 58%
rename from samples/06.using-cards/config.py
rename to tests/experimental/sso/parent/config.py
index 83f1bbbdf..88bbf313c 100644
--- a/samples/06.using-cards/config.py
+++ b/tests/experimental/sso/parent/config.py
@@ -3,6 +3,8 @@
 # Licensed under the MIT License.
 
 import os
+from typing import Dict
+from botbuilder.core.skills import BotFrameworkSkill
 
 """ Bot Configuration """
 
@@ -13,7 +15,5 @@ class DefaultConfig:
     PORT = 3978
     APP_ID = os.environ.get("MicrosoftAppId", "")
     APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "")
-    LUIS_APP_ID = os.environ.get("LuisAppId", "")
-    LUIS_API_KEY = os.environ.get("LuisAPIKey", "")
-    # LUIS endpoint host name, ie "westus.api.cognitive.microsoft.com"
-    LUIS_API_HOST_NAME = os.environ.get("LuisAPIHostName", "")
+    CONNECTION_NAME = ""
+    SKILL_MICROSOFT_APP_ID = ""
diff --git a/tests/experimental/sso/parent/dialogs/__init__.py b/tests/experimental/sso/parent/dialogs/__init__.py
new file mode 100644
index 000000000..9a834bd37
--- /dev/null
+++ b/tests/experimental/sso/parent/dialogs/__init__.py
@@ -0,0 +1,5 @@
+from .main_dialog import MainDialog
+
+__all__ = [
+    "MainDialog"
+]
diff --git a/tests/experimental/sso/parent/dialogs/main_dialog.py b/tests/experimental/sso/parent/dialogs/main_dialog.py
new file mode 100644
index 000000000..58787e0bd
--- /dev/null
+++ b/tests/experimental/sso/parent/dialogs/main_dialog.py
@@ -0,0 +1,56 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.dialogs import (
+    ComponentDialog,
+    DialogTurnResult,
+    WaterfallDialog,
+    WaterfallStepContext
+)
+from botbuilder.dialogs.prompts import (
+    OAuthPrompt,
+    OAuthPromptSettings
+)
+from botbuilder.schema import TokenResponse
+from botbuilder.core import MessageFactory
+
+from config import DefaultConfig
+
+
+class MainDialog(ComponentDialog):
+    def __init__(self, configuration: DefaultConfig):
+        super().__init__(MainDialog.__name__)
+
+        self._connection_name = configuration.CONNECTION_NAME
+
+        self.add_dialog(
+            OAuthPrompt(
+                OAuthPrompt.__name__,
+                OAuthPromptSettings(
+                    connection_name=self._connection_name,
+                    text=f"Sign In to AAD",
+                    title="Sign In",
+                ),
+            )
+        )
+
+        self.add_dialog(
+            WaterfallDialog(
+                WaterfallDialog.__name__, [self._sign_in_step, self._show_token_response]
+            )
+        )
+
+        self.initial_dialog_id = WaterfallDialog.__name__
+
+    async def _sign_in_step(self, context: WaterfallStepContext) -> DialogTurnResult:
+        return await context.begin_dialog(OAuthPrompt.__name__)
+
+    async def _show_token_response(self, context: WaterfallStepContext) -> DialogTurnResult:
+        result: TokenResponse = context.result
+
+        if not result:
+            await context.context.send_activity(MessageFactory.text("No token response from OAuthPrompt"))
+        else:
+            await context.context.send_activity(MessageFactory.text(f"Your token is {result.token}"))
+
+        return await context.end_dialog()
diff --git a/tests/experimental/sso/parent/helpers/__init__.py b/tests/experimental/sso/parent/helpers/__init__.py
new file mode 100644
index 000000000..a824eb8f4
--- /dev/null
+++ b/tests/experimental/sso/parent/helpers/__init__.py
@@ -0,0 +1,6 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from . import dialog_helper
+
+__all__ = ["dialog_helper"]
diff --git a/samples/13.core-bot/helpers/dialog_helper.py b/tests/experimental/sso/parent/helpers/dialog_helper.py
similarity index 100%
rename from samples/13.core-bot/helpers/dialog_helper.py
rename to tests/experimental/sso/parent/helpers/dialog_helper.py
diff --git a/tests/experimental/sso/parent/skill_client.py b/tests/experimental/sso/parent/skill_client.py
new file mode 100644
index 000000000..ae43cc339
--- /dev/null
+++ b/tests/experimental/sso/parent/skill_client.py
@@ -0,0 +1,30 @@
+from botbuilder.core import BotFrameworkHttpClient, InvokeResponse, TurnContext
+from botbuilder.core.skills import BotFrameworkSkill, ConversationIdFactoryBase
+from botbuilder.schema import Activity
+
+
+class SkillHttpClient(BotFrameworkHttpClient):
+    def __init__(self, credential_provider, conversation_id_factory, channel_provider=None):
+        super().__init__(credential_provider, channel_provider)
+
+        self._conversation_id_factory: ConversationIdFactoryBase = conversation_id_factory
+
+    async def post_activity_to_skill(
+        self,
+        from_bot_id: str,
+        to_skill: BotFrameworkSkill,
+        callback_url: str,
+        activity: Activity,
+    ) -> InvokeResponse:
+        skill_conversation_id = await self._conversation_id_factory.create_skill_conversation_id(
+            TurnContext.get_conversation_reference(activity)
+        )
+
+        return await self.post_activity(
+            from_bot_id,
+            to_skill.app_id,
+            to_skill.skill_endpoint,
+            callback_url,
+            skill_conversation_id,
+            activity
+        )
diff --git a/tests/experimental/test-protocol/app.py b/tests/experimental/test-protocol/app.py
new file mode 100644
index 000000000..e890718e7
--- /dev/null
+++ b/tests/experimental/test-protocol/app.py
@@ -0,0 +1,55 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from aiohttp import web
+from aiohttp.web import Request, Response
+
+from botframework.connector.auth import AuthenticationConfiguration, SimpleCredentialProvider
+from botbuilder.core.integration import aiohttp_channel_service_routes, BotFrameworkHttpClient
+from botbuilder.schema import Activity
+
+from config import DefaultConfig
+from routing_id_factory import RoutingIdFactory
+from routing_handler import RoutingHandler
+
+
+CONFIG = DefaultConfig()
+CREDENTIAL_PROVIDER = SimpleCredentialProvider(CONFIG.APP_ID, CONFIG.APP_PASSWORD)
+CLIENT = BotFrameworkHttpClient(CREDENTIAL_PROVIDER)
+AUTH_CONFIG = AuthenticationConfiguration()
+
+TO_URI = CONFIG.NEXT
+SERVICE_URL = CONFIG.SERVICE_URL
+
+FACTORY = RoutingIdFactory()
+
+ROUTING_HANDLER = RoutingHandler(FACTORY, CREDENTIAL_PROVIDER, AUTH_CONFIG)
+
+
+async def messages(req: Request) -> Response:
+    # Main bot message handler.
+    if "application/json" in req.headers["Content-Type"]:
+        body = await req.json()
+    else:
+        return Response(status=415)
+
+    inbound_activity: Activity = Activity().deserialize(body)
+
+    current_conversation_id = inbound_activity.conversation.id
+    current_service_url = inbound_activity.service_url
+
+    next_conversation_id = FACTORY.create_skill_conversation_id(current_conversation_id, current_service_url)
+
+    await CLIENT.post_activity(CONFIG.APP_ID, CONFIG.SKILL_APP_ID, TO_URI, SERVICE_URL, next_conversation_id, inbound_activity)
+    return Response(status=201)
+
+APP = web.Application()
+
+APP.router.add_post("/api/messages", messages)
+APP.router.add_routes(aiohttp_channel_service_routes(ROUTING_HANDLER, "/api/connector"))
+
+if __name__ == "__main__":
+    try:
+        web.run_app(APP, host="localhost", port=CONFIG.PORT)
+    except Exception as error:
+        raise error
diff --git a/tests/experimental/test-protocol/config.py b/tests/experimental/test-protocol/config.py
new file mode 100644
index 000000000..a6a419f17
--- /dev/null
+++ b/tests/experimental/test-protocol/config.py
@@ -0,0 +1,18 @@
+#!/usr/bin/env python3
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+
+""" Bot Configuration """
+
+
+class DefaultConfig:
+    """ Bot Configuration """
+
+    PORT = 3428
+    APP_ID = os.environ.get("MicrosoftAppId", "")
+    APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "")
+    NEXT = "http://localhost:3978/api/messages"
+    SERVICE_URL = "http://localhost:3428/api/connector"
+    SKILL_APP_ID = ""
diff --git a/tests/experimental/test-protocol/routing_handler.py b/tests/experimental/test-protocol/routing_handler.py
new file mode 100644
index 000000000..8d13b45a2
--- /dev/null
+++ b/tests/experimental/test-protocol/routing_handler.py
@@ -0,0 +1,134 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from typing import List
+
+from botbuilder.core import ChannelServiceHandler
+from botbuilder.schema import (
+    Activity,
+    ChannelAccount,
+    ConversationParameters,
+    ConversationResourceResponse,
+    ConversationsResult,
+    PagedMembersResult,
+    ResourceResponse
+)
+from botframework.connector.aio import ConnectorClient
+from botframework.connector.auth import (
+    AuthenticationConfiguration,
+    ChannelProvider,
+    ClaimsIdentity,
+    CredentialProvider,
+    MicrosoftAppCredentials
+)
+
+from routing_id_factory import RoutingIdFactory
+
+
+class RoutingHandler(ChannelServiceHandler):
+    def __init__(
+        self,
+        conversation_id_factory: RoutingIdFactory,
+        credential_provider: CredentialProvider,
+        auth_configuration: AuthenticationConfiguration,
+        channel_provider: ChannelProvider = None
+    ):
+        super().__init__(credential_provider, auth_configuration, channel_provider)
+        self._factory = conversation_id_factory
+        self._credentials = MicrosoftAppCredentials(None, None)
+
+    async def on_reply_to_activity(
+        self,
+        claims_identity: ClaimsIdentity,
+        conversation_id: str,
+        activity_id: str,
+        activity: Activity,
+    ) -> ResourceResponse:
+        back_conversation_id, back_service_url = self._factory.get_conversation_info(conversation_id)
+        connector_client = self._get_connector_client(back_service_url)
+        activity.conversation.id = back_conversation_id
+        activity.service_url = back_service_url
+
+        return await connector_client.conversations.send_to_conversation(back_conversation_id, activity)
+
+    async def on_send_to_conversation(
+        self, claims_identity: ClaimsIdentity, conversation_id: str, activity: Activity,
+    ) -> ResourceResponse:
+        back_conversation_id, back_service_url = self._factory.get_conversation_info(conversation_id)
+        connector_client = self._get_connector_client(back_service_url)
+        activity.conversation.id = back_conversation_id
+        activity.service_url = back_service_url
+
+        return await connector_client.conversations.send_to_conversation(back_conversation_id, activity)
+
+    async def on_update_activity(
+        self,
+        claims_identity: ClaimsIdentity,
+        conversation_id: str,
+        activity_id: str,
+        activity: Activity,
+    ) -> ResourceResponse:
+        back_conversation_id, back_service_url = self._factory.get_conversation_info(conversation_id)
+        connector_client = self._get_connector_client(back_service_url)
+        activity.conversation.id = back_conversation_id
+        activity.service_url = back_service_url
+
+        return await connector_client.conversations.update_activity(back_conversation_id, activity.id, activity)
+
+    async def on_delete_activity(
+        self, claims_identity: ClaimsIdentity, conversation_id: str, activity_id: str,
+    ):
+        back_conversation_id, back_service_url = self._factory.get_conversation_info(conversation_id)
+        connector_client = self._get_connector_client(back_service_url)
+
+        return await connector_client.conversations.delete_activity(back_conversation_id, activity_id)
+
+    async def on_create_conversation(
+        self, claims_identity: ClaimsIdentity, parameters: ConversationParameters,
+    ) -> ConversationResourceResponse:
+        # This call will be used in Teams scenarios.
+
+        # Scenario #1 - creating a thread with an activity in a Channel in a Team
+        # In order to know the serviceUrl in the case of Teams we would need to look it up based upon the
+        # TeamsChannelData.
+        # The inbound activity will contain the TeamsChannelData and so will the ConversationParameters.
+
+        # Scenario #2 - starting a one on one conversation with a particular user
+        # - needs further analysis -
+
+        back_service_url = "http://tempuri"
+        connector_client = self._get_connector_client(back_service_url)
+
+        return await connector_client.conversations.create_conversation(parameters)
+
+    async def on_delete_conversation_member(
+        self, claims_identity: ClaimsIdentity, conversation_id: str, member_id: str,
+    ):
+        return await super().on_delete_conversation_member(claims_identity, conversation_id, member_id)
+
+    async def on_get_activity_members(
+        self, claims_identity: ClaimsIdentity, conversation_id: str, activity_id: str,
+    ) -> List[ChannelAccount]:
+        return await super().on_get_activity_members(claims_identity, conversation_id, activity_id)
+
+    async def on_get_conversation_members(
+        self, claims_identity: ClaimsIdentity, conversation_id: str,
+    ) -> List[ChannelAccount]:
+        return await super().on_get_conversation_members(claims_identity, conversation_id)
+
+    async def on_get_conversations(
+        self, claims_identity: ClaimsIdentity, continuation_token: str = "",
+    ) -> ConversationsResult:
+        return await super().on_get_conversations(claims_identity, continuation_token)
+
+    async def on_get_conversation_paged_members(
+        self,
+        claims_identity: ClaimsIdentity,
+        conversation_id: str,
+        page_size: int = None,
+        continuation_token: str = "",
+    ) -> PagedMembersResult:
+        return await super().on_get_conversation_paged_members(claims_identity, conversation_id, continuation_token)
+
+    def _get_connector_client(self, service_url: str):
+        return ConnectorClient(self._credentials, service_url)
diff --git a/tests/experimental/test-protocol/routing_id_factory.py b/tests/experimental/test-protocol/routing_id_factory.py
new file mode 100644
index 000000000..0460f2df9
--- /dev/null
+++ b/tests/experimental/test-protocol/routing_id_factory.py
@@ -0,0 +1,22 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from uuid import uuid4
+from typing import Dict, Tuple
+
+
+class RoutingIdFactory:
+    def __init__(self):
+        self._forward_x_ref: Dict[str, str] = {}
+        self._backward_x_ref: Dict[str, Tuple[str, str]] = {}
+
+    def create_skill_conversation_id(self, conversation_id: str, service_url: str) -> str:
+        result = self._forward_x_ref.get(conversation_id, str(uuid4()))
+
+        self._forward_x_ref[conversation_id] = result
+        self._backward_x_ref[result] = (conversation_id, service_url)
+
+        return result
+
+    def get_conversation_info(self, encoded_conversation_id) -> Tuple[str, str]:
+        return self._backward_x_ref[encoded_conversation_id]
diff --git a/tests/skills/skills-buffered/child/app.py b/tests/skills/skills-buffered/child/app.py
new file mode 100644
index 000000000..27351c36d
--- /dev/null
+++ b/tests/skills/skills-buffered/child/app.py
@@ -0,0 +1,78 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import sys
+import traceback
+
+from aiohttp import web
+from aiohttp.web import Request, Response
+from aiohttp.web_response import json_response
+from botbuilder.core import (
+    BotFrameworkAdapterSettings,
+    TurnContext,
+    BotFrameworkAdapter,
+)
+from botbuilder.schema import Activity
+
+from bots import ChildBot
+from config import DefaultConfig
+
+CONFIG = DefaultConfig()
+
+# Create adapter.
+# See https://aka.ms/about-bot-adapter to learn more about how bots work.
+SETTINGS = BotFrameworkAdapterSettings(
+    app_id=CONFIG.APP_ID, app_password=CONFIG.APP_PASSWORD,
+)
+ADAPTER = BotFrameworkAdapter(SETTINGS)
+
+
+# Catch-all for errors.
+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.")
+    await context.send_activity(
+        "To continue to run this bot, please fix the bot source code."
+    )
+
+
+ADAPTER.on_turn_error = on_error
+
+# Create the Bot
+BOT = ChildBot()
+
+
+# Listen for incoming requests on /api/messages
+async def messages(req: Request) -> Response:
+    # Main bot message handler.
+    if "application/json" in req.headers["Content-Type"]:
+        body = await req.json()
+    else:
+        return Response(status=415)
+
+    activity = Activity().deserialize(body)
+    auth_header = req.headers["Authorization"] if "Authorization" in req.headers else ""
+
+    try:
+        response = await ADAPTER.process_activity(activity, auth_header, BOT.on_turn)
+        if response:
+            return json_response(data=response.body, status=response.status)
+        return Response(status=201)
+    except Exception as exception:
+        raise exception
+
+
+APP = web.Application()
+APP.router.add_post("/api/messages", messages)
+
+if __name__ == "__main__":
+    try:
+        web.run_app(APP, host="localhost", port=CONFIG.PORT)
+    except Exception as error:
+        raise error
diff --git a/tests/skills/skills-buffered/child/bots/__init__.py b/tests/skills/skills-buffered/child/bots/__init__.py
new file mode 100644
index 000000000..a1643fbf8
--- /dev/null
+++ b/tests/skills/skills-buffered/child/bots/__init__.py
@@ -0,0 +1,6 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from .child_bot import ChildBot
+
+__all__ = ["ChildBot"]
diff --git a/tests/skills/skills-buffered/child/bots/child_bot.py b/tests/skills/skills-buffered/child/bots/child_bot.py
new file mode 100644
index 000000000..ad6a37839
--- /dev/null
+++ b/tests/skills/skills-buffered/child/bots/child_bot.py
@@ -0,0 +1,12 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.core import ActivityHandler, TurnContext
+
+
+class ChildBot(ActivityHandler):
+    async def on_message_activity(self, turn_context: TurnContext):
+        await turn_context.send_activity("child: activity (1)")
+        await turn_context.send_activity("child: activity (2)")
+        await turn_context.send_activity("child: activity (3)")
+        await turn_context.send_activity(f"child: {turn_context.activity.text}")
diff --git a/tests/skills/skills-buffered/child/config.py b/tests/skills/skills-buffered/child/config.py
new file mode 100644
index 000000000..f21c1df0e
--- /dev/null
+++ b/tests/skills/skills-buffered/child/config.py
@@ -0,0 +1,13 @@
+#!/usr/bin/env python3
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+
+
+class DefaultConfig:
+    """ Bot Configuration """
+
+    PORT = 3979
+    APP_ID = os.environ.get("MicrosoftAppId", "")
+    APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "")
diff --git a/tests/skills/skills-buffered/child/requirements.txt b/tests/skills/skills-buffered/child/requirements.txt
new file mode 100644
index 000000000..20f8f8fe5
--- /dev/null
+++ b/tests/skills/skills-buffered/child/requirements.txt
@@ -0,0 +1,2 @@
+botbuilder-core>=4.7.1
+aiohttp
diff --git a/tests/skills/skills-buffered/parent/app.py b/tests/skills/skills-buffered/parent/app.py
new file mode 100644
index 000000000..d1e9fbc0a
--- /dev/null
+++ b/tests/skills/skills-buffered/parent/app.py
@@ -0,0 +1,100 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import sys
+import traceback
+
+from aiohttp import web
+from aiohttp.web import Request, Response
+from aiohttp.web_response import json_response
+from botbuilder.core import (
+    BotFrameworkAdapterSettings,
+    MemoryStorage,
+    TurnContext,
+    BotFrameworkAdapter,
+)
+from botbuilder.core.integration import (
+    aiohttp_channel_service_routes,
+    aiohttp_error_middleware,
+    BotFrameworkHttpClient
+)
+from botbuilder.core.skills import SkillHandler
+from botbuilder.schema import Activity
+from botframework.connector.auth import (
+    AuthenticationConfiguration,
+    SimpleCredentialProvider,
+)
+
+from bots.parent_bot import ParentBot
+from skill_conversation_id_factory import SkillConversationIdFactory
+from config import DefaultConfig
+
+CONFIG = DefaultConfig()
+
+# Create adapter.
+# See https://aka.ms/about-bot-adapter to learn more about how bots work.
+SETTINGS = BotFrameworkAdapterSettings(
+    app_id=CONFIG.APP_ID, app_password=CONFIG.APP_PASSWORD,
+)
+ADAPTER = BotFrameworkAdapter(SETTINGS)
+
+CREDENTIAL_PROVIDER = SimpleCredentialProvider(CONFIG.APP_ID, CONFIG.APP_PASSWORD)
+CLIENT = BotFrameworkHttpClient(CREDENTIAL_PROVIDER)
+
+
+# Catch-all for errors.
+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.")
+    await context.send_activity(
+        "To continue to run this bot, please fix the bot source code."
+    )
+
+
+ADAPTER.on_turn_error = on_error
+
+# Create the Bot
+BOT = ParentBot(CLIENT)
+
+STORAGE = MemoryStorage()
+ID_FACTORY = SkillConversationIdFactory(STORAGE)
+SKILL_HANDLER = SkillHandler(
+    ADAPTER, BOT, ID_FACTORY, CREDENTIAL_PROVIDER, AuthenticationConfiguration()
+)
+
+
+# Listen for incoming requests on /api/messages
+async def messages(req: Request) -> Response:
+    # Main bot message handler.
+    if "application/json" in req.headers["Content-Type"]:
+        body = await req.json()
+    else:
+        return Response(status=415)
+
+    activity = Activity().deserialize(body)
+    auth_header = req.headers["Authorization"] if "Authorization" in req.headers else ""
+
+    try:
+        response = await ADAPTER.process_activity(activity, auth_header, BOT.on_turn)
+        if response:
+            return json_response(data=response.body, status=response.status)
+        return Response(status=201)
+    except Exception as exception:
+        raise exception
+
+
+APP = web.Application(middlewares=[aiohttp_error_middleware])
+APP.router.add_post("/api/messages", messages)
+APP.router.add_routes(aiohttp_channel_service_routes(SKILL_HANDLER, "/api/skills"))
+
+if __name__ == "__main__":
+    try:
+        web.run_app(APP, host="localhost", port=CONFIG.PORT)
+    except Exception as error:
+        raise error
diff --git a/tests/skills/skills-buffered/parent/bots/__init__.py b/tests/skills/skills-buffered/parent/bots/__init__.py
new file mode 100644
index 000000000..01c37eaea
--- /dev/null
+++ b/tests/skills/skills-buffered/parent/bots/__init__.py
@@ -0,0 +1,4 @@
+from .parent_bot import ParentBot
+
+
+__all__ = ["ParentBot"]
diff --git a/tests/skills/skills-buffered/parent/bots/parent_bot.py b/tests/skills/skills-buffered/parent/bots/parent_bot.py
new file mode 100644
index 000000000..a94ce696d
--- /dev/null
+++ b/tests/skills/skills-buffered/parent/bots/parent_bot.py
@@ -0,0 +1,43 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import uuid
+
+from botbuilder.core import (
+    ActivityHandler,
+    TurnContext,
+    MessageFactory,
+)
+from botbuilder.integration import BotFrameworkHttpClient
+
+from botbuilder.schema import DeliveryModes
+
+
+class ParentBot(ActivityHandler):
+    def __init__(
+        self, skill_client: BotFrameworkHttpClient,
+    ):
+        self.client = skill_client
+
+    async def on_message_activity(self, turn_context: TurnContext):
+        await turn_context.send_activity("parent: before child")
+
+        activity = MessageFactory.text("parent to child")
+        TurnContext.apply_conversation_reference(
+            activity, TurnContext.get_conversation_reference(turn_context.activity)
+        )
+        activity.delivery_mode = DeliveryModes.expect_replies
+
+        activities = await self.client.post_buffered_activity(
+            None,
+            "toBotId",
+            "http://localhost:3979/api/messages",
+            "http://tempuri.org/whatever",
+            str(uuid.uuid4()),
+            activity,
+        )
+
+        if activities:
+            await turn_context.send_activities(activities)
+
+        await turn_context.send_activity("parent: after child")
diff --git a/tests/skills/skills-buffered/parent/config.py b/tests/skills/skills-buffered/parent/config.py
new file mode 100644
index 000000000..d66581d4c
--- /dev/null
+++ b/tests/skills/skills-buffered/parent/config.py
@@ -0,0 +1,13 @@
+#!/usr/bin/env python3
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+
+
+class DefaultConfig:
+    """ Bot Configuration """
+
+    PORT = 3978
+    APP_ID = os.environ.get("MicrosoftAppId", "")
+    APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "")
diff --git a/tests/skills/skills-buffered/parent/requirements.txt b/tests/skills/skills-buffered/parent/requirements.txt
new file mode 100644
index 000000000..20f8f8fe5
--- /dev/null
+++ b/tests/skills/skills-buffered/parent/requirements.txt
@@ -0,0 +1,2 @@
+botbuilder-core>=4.7.1
+aiohttp
diff --git a/tests/skills/skills-buffered/parent/skill_conversation_id_factory.py b/tests/skills/skills-buffered/parent/skill_conversation_id_factory.py
new file mode 100644
index 000000000..8faaae025
--- /dev/null
+++ b/tests/skills/skills-buffered/parent/skill_conversation_id_factory.py
@@ -0,0 +1,47 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.core import Storage
+from botbuilder.core.skills import ConversationIdFactoryBase
+from botbuilder.schema import ConversationReference
+
+
+class SkillConversationIdFactory(ConversationIdFactoryBase):
+    def __init__(self, storage: Storage):
+        if not storage:
+            raise TypeError("storage can't be None")
+
+        self._storage = storage
+
+    async def create_skill_conversation_id(
+        self, conversation_reference: ConversationReference
+    ) -> str:
+        if not conversation_reference:
+            raise TypeError("conversation_reference can't be None")
+
+        if not conversation_reference.conversation.id:
+            raise TypeError("conversation id in conversation reference can't be None")
+
+        if not conversation_reference.channel_id:
+            raise TypeError("channel id in conversation reference can't be None")
+
+        storage_key = f"{conversation_reference.channel_id}:{conversation_reference.conversation.id}"
+
+        skill_conversation_info = {storage_key: conversation_reference}
+
+        await self._storage.write(skill_conversation_info)
+
+        return storage_key
+
+    async def get_conversation_reference(
+        self, skill_conversation_id: str
+    ) -> ConversationReference:
+        if not skill_conversation_id:
+            raise TypeError("skill_conversation_id can't be None")
+
+        skill_conversation_info = await self._storage.read([skill_conversation_id])
+
+        return skill_conversation_info.get(skill_conversation_id)
+
+    async def delete_conversation_reference(self, skill_conversation_id: str):
+        await self._storage.delete([skill_conversation_id])
diff --git a/tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/README.md b/tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/README.md
new file mode 100644
index 000000000..f1a48af72
--- /dev/null
+++ b/tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/README.md
@@ -0,0 +1,30 @@
+# EchoBot
+
+Bot Framework v4 echo bot sample.
+
+This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a simple bot that accepts input from the user and echoes it back.
+
+## Running the sample
+- Clone the repository
+```bash
+git clone https://github.com/Microsoft/botbuilder-python.git
+```
+- Activate your desired virtual environment
+- Bring up a terminal, navigate to `botbuilder-python\samples\02.echo-bot` folder
+- In the terminal, type `pip install -r requirements.txt`
+- In the terminal, type `python app.py`
+
+## Testing the bot using Bot Framework Emulator
+[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel.
+
+- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases)
+
+### Connect to bot using Bot Framework Emulator
+- Launch Bot Framework Emulator
+- Paste this URL in the emulator window - http://localhost:3978/api/messages
+
+## Further reading
+
+- [Bot Framework Documentation](https://docs.botframework.com)
+- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0)
+- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0)
diff --git a/tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/app.py b/tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/app.py
new file mode 100644
index 000000000..96ffb9b2b
--- /dev/null
+++ b/tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/app.py
@@ -0,0 +1,98 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import sys
+import traceback
+from datetime import datetime
+
+from aiohttp import web
+from aiohttp.web import Request, Response
+from botbuilder.core import (
+    BotFrameworkAdapterSettings,
+    ConversationState,
+    MemoryStorage,
+    UserState,
+    TurnContext,
+    BotFrameworkAdapter,
+)
+from botbuilder.schema import Activity, ActivityTypes
+
+from bots import AuthBot
+from dialogs import MainDialog
+from config import DefaultConfig
+
+CONFIG = DefaultConfig()
+
+# Create adapter.
+# See https://aka.ms/about-bot-adapter to learn more about how bots work.
+SETTINGS = BotFrameworkAdapterSettings(CONFIG.APP_ID, CONFIG.APP_PASSWORD)
+ADAPTER = BotFrameworkAdapter(SETTINGS)
+
+STORAGE = MemoryStorage()
+
+CONVERSATION_STATE = ConversationState(STORAGE)
+USER_STATE = UserState(STORAGE)
+
+
+# Catch-all for errors.
+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.")
+    await context.send_activity(
+        "To continue to run this bot, please fix the bot source code."
+    )
+    # Send a trace activity if we're talking to the Bot Framework Emulator
+    if context.activity.channel_id == "emulator":
+        # Create a trace activity that contains the error object
+        trace_activity = Activity(
+            label="TurnError",
+            name="on_turn_error Trace",
+            timestamp=datetime.utcnow(),
+            type=ActivityTypes.trace,
+            value=f"{error}",
+            value_type="https://www.botframework.com/schemas/error",
+        )
+        # Send a trace activity, which will be displayed in Bot Framework Emulator
+        await context.send_activity(trace_activity)
+
+
+ADAPTER.on_turn_error = on_error
+
+DIALOG = MainDialog(CONFIG)
+
+
+# Listen for incoming requests on /api/messages
+async def messages(req: Request) -> Response:
+    # Create the Bot
+    bot = AuthBot(CONVERSATION_STATE, USER_STATE, DIALOG)
+
+    # Main bot message handler.
+    if "application/json" in req.headers["Content-Type"]:
+        body = await req.json()
+    else:
+        return Response(status=415)
+
+    activity = Activity().deserialize(body)
+    auth_header = req.headers["Authorization"] if "Authorization" in req.headers else ""
+
+    try:
+        await ADAPTER.process_activity(activity, auth_header, bot.on_turn)
+        return Response(status=201)
+    except Exception as exception:
+        raise exception
+
+
+APP = web.Application()
+APP.router.add_post("/api/messages", messages)
+
+if __name__ == "__main__":
+    try:
+        web.run_app(APP, host="localhost", port=CONFIG.PORT)
+    except Exception as error:
+        raise error
diff --git a/samples/13.core-bot/bots/__init__.py b/tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/bots/__init__.py
similarity index 55%
rename from samples/13.core-bot/bots/__init__.py
rename to tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/bots/__init__.py
index 6925db302..9b49815be 100644
--- a/samples/13.core-bot/bots/__init__.py
+++ b/tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/bots/__init__.py
@@ -1,7 +1,6 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from .dialog_bot import DialogBot
-from .dialog_and_welcome_bot import DialogAndWelcomeBot
-
-__all__ = ["DialogBot", "DialogAndWelcomeBot"]
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+from .dialog_bot import DialogBot
+from .auth_bot import AuthBot
+
+__all__ = ["DialogBot", "AuthBot"]
diff --git a/tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/bots/auth_bot.py b/tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/bots/auth_bot.py
new file mode 100644
index 000000000..e72b681c1
--- /dev/null
+++ b/tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/bots/auth_bot.py
@@ -0,0 +1,42 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from typing import List
+
+from botbuilder.core import MessageFactory, TurnContext
+from botbuilder.schema import ActivityTypes, ChannelAccount
+
+from helpers.dialog_helper import DialogHelper
+from bots import DialogBot
+
+
+class AuthBot(DialogBot):
+    async def on_turn(self, turn_context: TurnContext):
+        if turn_context.activity.type == ActivityTypes.invoke:
+            await DialogHelper.run_dialog(
+                self.dialog,
+                turn_context,
+                self.conversation_state.create_property("DialogState")
+            )
+        else:
+            await super().on_turn(turn_context)
+
+    async def on_members_added_activity(
+        self, members_added: List[ChannelAccount], turn_context: TurnContext
+    ):
+        for member in members_added:
+            if member.id != turn_context.activity.recipient.id:
+                await turn_context.send_activity(
+                    MessageFactory.text("Hello and welcome!")
+                )
+
+    async def on_token_response_event(
+        self, turn_context: TurnContext
+    ):
+        print("on token: Running dialog with Message Activity.")
+
+        return await DialogHelper.run_dialog(
+            self.dialog,
+            turn_context,
+            self.conversation_state.create_property("DialogState")
+        )
diff --git a/tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/bots/dialog_bot.py b/tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/bots/dialog_bot.py
new file mode 100644
index 000000000..eb9b355f6
--- /dev/null
+++ b/tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/bots/dialog_bot.py
@@ -0,0 +1,29 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.core import ActivityHandler, ConversationState, UserState, TurnContext
+from botbuilder.dialogs import Dialog
+
+from helpers.dialog_helper import DialogHelper
+
+
+class DialogBot(ActivityHandler):
+    def __init__(self, conversation_state: ConversationState, user_state: UserState, dialog: Dialog):
+        self.conversation_state = conversation_state
+        self._user_state = user_state
+        self.dialog = dialog
+
+    async def on_turn(self, turn_context: TurnContext):
+        await super().on_turn(turn_context)
+
+        await self.conversation_state.save_changes(turn_context, False)
+        await self._user_state.save_changes(turn_context, False)
+
+    async def on_message_activity(self, turn_context: TurnContext):
+        print("on message: Running dialog with Message Activity.")
+
+        return await DialogHelper.run_dialog(
+            self.dialog,
+            turn_context,
+            self.conversation_state.create_property("DialogState")
+        )
diff --git a/samples/13.core-bot/config.py b/tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/config.py
similarity index 58%
rename from samples/13.core-bot/config.py
rename to tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/config.py
index 83f1bbbdf..3c064f3ff 100644
--- a/samples/13.core-bot/config.py
+++ b/tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/config.py
@@ -1,19 +1,16 @@
-#!/usr/bin/env python3
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import os
-
-""" Bot Configuration """
-
-
-class DefaultConfig:
-    """ Bot Configuration """
-
-    PORT = 3978
-    APP_ID = os.environ.get("MicrosoftAppId", "")
-    APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "")
-    LUIS_APP_ID = os.environ.get("LuisAppId", "")
-    LUIS_API_KEY = os.environ.get("LuisAPIKey", "")
-    # LUIS endpoint host name, ie "westus.api.cognitive.microsoft.com"
-    LUIS_API_HOST_NAME = os.environ.get("LuisAPIHostName", "")
+#!/usr/bin/env python3
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+
+""" Bot Configuration """
+
+
+class DefaultConfig:
+    """ Bot Configuration """
+
+    PORT = 3978
+    APP_ID = os.environ.get("MicrosoftAppId", "")
+    APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "")
+    CONNECTION_NAME = ""
diff --git a/tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/dialogs/__init__.py b/tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/dialogs/__init__.py
new file mode 100644
index 000000000..6ec3374f3
--- /dev/null
+++ b/tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/dialogs/__init__.py
@@ -0,0 +1,7 @@
+from .logout_dialog import LogoutDialog
+from .main_dialog import MainDialog
+
+__all__ = [
+    "LogoutDialog",
+    "MainDialog"
+]
diff --git a/tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/dialogs/logout_dialog.py b/tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/dialogs/logout_dialog.py
new file mode 100644
index 000000000..6855b8710
--- /dev/null
+++ b/tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/dialogs/logout_dialog.py
@@ -0,0 +1,47 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.dialogs import (
+    ComponentDialog,
+    DialogTurnResult,
+)
+from botbuilder.dialogs import DialogContext
+from botbuilder.core import BotFrameworkAdapter, MessageFactory
+from botbuilder.schema import ActivityTypes
+
+
+class LogoutDialog(ComponentDialog):
+    def __init__(
+        self, dialog_id: str, connection_name: str,
+    ):
+        super().__init__(dialog_id)
+
+        self.connection_name = connection_name
+
+    async def on_begin_dialog(
+        self, inner_dc: DialogContext, options: object
+    ) -> DialogTurnResult:
+        result = await self._interrupt(inner_dc)
+        if result:
+            return result
+
+        return await super().on_begin_dialog(inner_dc, options)
+
+    async def on_continue_dialog(self, inner_dc: DialogContext) -> DialogTurnResult:
+        result = await self._interrupt(inner_dc)
+        if result:
+            return result
+
+        return await super().on_continue_dialog(inner_dc)
+
+    async def _interrupt(self, inner_dc: DialogContext):
+        if inner_dc.context.activity.type == ActivityTypes.message:
+            text = inner_dc.context.activity.text.lower()
+
+            if text == "logout":
+                bot_adapter: BotFrameworkAdapter = inner_dc.context.adapter
+                await bot_adapter.sign_out_user(inner_dc.context, self.connection_name)
+                await inner_dc.context.send_activity(MessageFactory.text("You have been signed out."))
+                return await inner_dc.cancel_all_dialogs()
+
+        return None
diff --git a/tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/dialogs/main_dialog.py b/tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/dialogs/main_dialog.py
new file mode 100644
index 000000000..afdf3727a
--- /dev/null
+++ b/tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/dialogs/main_dialog.py
@@ -0,0 +1,72 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.dialogs import (
+    WaterfallDialog,
+    WaterfallStepContext,
+    DialogTurnResult,
+)
+from botbuilder.dialogs.prompts import ConfirmPrompt, PromptOptions, OAuthPrompt, OAuthPromptSettings
+from botbuilder.core import MessageFactory
+from dialogs import LogoutDialog
+
+
+class MainDialog(LogoutDialog):
+    def __init__(
+        self, configuration,
+    ):
+        super().__init__(MainDialog.__name__, configuration.CONNECTION_NAME)
+
+        self.add_dialog(
+            OAuthPrompt(
+                OAuthPrompt.__name__,
+                OAuthPromptSettings(
+                    connection_name=self.connection_name,
+                    text="Please Sign In",
+                    title="Sign In",
+                    timeout=30000,
+                )
+            )
+        )
+        self.add_dialog(ConfirmPrompt(ConfirmPrompt.__name__))
+        self.add_dialog(
+            WaterfallDialog(
+                "WFDialog",
+                [self.prompt_step, self.login_step, self.display_token_phase_one, self.display_token_phase_two]
+            )
+        )
+
+        self.initial_dialog_id = "WFDialog"
+
+    async def prompt_step(self, step_context: WaterfallStepContext) -> DialogTurnResult:
+        return await step_context.begin_dialog(
+            OAuthPrompt.__name__
+        )
+
+    async def login_step(self, step_context: WaterfallStepContext) -> DialogTurnResult:
+        token_response = step_context.result
+        if token_response:
+            await step_context.context.send_activity(MessageFactory.text("You are now logged in."))
+            return await step_context.prompt(
+                ConfirmPrompt.__name__,
+                PromptOptions(prompt=MessageFactory.text("Would you like to view your token?"))
+            )
+
+        await step_context.context.send_activity(MessageFactory.text("Login was not successful please try again."))
+        return await step_context.end_dialog()
+
+    async def display_token_phase_one(self, step_context: WaterfallStepContext) -> DialogTurnResult:
+        await step_context.context.send_activity(MessageFactory.text("Thank you"))
+
+        result = step_context.result
+        if result:
+            return await step_context.begin_dialog(OAuthPrompt.__name__)
+
+        return await step_context.end_dialog()
+
+    async def display_token_phase_two(self, step_context: WaterfallStepContext) -> DialogTurnResult:
+        token_response = step_context.result
+        if token_response:
+            await step_context.context.send_activity(MessageFactory.text(f"Here is your token {token_response.token}"))
+
+        return await step_context.end_dialog()
diff --git a/tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/helpers/__init__.py b/tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/helpers/__init__.py
new file mode 100644
index 000000000..8dba0e6d6
--- /dev/null
+++ b/tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/helpers/__init__.py
@@ -0,0 +1,6 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from . import dialog_helper
+
+__all__ = ["dialog_helper"]
diff --git a/samples/python_django/13.core-bot/helpers/dialog_helper.py b/tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/helpers/dialog_helper.py
similarity index 83%
rename from samples/python_django/13.core-bot/helpers/dialog_helper.py
rename to tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/helpers/dialog_helper.py
index 7c896d18c..062271fd8 100644
--- a/samples/python_django/13.core-bot/helpers/dialog_helper.py
+++ b/tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/helpers/dialog_helper.py
@@ -1,22 +1,19 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-"""Utility to run dialogs."""
-from botbuilder.core import StatePropertyAccessor, TurnContext
-from botbuilder.dialogs import Dialog, DialogSet, DialogTurnStatus
-
-
-class DialogHelper:
-    """Dialog Helper implementation."""
-
-    @staticmethod
-    async def run_dialog(
-        dialog: Dialog, turn_context: TurnContext, accessor: StatePropertyAccessor
-    ):  # pylint: disable=line-too-long
-        """Run dialog."""
-        dialog_set = DialogSet(accessor)
-        dialog_set.add(dialog)
-
-        dialog_context = await dialog_set.create_context(turn_context)
-        results = await dialog_context.continue_dialog()
-        if results.status == DialogTurnStatus.Empty:
-            await dialog_context.begin_dialog(dialog.id)
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.core import StatePropertyAccessor, TurnContext
+from botbuilder.dialogs import Dialog, DialogSet, DialogTurnStatus
+
+
+class DialogHelper:
+    @staticmethod
+    async def run_dialog(
+        dialog: Dialog, turn_context: TurnContext, accessor: StatePropertyAccessor
+    ):
+        dialog_set = DialogSet(accessor)
+        dialog_set.add(dialog)
+
+        dialog_context = await dialog_set.create_context(turn_context)
+        results = await dialog_context.continue_dialog()
+        if results.status == DialogTurnStatus.Empty:
+            await dialog_context.begin_dialog(dialog.id)
diff --git a/tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/requirements.txt b/tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/requirements.txt
new file mode 100644
index 000000000..87eba6848
--- /dev/null
+++ b/tests/skills/skills-prototypes/dialog-to-dialog/authentication-bot/requirements.txt
@@ -0,0 +1,2 @@
+botbuilder-core>=4.7.1
+flask>=1.0.3
diff --git a/tests/skills/skills-prototypes/simple-bot-to-bot/simple-child-bot/README.md b/tests/skills/skills-prototypes/simple-bot-to-bot/simple-child-bot/README.md
new file mode 100644
index 000000000..f1a48af72
--- /dev/null
+++ b/tests/skills/skills-prototypes/simple-bot-to-bot/simple-child-bot/README.md
@@ -0,0 +1,30 @@
+# EchoBot
+
+Bot Framework v4 echo bot sample.
+
+This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a simple bot that accepts input from the user and echoes it back.
+
+## Running the sample
+- Clone the repository
+```bash
+git clone https://github.com/Microsoft/botbuilder-python.git
+```
+- Activate your desired virtual environment
+- Bring up a terminal, navigate to `botbuilder-python\samples\02.echo-bot` folder
+- In the terminal, type `pip install -r requirements.txt`
+- In the terminal, type `python app.py`
+
+## Testing the bot using Bot Framework Emulator
+[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel.
+
+- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases)
+
+### Connect to bot using Bot Framework Emulator
+- Launch Bot Framework Emulator
+- Paste this URL in the emulator window - http://localhost:3978/api/messages
+
+## Further reading
+
+- [Bot Framework Documentation](https://docs.botframework.com)
+- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0)
+- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0)
diff --git a/tests/skills/skills-prototypes/simple-bot-to-bot/simple-child-bot/app.py b/tests/skills/skills-prototypes/simple-bot-to-bot/simple-child-bot/app.py
new file mode 100644
index 000000000..d1964743e
--- /dev/null
+++ b/tests/skills/skills-prototypes/simple-bot-to-bot/simple-child-bot/app.py
@@ -0,0 +1,85 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import sys
+from datetime import datetime
+
+from aiohttp import web
+from aiohttp.web import Request, Response
+from botbuilder.core import (
+    BotFrameworkAdapterSettings,
+    TurnContext,
+    BotFrameworkAdapter,
+)
+from botbuilder.schema import Activity, ActivityTypes
+
+from bots import EchoBot
+from config import DefaultConfig
+
+CONFIG = DefaultConfig()
+
+# Create adapter.
+# See https://aka.ms/about-bot-adapter to learn more about how bots work.
+SETTINGS = BotFrameworkAdapterSettings(CONFIG.APP_ID, CONFIG.APP_PASSWORD)
+ADAPTER = BotFrameworkAdapter(SETTINGS)
+
+
+# Catch-all for errors.
+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)
+
+    # Send a message to the user
+    await context.send_activity("The bot encountered an error or bug.")
+    await context.send_activity(
+        "To continue to run this bot, please fix the bot source code."
+    )
+    # Send a trace activity if we're talking to the Bot Framework Emulator
+    if context.activity.channel_id == "emulator":
+        # Create a trace activity that contains the error object
+        trace_activity = Activity(
+            label="TurnError",
+            name="on_turn_error Trace",
+            timestamp=datetime.utcnow(),
+            type=ActivityTypes.trace,
+            value=f"{error}",
+            value_type="https://www.botframework.com/schemas/error",
+        )
+        # Send a trace activity, which will be displayed in Bot Framework Emulator
+        await context.send_activity(trace_activity)
+
+
+ADAPTER.on_turn_error = on_error
+
+# Create the Bot
+BOT = EchoBot()
+
+
+# Listen for incoming requests on /api/messages
+async def messages(req: Request) -> Response:
+    # Main bot message handler.
+    if "application/json" in req.headers["Content-Type"]:
+        body = await req.json()
+    else:
+        return Response(status=415)
+
+    activity = Activity().deserialize(body)
+    auth_header = req.headers["Authorization"] if "Authorization" in req.headers else ""
+
+    try:
+        await ADAPTER.process_activity(activity, auth_header, BOT.on_turn)
+        return Response(status=201)
+    except Exception as exception:
+        raise exception
+
+
+APP = web.Application()
+APP.router.add_post("/api/messages", messages)
+
+if __name__ == "__main__":
+    try:
+        web.run_app(APP, host="localhost", port=CONFIG.PORT)
+    except Exception as error:
+        raise error
diff --git a/tests/skills/skills-prototypes/simple-bot-to-bot/simple-child-bot/bots/__init__.py b/tests/skills/skills-prototypes/simple-bot-to-bot/simple-child-bot/bots/__init__.py
new file mode 100644
index 000000000..e41ca32ac
--- /dev/null
+++ b/tests/skills/skills-prototypes/simple-bot-to-bot/simple-child-bot/bots/__init__.py
@@ -0,0 +1,6 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from .echo_bot import EchoBot
+
+__all__ = ["EchoBot"]
diff --git a/tests/skills/skills-prototypes/simple-bot-to-bot/simple-child-bot/bots/echo_bot.py b/tests/skills/skills-prototypes/simple-bot-to-bot/simple-child-bot/bots/echo_bot.py
new file mode 100644
index 000000000..e82cebb51
--- /dev/null
+++ b/tests/skills/skills-prototypes/simple-bot-to-bot/simple-child-bot/bots/echo_bot.py
@@ -0,0 +1,27 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.core import ActivityHandler, MessageFactory, TurnContext
+from botbuilder.schema import Activity, ActivityTypes, EndOfConversationCodes
+
+
+class EchoBot(ActivityHandler):
+    async def on_message_activity(self, turn_context: TurnContext):
+        if "end" in turn_context.activity.text or "exit" in turn_context.activity.text:
+            # Send End of conversation at the end.
+            await turn_context.send_activity(
+                MessageFactory.text("Ending conversation from the skill...")
+            )
+
+            end_of_conversation = Activity(type=ActivityTypes.end_of_conversation)
+            end_of_conversation.code = EndOfConversationCodes.completed_successfully
+            await turn_context.send_activity(end_of_conversation)
+        else:
+            await turn_context.send_activity(
+                MessageFactory.text(f"Echo: {turn_context.activity.text}")
+            )
+            await turn_context.send_activity(
+                MessageFactory.text(
+                    f'Say "end" or "exit" and I\'ll end the conversation and back to the parent.'
+                )
+            )
diff --git a/tests/skills/skills-prototypes/simple-bot-to-bot/simple-child-bot/config.py b/tests/skills/skills-prototypes/simple-bot-to-bot/simple-child-bot/config.py
new file mode 100644
index 000000000..ed68df254
--- /dev/null
+++ b/tests/skills/skills-prototypes/simple-bot-to-bot/simple-child-bot/config.py
@@ -0,0 +1,15 @@
+#!/usr/bin/env python3
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+
+""" Bot Configuration """
+
+
+class DefaultConfig:
+    """ Bot Configuration """
+
+    PORT = 3978
+    APP_ID = os.environ.get("MicrosoftAppId", "")
+    APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "")
diff --git a/tests/skills/skills-prototypes/simple-bot-to-bot/simple-child-bot/requirements.txt b/tests/skills/skills-prototypes/simple-bot-to-bot/simple-child-bot/requirements.txt
new file mode 100644
index 000000000..87eba6848
--- /dev/null
+++ b/tests/skills/skills-prototypes/simple-bot-to-bot/simple-child-bot/requirements.txt
@@ -0,0 +1,2 @@
+botbuilder-core>=4.7.1
+flask>=1.0.3
diff --git a/tests/skills/skills-prototypes/simple-bot-to-bot/simple-root-bot/app.py b/tests/skills/skills-prototypes/simple-bot-to-bot/simple-root-bot/app.py
new file mode 100644
index 000000000..2915c0d47
--- /dev/null
+++ b/tests/skills/skills-prototypes/simple-bot-to-bot/simple-root-bot/app.py
@@ -0,0 +1,113 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import sys
+import traceback
+from datetime import datetime
+
+from aiohttp import web
+from aiohttp.web import Request, Response
+from botbuilder.core import (
+    BotFrameworkAdapterSettings,
+    ConversationState,
+    MemoryStorage,
+    TurnContext,
+    BotFrameworkAdapter,
+)
+from botbuilder.core.integration import (
+    aiohttp_channel_service_routes,
+    aiohttp_error_middleware,
+    BotFrameworkHttpClient,
+)
+from botbuilder.core.skills import SkillConversationIdFactory, SkillHandler
+from botbuilder.schema import Activity, ActivityTypes
+from botframework.connector.auth import (
+    AuthenticationConfiguration,
+    SimpleCredentialProvider,
+)
+
+from bots import RootBot
+from config import DefaultConfig, SkillConfiguration
+
+CONFIG = DefaultConfig()
+SKILL_CONFIG = SkillConfiguration()
+
+CREDENTIAL_PROVIDER = SimpleCredentialProvider(CONFIG.APP_ID, CONFIG.APP_PASSWORD)
+CLIENT = BotFrameworkHttpClient(CREDENTIAL_PROVIDER)
+
+# Create adapter.
+# See https://aka.ms/about-bot-adapter to learn more about how bots work.
+SETTINGS = BotFrameworkAdapterSettings(CONFIG.APP_ID, CONFIG.APP_PASSWORD)
+ADAPTER = BotFrameworkAdapter(SETTINGS)
+
+STORAGE = MemoryStorage()
+
+CONVERSATION_STATE = ConversationState(STORAGE)
+ID_FACTORY = SkillConversationIdFactory(STORAGE)
+
+
+# Catch-all for errors.
+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.")
+    await context.send_activity(
+        "To continue to run this bot, please fix the bot source code."
+    )
+    # Send a trace activity if we're talking to the Bot Framework Emulator
+    if context.activity.channel_id == "emulator":
+        # Create a trace activity that contains the error object
+        trace_activity = Activity(
+            label="TurnError",
+            name="on_turn_error Trace",
+            timestamp=datetime.utcnow(),
+            type=ActivityTypes.trace,
+            value=f"{error}",
+            value_type="https://www.botframework.com/schemas/error",
+        )
+        # Send a trace activity, which will be displayed in Bot Framework Emulator
+        await context.send_activity(trace_activity)
+
+
+ADAPTER.on_turn_error = on_error
+
+# Create the Bot
+BOT = RootBot(CONVERSATION_STATE, SKILL_CONFIG, ID_FACTORY, CLIENT, CONFIG)
+
+SKILL_HANDLER = SkillHandler(
+    ADAPTER, BOT, ID_FACTORY, CREDENTIAL_PROVIDER, AuthenticationConfiguration()
+)
+
+
+# Listen for incoming requests on /api/messages
+async def messages(req: Request) -> Response:
+    # Main bot message handler.
+    if "application/json" in req.headers["Content-Type"]:
+        body = await req.json()
+    else:
+        return Response(status=415)
+
+    activity = Activity().deserialize(body)
+    auth_header = req.headers["Authorization"] if "Authorization" in req.headers else ""
+
+    try:
+        await ADAPTER.process_activity(activity, auth_header, BOT.on_turn)
+        return Response(status=201)
+    except Exception as exception:
+        raise exception
+
+
+APP = web.Application(middlewares=[aiohttp_error_middleware])
+APP.router.add_post("/api/messages", messages)
+APP.router.add_routes(aiohttp_channel_service_routes(SKILL_HANDLER, "/api/skills"))
+
+if __name__ == "__main__":
+    try:
+        web.run_app(APP, host="localhost", port=CONFIG.PORT)
+    except Exception as error:
+        raise error
diff --git a/tests/skills/skills-prototypes/simple-bot-to-bot/simple-root-bot/bots/__init__.py b/tests/skills/skills-prototypes/simple-bot-to-bot/simple-root-bot/bots/__init__.py
new file mode 100644
index 000000000..5cf1c3615
--- /dev/null
+++ b/tests/skills/skills-prototypes/simple-bot-to-bot/simple-root-bot/bots/__init__.py
@@ -0,0 +1,4 @@
+from .root_bot import RootBot
+
+
+__all__ = ["RootBot"]
diff --git a/tests/skills/skills-prototypes/simple-bot-to-bot/simple-root-bot/bots/root_bot.py b/tests/skills/skills-prototypes/simple-bot-to-bot/simple-root-bot/bots/root_bot.py
new file mode 100644
index 000000000..c271904fd
--- /dev/null
+++ b/tests/skills/skills-prototypes/simple-bot-to-bot/simple-root-bot/bots/root_bot.py
@@ -0,0 +1,108 @@
+from typing import List
+
+from botbuilder.core import (
+    ActivityHandler,
+    ConversationState,
+    MessageFactory,
+    TurnContext,
+)
+from botbuilder.core.skills import SkillConversationIdFactory
+from botbuilder.integration.aiohttp import BotFrameworkHttpClient
+
+from botbuilder.schema import ActivityTypes, ChannelAccount
+
+from config import DefaultConfig, SkillConfiguration
+
+
+class RootBot(ActivityHandler):
+    def __init__(
+        self,
+        conversation_state: ConversationState,
+        skills_config: SkillConfiguration,
+        conversation_id_factory: SkillConversationIdFactory,
+        skill_client: BotFrameworkHttpClient,
+        config: DefaultConfig,
+    ):
+        self._conversation_id_factory = conversation_id_factory
+        self._bot_id = config.APP_ID
+        self._skill_client = skill_client
+        self._skills_config = skills_config
+        self._conversation_state = conversation_state
+        self._active_skill_property = conversation_state.create_property(
+            "activeSkillProperty"
+        )
+
+    async def on_turn(self, turn_context: TurnContext):
+        if turn_context.activity.type == ActivityTypes.end_of_conversation:
+            # Handle end of conversation back from the skill
+            # forget skill invocation
+            await self._active_skill_property.delete(turn_context)
+            await self._conversation_state.save_changes(turn_context, force=True)
+
+            # We are back
+            await turn_context.send_activity(
+                MessageFactory.text(
+                    'Back in the root bot. Say "skill" and I\'ll patch you through'
+                )
+            )
+        else:
+            await super().on_turn(turn_context)
+
+    async def on_message_activity(self, turn_context: TurnContext):
+        # If there is an active skill
+        active_skill_id: str = await self._active_skill_property.get(turn_context)
+        skill_conversation_id = await self._conversation_id_factory.create_skill_conversation_id(
+            TurnContext.get_conversation_reference(turn_context.activity)
+        )
+
+        if active_skill_id:
+            # NOTE: Always SaveChanges() before calling a skill so that any activity generated by the skill
+            # will have access to current accurate state.
+            await self._conversation_state.save_changes(turn_context, force=True)
+
+            # route activity to the skill
+            await self._skill_client.post_activity(
+                self._bot_id,
+                self._skills_config.SKILLS[active_skill_id].app_id,
+                self._skills_config.SKILLS[active_skill_id].skill_endpoint,
+                self._skills_config.SKILL_HOST_ENDPOINT,
+                skill_conversation_id,
+                turn_context.activity,
+            )
+        else:
+            if "skill" in turn_context.activity.text:
+                await turn_context.send_activity(
+                    MessageFactory.text("Got it, connecting you to the skill...")
+                )
+
+                # save ConversationReferene for skill
+                await self._active_skill_property.set(turn_context, "SkillBot")
+
+                # NOTE: Always SaveChanges() before calling a skill so that any activity generated by the
+                # skill will have access to current accurate state.
+                await self._conversation_state.save_changes(turn_context, force=True)
+
+                await self._skill_client.post_activity(
+                    self._bot_id,
+                    self._skills_config.SKILLS["SkillBot"].app_id,
+                    self._skills_config.SKILLS["SkillBot"].skill_endpoint,
+                    self._skills_config.SKILL_HOST_ENDPOINT,
+                    skill_conversation_id,
+                    turn_context.activity,
+                )
+            else:
+                # just respond
+                await turn_context.send_activity(
+                    MessageFactory.text(
+                        "Me no nothin'. Say \"skill\" and I'll patch you through"
+                    )
+                )
+
+    async def on_members_added_activity(
+        self, members_added: List[ChannelAccount], turn_context: TurnContext
+    ):
+        for member in members_added:
+            if member.id != turn_context.activity.recipient.id:
+                await turn_context.send_activity(
+                    MessageFactory.text("Hello and welcome!")
+                )
diff --git a/tests/skills/skills-prototypes/simple-bot-to-bot/simple-root-bot/config.py b/tests/skills/skills-prototypes/simple-bot-to-bot/simple-root-bot/config.py
new file mode 100644
index 000000000..af0df9c81
--- /dev/null
+++ b/tests/skills/skills-prototypes/simple-bot-to-bot/simple-root-bot/config.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+from typing import Dict
+from botbuilder.core.skills import BotFrameworkSkill
+
+""" Bot Configuration """
+
+
+class DefaultConfig:
+    """ Bot Configuration """
+
+    PORT = 3428
+    APP_ID = os.environ.get("MicrosoftAppId", "")
+    APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "")
+    SKILL_HOST_ENDPOINT = "http://localhost:3428/api/skills"
+    SKILLS = [
+        {
+            "id": "SkillBot",
+            "app_id": "",
+            "skill_endpoint": "http://localhost:3978/api/messages",
+        },
+    ]
+
+
+class SkillConfiguration:
+    SKILL_HOST_ENDPOINT = DefaultConfig.SKILL_HOST_ENDPOINT
+    SKILLS: Dict[str, BotFrameworkSkill] = {
+        skill["id"]: BotFrameworkSkill(**skill) for skill in DefaultConfig.SKILLS
+    }
diff --git a/tests/skills/skills-prototypes/simple-bot-to-bot/simple-root-bot/middleware/__init__.py b/tests/skills/skills-prototypes/simple-bot-to-bot/simple-root-bot/middleware/__init__.py
new file mode 100644
index 000000000..b4c3cd2cf
--- /dev/null
+++ b/tests/skills/skills-prototypes/simple-bot-to-bot/simple-root-bot/middleware/__init__.py
@@ -0,0 +1,4 @@
+from .dummy_middleware import DummyMiddleware
+
+
+__all__ = ["DummyMiddleware"]
diff --git a/tests/skills/skills-prototypes/simple-bot-to-bot/simple-root-bot/middleware/dummy_middleware.py b/tests/skills/skills-prototypes/simple-bot-to-bot/simple-root-bot/middleware/dummy_middleware.py
new file mode 100644
index 000000000..82eb34707
--- /dev/null
+++ b/tests/skills/skills-prototypes/simple-bot-to-bot/simple-root-bot/middleware/dummy_middleware.py
@@ -0,0 +1,32 @@
+from typing import Awaitable, Callable, List
+
+from botbuilder.core import Middleware, TurnContext
+from botbuilder.schema import Activity, ResourceResponse
+
+
+class DummyMiddleware(Middleware):
+    def __init__(self, label: str):
+        self._label = label
+
+    async def on_turn(
+        self, context: TurnContext, logic: Callable[[TurnContext], Awaitable]
+    ):
+        message = f"{self._label} {context.activity.type} {context.activity.text}"
+        print(message)
+
+        # Register outgoing handler
+        context.on_send_activities(self._outgoing_handler)
+
+        await logic()
+
+    async def _outgoing_handler(
+        self,
+        context: TurnContext,  # pylint: disable=unused-argument
+        activities: List[Activity],
+        logic: Callable[[TurnContext], Awaitable[List[ResourceResponse]]],
+    ):
+        for activity in activities:
+            message = f"{self._label} {activity.type} {activity.text}"
+            print(message)
+
+        return await logic()
diff --git a/tests/teams/scenarios/action-based-messaging-extension-fetch-task/app.py b/tests/teams/scenarios/action-based-messaging-extension-fetch-task/app.py
new file mode 100644
index 000000000..103c5f31a
--- /dev/null
+++ b/tests/teams/scenarios/action-based-messaging-extension-fetch-task/app.py
@@ -0,0 +1,93 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import json
+import sys
+from datetime import datetime
+
+from aiohttp import web
+from aiohttp.web import Request, Response, json_response
+from botbuilder.core import (
+    BotFrameworkAdapterSettings,
+    TurnContext,
+    BotFrameworkAdapter,
+)
+from botbuilder.schema import Activity, ActivityTypes
+from bots import ActionBasedMessagingExtensionFetchTaskBot
+from config import DefaultConfig
+
+CONFIG = DefaultConfig()
+
+# Create adapter.
+# See https://aka.ms/about-bot-adapter to learn more about how bots work.
+SETTINGS = BotFrameworkAdapterSettings(CONFIG.APP_ID, CONFIG.APP_PASSWORD)
+ADAPTER = BotFrameworkAdapter(SETTINGS)
+
+
+# Catch-all for errors.
+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)
+
+    # Send a message to the user
+    await context.send_activity("The bot encountered an error or bug.")
+    await context.send_activity(
+        "To continue to run this bot, please fix the bot source code."
+    )
+    # Send a trace activity if we're talking to the Bot Framework Emulator
+    if context.activity.channel_id == "emulator":
+        # Create a trace activity that contains the error object
+        trace_activity = Activity(
+            label="TurnError",
+            name="on_turn_error Trace",
+            timestamp=datetime.utcnow(),
+            type=ActivityTypes.trace,
+            value=f"{error}",
+            value_type="https://www.botframework.com/schemas/error",
+        )
+        # Send a trace activity, which will be displayed in Bot Framework Emulator
+        await context.send_activity(trace_activity)
+
+
+ADAPTER.on_turn_error = on_error
+
+# Create the Bot
+BOT = ActionBasedMessagingExtensionFetchTaskBot()
+
+
+# Listen for incoming requests on /api/messages
+async def messages(req: Request) -> Response:
+    # Main bot message handler.
+    if "application/json" in req.headers["Content-Type"]:
+        body = await req.json()
+    else:
+        return Response(status=415)
+
+    activity = Activity().deserialize(body)
+    auth_header = req.headers["Authorization"] if "Authorization" in req.headers else ""
+
+    try:
+        invoke_response = await ADAPTER.process_activity(
+            activity, auth_header, BOT.on_turn
+        )
+        if invoke_response:
+            return json_response(
+                data=invoke_response.body, status=invoke_response.status
+            )
+        return Response(status=201)
+    except PermissionError:
+        return Response(status=401)
+    except Exception:
+        return Response(status=500)
+
+
+APP = web.Application()
+APP.router.add_post("/api/messages", messages)
+
+if __name__ == "__main__":
+    try:
+        web.run_app(APP, host="localhost", port=CONFIG.PORT)
+    except Exception as error:
+        raise error
diff --git a/tests/teams/scenarios/action-based-messaging-extension-fetch-task/bots/__init__.py b/tests/teams/scenarios/action-based-messaging-extension-fetch-task/bots/__init__.py
new file mode 100644
index 000000000..fe9caf948
--- /dev/null
+++ b/tests/teams/scenarios/action-based-messaging-extension-fetch-task/bots/__init__.py
@@ -0,0 +1,8 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from .action_based_messaging_extension_fetch_task_bot import (
+    ActionBasedMessagingExtensionFetchTaskBot,
+)
+
+__all__ = ["ActionBasedMessagingExtensionFetchTaskBot"]
diff --git a/tests/teams/scenarios/action-based-messaging-extension-fetch-task/bots/action_based_messaging_extension_fetch_task_bot.py b/tests/teams/scenarios/action-based-messaging-extension-fetch-task/bots/action_based_messaging_extension_fetch_task_bot.py
new file mode 100644
index 000000000..9e9c13fa9
--- /dev/null
+++ b/tests/teams/scenarios/action-based-messaging-extension-fetch-task/bots/action_based_messaging_extension_fetch_task_bot.py
@@ -0,0 +1,229 @@
+# Copyright (c) Microsoft Corp. All rights reserved.
+# Licensed under the MIT License.
+
+from typing import List
+import random
+from botbuilder.core import (
+    CardFactory,
+    MessageFactory,
+    TurnContext,
+)
+from botbuilder.schema import Attachment
+from botbuilder.schema.teams import (
+    MessagingExtensionAction,
+    MessagingExtensionActionResponse,
+    TaskModuleContinueResponse,
+    MessagingExtensionResult,
+    TaskModuleTaskInfo,
+)
+from botbuilder.core.teams import TeamsActivityHandler
+from example_data import ExampleData
+
+
+class ActionBasedMessagingExtensionFetchTaskBot(TeamsActivityHandler):
+    async def on_message_activity(self, turn_context: TurnContext):
+        value = turn_context.activity.value
+        if value is not None:
+            # This was a message from the card.
+            answer = value["Answer"]
+            choices = value["Choices"]
+            reply = MessageFactory.text(
+                f"{turn_context.activity.from_property.name} answered '{answer}' and chose '{choices}'."
+            )
+            await turn_context.send_activity(reply)
+        else:
+            # This is a regular text message.
+            reply = MessageFactory.text(
+                "Hello from ActionBasedMessagingExtensionFetchTaskBot."
+            )
+            await turn_context.send_activity(reply)
+
+    async def on_teams_messaging_extension_fetch_task(
+        self, turn_context: TurnContext, action: MessagingExtensionAction
+    ) -> MessagingExtensionActionResponse:
+        card = self._create_adaptive_card_editor()
+        task_info = TaskModuleTaskInfo(
+            card=card, height=450, title="Task Module Fetch Example", width=500
+        )
+        continue_response = TaskModuleContinueResponse(type="continue", value=task_info)
+        return MessagingExtensionActionResponse(task=continue_response)
+
+    async def on_teams_messaging_extension_submit_action(  # pylint: disable=unused-argument
+        self, turn_context: TurnContext, action: MessagingExtensionAction
+    ) -> MessagingExtensionActionResponse:
+        question = action.data["Question"]
+        multi_select = action.data["MultiSelect"]
+        option1 = action.data["Option1"]
+        option2 = action.data["Option2"]
+        option3 = action.data["Option3"]
+        preview_card = self._create_adaptive_card_preview(
+            user_text=question,
+            is_multi_select=multi_select,
+            option1=option1,
+            option2=option2,
+            option3=option3,
+        )
+
+        extension_result = MessagingExtensionResult(
+            type="botMessagePreview",
+            activity_preview=MessageFactory.attachment(preview_card),
+        )
+        return MessagingExtensionActionResponse(compose_extension=extension_result)
+
+    async def on_teams_messaging_extension_bot_message_preview_edit(  # pylint: disable=unused-argument
+        self, turn_context: TurnContext, action: MessagingExtensionAction
+    ) -> MessagingExtensionActionResponse:
+        activity_preview = action.bot_activity_preview[0]
+        content = activity_preview.attachments[0].content
+        data = self._get_example_data(content)
+        card = self._create_adaptive_card_editor(
+            data.question,
+            data.is_multi_select,
+            data.option1,
+            data.option2,
+            data.option3,
+        )
+        task_info = TaskModuleTaskInfo(
+            card=card, height=450, title="Task Module Fetch Example", width=500
+        )
+        continue_response = TaskModuleContinueResponse(type="continue", value=task_info)
+        return MessagingExtensionActionResponse(task=continue_response)
+
+    async def on_teams_messaging_extension_bot_message_preview_send(  # pylint: disable=unused-argument
+        self, turn_context: TurnContext, action: MessagingExtensionAction
+    ) -> MessagingExtensionActionResponse:
+        activity_preview = action.bot_activity_preview[0]
+        content = activity_preview.attachments[0].content
+        data = self._get_example_data(content)
+        card = self._create_adaptive_card_preview(
+            data.question,
+            data.is_multi_select,
+            data.option1,
+            data.option2,
+            data.option3,
+        )
+        message = MessageFactory.attachment(card)
+        await turn_context.send_activity(message)
+
+    def _get_example_data(self, content: dict) -> ExampleData:
+        body = content["body"]
+        question = body[1]["text"]
+        choice_set = body[3]
+        multi_select = "isMultiSelect" in choice_set
+        option1 = choice_set["choices"][0]["value"]
+        option2 = choice_set["choices"][1]["value"]
+        option3 = choice_set["choices"][2]["value"]
+        return ExampleData(question, multi_select, option1, option2, option3)
+
+    def _create_adaptive_card_editor(
+        self,
+        user_text: str = None,
+        is_multi_select: bool = False,
+        option1: str = None,
+        option2: str = None,
+        option3: str = None,
+    ) -> Attachment:
+        return CardFactory.adaptive_card(
+            {
+                "actions": [
+                    {
+                        "data": {"submitLocation": "messagingExtensionFetchTask"},
+                        "title": "Submit",
+                        "type": "Action.Submit",
+                    }
+                ],
+                "body": [
+                    {
+                        "text": "This is an Adaptive Card within a Task Module",
+                        "type": "TextBlock",
+                        "weight": "bolder",
+                    },
+                    {"type": "TextBlock", "text": "Enter text for Question:"},
+                    {
+                        "id": "Question",
+                        "placeholder": "Question text here",
+                        "type": "Input.Text",
+                        "value": user_text,
+                    },
+                    {"type": "TextBlock", "text": "Options for Question:"},
+                    {"type": "TextBlock", "text": "Is Multi-Select:"},
+                    {
+                        "choices": [
+                            {"title": "True", "value": "true"},
+                            {"title": "False", "value": "false"},
+                        ],
+                        "id": "MultiSelect",
+                        "isMultiSelect": "false",
+                        "style": "expanded",
+                        "type": "Input.ChoiceSet",
+                        "value": "true" if is_multi_select else "false",
+                    },
+                    {
+                        "id": "Option1",
+                        "placeholder": "Option 1 here",
+                        "type": "Input.Text",
+                        "value": option1,
+                    },
+                    {
+                        "id": "Option2",
+                        "placeholder": "Option 2 here",
+                        "type": "Input.Text",
+                        "value": option2,
+                    },
+                    {
+                        "id": "Option3",
+                        "placeholder": "Option 3 here",
+                        "type": "Input.Text",
+                        "value": option3,
+                    },
+                ],
+                "type": "AdaptiveCard",
+                "version": "1.0",
+            }
+        )
+
+    def _create_adaptive_card_preview(
+        self,
+        user_text: str = None,
+        is_multi_select: bool = False,
+        option1: str = None,
+        option2: str = None,
+        option3: str = None,
+    ) -> Attachment:
+        return CardFactory.adaptive_card(
+            {
+                "actions": [
+                    {
+                        "type": "Action.Submit",
+                        "title": "Submit",
+                        "data": {"submitLocation": "messagingExtensionSubmit"},
+                    }
+                ],
+                "body": [
+                    {
+                        "text": "Adaptive Card from Task Module",
+                        "type": "TextBlock",
+                        "weight": "bolder",
+                    },
+                    {"text": user_text, "type": "TextBlock", "id": "Question"},
+                    {
+                        "id": "Answer",
+                        "placeholder": "Answer here...",
+                        "type": "Input.Text",
+                    },
+                    {
+                        "choices": [
+                            {"title": option1, "value": option1},
+                            {"title": option2, "value": option2},
+                            {"title": option3, "value": option3},
+                        ],
+                        "id": "Choices",
+                        "isMultiSelect": is_multi_select,
+                        "style": "expanded",
+                        "type": "Input.ChoiceSet",
+                    },
+                ],
+                "type": "AdaptiveCard",
+                "version": "1.0",
+            }
+        )
diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/config.py b/tests/teams/scenarios/action-based-messaging-extension-fetch-task/config.py
similarity index 99%
rename from generators/app/templates/echo/{{cookiecutter.bot_name}}/config.py
rename to tests/teams/scenarios/action-based-messaging-extension-fetch-task/config.py
index 7163a79aa..6b5116fba 100644
--- a/generators/app/templates/echo/{{cookiecutter.bot_name}}/config.py
+++ b/tests/teams/scenarios/action-based-messaging-extension-fetch-task/config.py
@@ -4,6 +4,7 @@
 
 import os
 
+
 class DefaultConfig:
     """ Bot Configuration """
 
diff --git a/tests/teams/scenarios/action-based-messaging-extension-fetch-task/example_data.py b/tests/teams/scenarios/action-based-messaging-extension-fetch-task/example_data.py
new file mode 100644
index 000000000..79dede038
--- /dev/null
+++ b/tests/teams/scenarios/action-based-messaging-extension-fetch-task/example_data.py
@@ -0,0 +1,18 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+
+class ExampleData(object):
+    def __init__(
+        self,
+        question: str = None,
+        is_multi_select: bool = False,
+        option1: str = None,
+        option2: str = None,
+        option3: str = None,
+    ):
+        self.question = question
+        self.is_multi_select = is_multi_select
+        self.option1 = option1
+        self.option2 = option2
+        self.option3 = option3
diff --git a/tests/teams/scenarios/action-based-messaging-extension-fetch-task/requirements.txt b/tests/teams/scenarios/action-based-messaging-extension-fetch-task/requirements.txt
new file mode 100644
index 000000000..87eba6848
--- /dev/null
+++ b/tests/teams/scenarios/action-based-messaging-extension-fetch-task/requirements.txt
@@ -0,0 +1,2 @@
+botbuilder-core>=4.7.1
+flask>=1.0.3
diff --git a/tests/teams/scenarios/action-based-messaging-extension-fetch-task/teams_app_manifest/icon-color.png b/tests/teams/scenarios/action-based-messaging-extension-fetch-task/teams_app_manifest/icon-color.png
new file mode 100644
index 000000000..48a2de133
Binary files /dev/null and b/tests/teams/scenarios/action-based-messaging-extension-fetch-task/teams_app_manifest/icon-color.png differ
diff --git a/tests/teams/scenarios/action-based-messaging-extension-fetch-task/teams_app_manifest/icon-outline.png b/tests/teams/scenarios/action-based-messaging-extension-fetch-task/teams_app_manifest/icon-outline.png
new file mode 100644
index 000000000..dbfa92772
Binary files /dev/null and b/tests/teams/scenarios/action-based-messaging-extension-fetch-task/teams_app_manifest/icon-outline.png differ
diff --git a/tests/teams/scenarios/action-based-messaging-extension-fetch-task/teams_app_manifest/manifest.json b/tests/teams/scenarios/action-based-messaging-extension-fetch-task/teams_app_manifest/manifest.json
new file mode 100644
index 000000000..8c87f9f40
--- /dev/null
+++ b/tests/teams/scenarios/action-based-messaging-extension-fetch-task/teams_app_manifest/manifest.json
@@ -0,0 +1,67 @@
+{
+  "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json",
+  "manifestVersion": "1.5",
+  "version": "1.0",
+  "id": "<>",
+  "packageName": "com.microsoft.teams.samples",
+  "developer": {
+    "name": "Microsoft",
+    "websiteUrl": "https://dev.botframework.com",
+    "privacyUrl": "https://privacy.microsoft.com",
+    "termsOfUseUrl": "https://www.microsoft.com/en-us/legal/intellectualproperty/copyright/default.aspx"
+  },
+  "icons": {
+    "color": "icon-color.png",
+    "outline": "icon-outline.png"
+  },
+  "name": {
+    "short": "Preview Messaging Extension",
+    "full": "Microsoft Teams Action Based Messaging Extension with Preview"
+  },
+  "description": {
+    "short": "Sample demonstrating an Action Based Messaging Extension with Preview",
+    "full": "Sample Action Messaging Extension built with the Bot Builder SDK demonstrating Preview"
+  },
+  "accentColor": "#FFFFFF",
+  "bots": [
+    {
+      "botId": "<>",
+      "scopes": [
+        "team"
+      ]
+    }
+  ],
+  "composeExtensions": [
+    {
+      "botId": "<>",
+      "canUpdateConfiguration": false,
+      "commands": [
+        {
+          "id": "createWithPreview",
+          "type": "action",
+          "title": "Create Card",
+          "description": "Example of creating a Card",
+          "initialRun": false,
+          "fetchTask": true,
+          "context": [
+            "commandBox",
+            "compose",
+            "message"
+          ],
+          "parameters": [
+            {
+              "name": "param",
+              "title": "param",
+              "description": ""
+            }
+          ]
+        }
+      ]
+    }
+  ],
+  "permissions": [
+    "identity",
+    "messageTeamMembers"
+  ],
+  "validDomains": []
+}
\ No newline at end of file
diff --git a/tests/teams/scenarios/action-based-messaging-extension/app.py b/tests/teams/scenarios/action-based-messaging-extension/app.py
new file mode 100644
index 000000000..a65ff81f1
--- /dev/null
+++ b/tests/teams/scenarios/action-based-messaging-extension/app.py
@@ -0,0 +1,89 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import json
+import sys
+from datetime import datetime
+
+from aiohttp import web
+from aiohttp.web import Request, Response, json_response
+from botbuilder.core import (
+    BotFrameworkAdapterSettings,
+    TurnContext,
+    BotFrameworkAdapter,
+)
+from botbuilder.schema import Activity, ActivityTypes
+from bots import TeamsMessagingExtensionsActionBot
+from config import DefaultConfig
+
+CONFIG = DefaultConfig()
+
+# Create adapter.
+# See https://aka.ms/about-bot-adapter to learn more about how bots work.
+SETTINGS = BotFrameworkAdapterSettings(CONFIG.APP_ID, CONFIG.APP_PASSWORD)
+ADAPTER = BotFrameworkAdapter(SETTINGS)
+
+
+# Catch-all for errors.
+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)
+
+    # Send a message to the user
+    await context.send_activity("The bot encountered an error or bug.")
+    await context.send_activity(
+        "To continue to run this bot, please fix the bot source code."
+    )
+    # Send a trace activity if we're talking to the Bot Framework Emulator
+    if context.activity.channel_id == "emulator":
+        # Create a trace activity that contains the error object
+        trace_activity = Activity(
+            label="TurnError",
+            name="on_turn_error Trace",
+            timestamp=datetime.utcnow(),
+            type=ActivityTypes.trace,
+            value=f"{error}",
+            value_type="https://www.botframework.com/schemas/error",
+        )
+        # Send a trace activity, which will be displayed in Bot Framework Emulator
+        await context.send_activity(trace_activity)
+
+
+ADAPTER.on_turn_error = on_error
+
+# Create the Bot
+BOT = TeamsMessagingExtensionsActionBot()
+
+
+# Listen for incoming requests on /api/messages
+async def messages(req: Request) -> Response:
+    # Main bot message handler.
+    if "application/json" in req.headers["Content-Type"]:
+        body = await req.json()
+    else:
+        return Response(status=415)
+
+    activity = Activity().deserialize(body)
+    auth_header = req.headers["Authorization"] if "Authorization" in req.headers else ""
+
+    try:
+        invoke_response = await ADAPTER.process_activity(activity, auth_header, BOT.on_turn)
+        if invoke_response:
+            return json_response(data=invoke_response.body, status=invoke_response.status)
+        return Response(status=201)
+    except PermissionError:
+        return Response(status=401)
+    except Exception:
+        return Response(status=500)
+
+
+APP = web.Application()
+APP.router.add_post("/api/messages", messages)
+
+if __name__ == "__main__":
+    try:
+        web.run_app(APP, host="localhost", port=CONFIG.PORT)
+    except Exception as error:
+        raise error
diff --git a/tests/teams/scenarios/action-based-messaging-extension/bots/__init__.py b/tests/teams/scenarios/action-based-messaging-extension/bots/__init__.py
new file mode 100644
index 000000000..f67c560a6
--- /dev/null
+++ b/tests/teams/scenarios/action-based-messaging-extension/bots/__init__.py
@@ -0,0 +1,6 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from .teams_messaging_extensions_action_bot import TeamsMessagingExtensionsActionBot
+
+__all__ = ["TeamsMessagingExtensionsActionBot"]
diff --git a/tests/teams/scenarios/action-based-messaging-extension/bots/teams_messaging_extensions_action_bot.py b/tests/teams/scenarios/action-based-messaging-extension/bots/teams_messaging_extensions_action_bot.py
new file mode 100644
index 000000000..014e992a0
--- /dev/null
+++ b/tests/teams/scenarios/action-based-messaging-extension/bots/teams_messaging_extensions_action_bot.py
@@ -0,0 +1,92 @@
+# Copyright (c) Microsoft Corp. All rights reserved.
+# Licensed under the MIT License.
+
+from typing import List
+import random
+from botbuilder.core import (
+    CardFactory,
+    MessageFactory,
+    TurnContext,
+    UserState,
+    ConversationState,
+    PrivateConversationState,
+)
+from botbuilder.schema import ChannelAccount, HeroCard, CardAction, CardImage
+from botbuilder.schema.teams import (
+    MessagingExtensionAction,
+    MessagingExtensionActionResponse,
+    MessagingExtensionAttachment,
+    MessagingExtensionResult,
+)
+from botbuilder.core.teams import TeamsActivityHandler, TeamsInfo
+from botbuilder.azure import CosmosDbPartitionedStorage
+
+
+class TeamsMessagingExtensionsActionBot(TeamsActivityHandler):
+    async def on_teams_messaging_extension_submit_action_dispatch(
+        self, turn_context: TurnContext, action: MessagingExtensionAction
+    ) -> MessagingExtensionActionResponse:
+        if action.command_id == "createCard":
+            return await self.create_card_command(turn_context, action)
+        elif action.command_id == "shareMessage":
+            return await self.share_message_command(turn_context, action)
+
+    async def create_card_command(
+        self, turn_context: TurnContext, action: MessagingExtensionAction
+    ) -> MessagingExtensionActionResponse:
+        title = action.data["title"]
+        subTitle = action.data["subTitle"]
+        text = action.data["text"]
+
+        card = HeroCard(title=title, subtitle=subTitle, text=text)
+        cardAttachment = CardFactory.hero_card(card)
+        attachment = MessagingExtensionAttachment(
+            content=card,
+            content_type=CardFactory.content_types.hero_card,
+            preview=cardAttachment,
+        )
+        attachments = [attachment]
+
+        extension_result = MessagingExtensionResult(
+            attachment_layout="list", type="result", attachments=attachments
+        )
+        return MessagingExtensionActionResponse(compose_extension=extension_result)
+
+    async def share_message_command(
+        self, turn_context: TurnContext, action: MessagingExtensionAction
+    ) -> MessagingExtensionActionResponse:
+        # The user has chosen to share a message by choosing the 'Share Message' context menu command.
+
+        # TODO: .user is None
+        title = "Shared Message"  # f'{action.message_payload.from_property.user.display_name} orignally sent this message:'
+        text = action.message_payload.body.content
+        card = HeroCard(title=title, text=text)
+
+        if not action.message_payload.attachments is None:
+            # This sample does not add the MessagePayload Attachments.  This is left as an
+            #  exercise for the user.
+            card.subtitle = (
+                f"({len(action.message_payload.attachments)} Attachments not included)"
+            )
+
+        # This Messaging Extension example allows the user to check a box to include an image with the
+        # shared message.  This demonstrates sending custom parameters along with the message payload.
+        include_image = action.data["includeImage"]
+        if include_image == "true":
+            image = CardImage(
+                url="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU"
+            )
+            card.images = [image]
+
+        cardAttachment = CardFactory.hero_card(card)
+        attachment = MessagingExtensionAttachment(
+            content=card,
+            content_type=CardFactory.content_types.hero_card,
+            preview=cardAttachment,
+        )
+        attachments = [attachment]
+
+        extension_result = MessagingExtensionResult(
+            attachment_layout="list", type="result", attachments=attachments
+        )
+        return MessagingExtensionActionResponse(compose_extension=extension_result)
diff --git a/tests/teams/scenarios/action-based-messaging-extension/config.py b/tests/teams/scenarios/action-based-messaging-extension/config.py
new file mode 100644
index 000000000..d66581d4c
--- /dev/null
+++ b/tests/teams/scenarios/action-based-messaging-extension/config.py
@@ -0,0 +1,13 @@
+#!/usr/bin/env python3
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+
+
+class DefaultConfig:
+    """ Bot Configuration """
+
+    PORT = 3978
+    APP_ID = os.environ.get("MicrosoftAppId", "")
+    APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "")
diff --git a/tests/teams/scenarios/action-based-messaging-extension/requirements.txt b/tests/teams/scenarios/action-based-messaging-extension/requirements.txt
new file mode 100644
index 000000000..87eba6848
--- /dev/null
+++ b/tests/teams/scenarios/action-based-messaging-extension/requirements.txt
@@ -0,0 +1,2 @@
+botbuilder-core>=4.7.1
+flask>=1.0.3
diff --git a/tests/teams/scenarios/action-based-messaging-extension/teams_app_manifest/icon-color.png b/tests/teams/scenarios/action-based-messaging-extension/teams_app_manifest/icon-color.png
new file mode 100644
index 000000000..48a2de133
Binary files /dev/null and b/tests/teams/scenarios/action-based-messaging-extension/teams_app_manifest/icon-color.png differ
diff --git a/tests/teams/scenarios/action-based-messaging-extension/teams_app_manifest/icon-outline.png b/tests/teams/scenarios/action-based-messaging-extension/teams_app_manifest/icon-outline.png
new file mode 100644
index 000000000..dbfa92772
Binary files /dev/null and b/tests/teams/scenarios/action-based-messaging-extension/teams_app_manifest/icon-outline.png differ
diff --git a/tests/teams/scenarios/action-based-messaging-extension/teams_app_manifest/manifest.json b/tests/teams/scenarios/action-based-messaging-extension/teams_app_manifest/manifest.json
new file mode 100644
index 000000000..1b24a5665
--- /dev/null
+++ b/tests/teams/scenarios/action-based-messaging-extension/teams_app_manifest/manifest.json
@@ -0,0 +1,78 @@
+{
+  "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json",
+  "manifestVersion": "1.5",
+  "version": "1.0",
+  "id": "00000000-0000-0000-0000-000000000000",
+  "packageName": "com.microsoft.teams.samples",
+  "developer": {
+    "name": "Microsoft",
+    "websiteUrl": "https://dev.botframework.com",
+    "privacyUrl": "https://privacy.microsoft.com",
+    "termsOfUseUrl": "https://www.microsoft.com/en-us/legal/intellectualproperty/copyright/default.aspx"
+  },
+  "name": {
+    "short": "Action Messaging Extension",
+    "full": "Microsoft Teams Action Based Messaging Extension"
+  },
+  "description": {
+    "short": "Sample demonstrating an Action Based Messaging Extension",
+    "full": "Sample Action Messaging Extension built with the Bot Builder SDK"
+  },
+  "icons": {
+    "outline": "icon-outline.png",
+    "color": "icon-color.png"
+  },
+  "accentColor": "#FFFFFF",
+  "composeExtensions": [
+    {
+      "botId": "00000000-0000-0000-0000-000000000000",
+      "commands": [
+        {
+          "id": "createCard",
+          "type": "action",
+          "context": [ "compose" ],
+          "description": "Command to run action to create a Card from Compose Box",
+          "title": "Create Card",
+          "parameters": [
+            {
+              "name": "title",
+              "title": "Card title",
+              "description": "Title for the card",
+              "inputType": "text"
+            },
+            {
+              "name": "subTitle",
+              "title": "Subtitle",
+              "description": "Subtitle for the card",
+              "inputType": "text"
+            },
+            {
+              "name": "text",
+              "title": "Text",
+              "description": "Text for the card",
+              "inputType": "textarea"
+            }
+          ]
+        },
+        {
+          "id": "shareMessage",
+          "type": "action",
+          "context": [ "message" ],
+          "description": "Test command to run action on message context (message sharing)",
+          "title": "Share Message",
+          "parameters": [
+            {
+              "name": "includeImage",
+              "title": "Include Image",
+              "description": "Include image in Hero Card",
+              "inputType": "toggle"
+            }
+          ]
+        }
+      ]
+    }
+  ],
+  "permissions": [
+    "identity"
+  ]
+}
diff --git a/tests/teams/scenarios/activity-update-and-delete/README.md b/tests/teams/scenarios/activity-update-and-delete/README.md
new file mode 100644
index 000000000..f1a48af72
--- /dev/null
+++ b/tests/teams/scenarios/activity-update-and-delete/README.md
@@ -0,0 +1,30 @@
+# EchoBot
+
+Bot Framework v4 echo bot sample.
+
+This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a simple bot that accepts input from the user and echoes it back.
+
+## Running the sample
+- Clone the repository
+```bash
+git clone https://github.com/Microsoft/botbuilder-python.git
+```
+- Activate your desired virtual environment
+- Bring up a terminal, navigate to `botbuilder-python\samples\02.echo-bot` folder
+- In the terminal, type `pip install -r requirements.txt`
+- In the terminal, type `python app.py`
+
+## Testing the bot using Bot Framework Emulator
+[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel.
+
+- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases)
+
+### Connect to bot using Bot Framework Emulator
+- Launch Bot Framework Emulator
+- Paste this URL in the emulator window - http://localhost:3978/api/messages
+
+## Further reading
+
+- [Bot Framework Documentation](https://docs.botframework.com)
+- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0)
+- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0)
diff --git a/tests/teams/scenarios/activity-update-and-delete/app.py b/tests/teams/scenarios/activity-update-and-delete/app.py
new file mode 100644
index 000000000..d897fb8e7
--- /dev/null
+++ b/tests/teams/scenarios/activity-update-and-delete/app.py
@@ -0,0 +1,92 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import asyncio
+import sys
+from datetime import datetime
+from types import MethodType
+
+from flask import Flask, request, Response
+from botbuilder.core import (
+    BotFrameworkAdapterSettings,
+    TurnContext,
+    BotFrameworkAdapter,
+)
+from botbuilder.schema import Activity, ActivityTypes
+
+from bots import ActivitiyUpdateAndDeleteBot
+
+# Create the loop and Flask app
+LOOP = asyncio.get_event_loop()
+APP = Flask(__name__, instance_relative_config=True)
+APP.config.from_object("config.DefaultConfig")
+
+# Create adapter.
+# See https://aka.ms/about-bot-adapter to learn more about how bots work.
+SETTINGS = BotFrameworkAdapterSettings(APP.config["APP_ID"], APP.config["APP_PASSWORD"])
+ADAPTER = BotFrameworkAdapter(SETTINGS)
+
+
+# Catch-all for errors.
+async def on_error(  # pylint: disable=unused-argument
+    self, 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)
+
+    # Send a message to the user
+    await context.send_activity("The bot encountered an error or bug.")
+    await context.send_activity(
+        "To continue to run this bot, please fix the bot source code."
+    )
+    # Send a trace activity if we're talking to the Bot Framework Emulator
+    if context.activity.channel_id == "emulator":
+        # Create a trace activity that contains the error object
+        trace_activity = Activity(
+            label="TurnError",
+            name="on_turn_error Trace",
+            timestamp=datetime.utcnow(),
+            type=ActivityTypes.trace,
+            value=f"{error}",
+            value_type="https://www.botframework.com/schemas/error",
+        )
+        # Send a trace activity, which will be displayed in Bot Framework Emulator
+        await context.send_activity(trace_activity)
+
+
+ADAPTER.on_turn_error = MethodType(on_error, ADAPTER)
+ACTIVITY_IDS = []
+# Create the Bot
+BOT = ActivitiyUpdateAndDeleteBot(ACTIVITY_IDS)
+
+# Listen for incoming requests on /api/messages.s
+@APP.route("/api/messages", methods=["POST"])
+def messages():
+    # Main bot message handler.
+    if "application/json" in request.headers["Content-Type"]:
+        body = request.json
+    else:
+        return Response(status=415)
+
+    activity = Activity().deserialize(body)
+    auth_header = (
+        request.headers["Authorization"] if "Authorization" in request.headers else ""
+    )
+
+    try:
+        task = LOOP.create_task(
+            ADAPTER.process_activity(activity, auth_header, BOT.on_turn)
+        )
+        LOOP.run_until_complete(task)
+        return Response(status=201)
+    except Exception as exception:
+        raise exception
+
+
+if __name__ == "__main__":
+    try:
+        APP.run(debug=False, port=APP.config["PORT"])  # nosec debug
+    except Exception as exception:
+        raise exception
diff --git a/tests/teams/scenarios/activity-update-and-delete/bots/__init__.py b/tests/teams/scenarios/activity-update-and-delete/bots/__init__.py
new file mode 100644
index 000000000..8aa561191
--- /dev/null
+++ b/tests/teams/scenarios/activity-update-and-delete/bots/__init__.py
@@ -0,0 +1,6 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from .activity_update_and_delete_bot import ActivitiyUpdateAndDeleteBot
+
+__all__ = ["ActivitiyUpdateAndDeleteBot"]
diff --git a/tests/teams/scenarios/activity-update-and-delete/bots/activity_update_and_delete_bot.py b/tests/teams/scenarios/activity-update-and-delete/bots/activity_update_and_delete_bot.py
new file mode 100644
index 000000000..1a90329a8
--- /dev/null
+++ b/tests/teams/scenarios/activity-update-and-delete/bots/activity_update_and_delete_bot.py
@@ -0,0 +1,33 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.core import MessageFactory, TurnContext, ActivityHandler
+
+
+class ActivitiyUpdateAndDeleteBot(ActivityHandler):
+    def __init__(self, activity_ids):
+        self.activity_ids = activity_ids
+
+    async def on_message_activity(self, turn_context: TurnContext):
+        TurnContext.remove_recipient_mention(turn_context.activity)
+        if turn_context.activity.text == "delete":
+            for activity in self.activity_ids:
+                await turn_context.delete_activity(activity)
+
+            self.activity_ids = []
+        else:
+            await self._send_message_and_log_activity_id(
+                turn_context, turn_context.activity.text
+            )
+
+            for activity_id in self.activity_ids:
+                new_activity = MessageFactory.text(turn_context.activity.text)
+                new_activity.id = activity_id
+                await turn_context.update_activity(new_activity)
+
+    async def _send_message_and_log_activity_id(
+        self, turn_context: TurnContext, text: str
+    ):
+        reply_activity = MessageFactory.text(text)
+        resource_response = await turn_context.send_activity(reply_activity)
+        self.activity_ids.append(resource_response.id)
diff --git a/tests/teams/scenarios/activity-update-and-delete/config.py b/tests/teams/scenarios/activity-update-and-delete/config.py
new file mode 100644
index 000000000..d66581d4c
--- /dev/null
+++ b/tests/teams/scenarios/activity-update-and-delete/config.py
@@ -0,0 +1,13 @@
+#!/usr/bin/env python3
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+
+
+class DefaultConfig:
+    """ Bot Configuration """
+
+    PORT = 3978
+    APP_ID = os.environ.get("MicrosoftAppId", "")
+    APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "")
diff --git a/tests/teams/scenarios/activity-update-and-delete/requirements.txt b/tests/teams/scenarios/activity-update-and-delete/requirements.txt
new file mode 100644
index 000000000..87eba6848
--- /dev/null
+++ b/tests/teams/scenarios/activity-update-and-delete/requirements.txt
@@ -0,0 +1,2 @@
+botbuilder-core>=4.7.1
+flask>=1.0.3
diff --git a/tests/teams/scenarios/activity-update-and-delete/teams_app_manifest/color.png b/tests/teams/scenarios/activity-update-and-delete/teams_app_manifest/color.png
new file mode 100644
index 000000000..48a2de133
Binary files /dev/null and b/tests/teams/scenarios/activity-update-and-delete/teams_app_manifest/color.png differ
diff --git a/tests/teams/scenarios/activity-update-and-delete/teams_app_manifest/manifest.json b/tests/teams/scenarios/activity-update-and-delete/teams_app_manifest/manifest.json
new file mode 100644
index 000000000..697a9a3e8
--- /dev/null
+++ b/tests/teams/scenarios/activity-update-and-delete/teams_app_manifest/manifest.json
@@ -0,0 +1,43 @@
+{
+  "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json",
+  "manifestVersion": "1.5",
+  "version": "1.0.0",
+  "id": "",
+  "packageName": "com.teams.sample.conversationUpdate",
+  "developer": {
+    "name": "ConversationUpdatesBot",
+    "websiteUrl": "https://www.microsoft.com",
+    "privacyUrl": "https://www.teams.com/privacy",
+    "termsOfUseUrl": "https://www.teams.com/termsofuser"
+  },
+  "icons": {
+    "color": "color.png",
+    "outline": "outline.png"
+  },
+  "name": {
+    "short": "ConversationUpdatesBot",
+    "full": "ConversationUpdatesBot"
+  },
+  "description": {
+    "short": "ConversationUpdatesBot",
+    "full": "ConversationUpdatesBot"
+  },
+  "accentColor": "#FFFFFF",
+  "bots": [
+    {
+      "botId": "",
+      "scopes": [
+        "groupchat",
+        "team",
+        "personal"
+      ],
+      "supportsFiles": false,
+      "isNotificationOnly": false
+    }
+  ],
+  "permissions": [
+    "identity",
+    "messageTeamMembers"
+  ],
+  "validDomains": []
+}
\ No newline at end of file
diff --git a/tests/teams/scenarios/activity-update-and-delete/teams_app_manifest/outline.png b/tests/teams/scenarios/activity-update-and-delete/teams_app_manifest/outline.png
new file mode 100644
index 000000000..dbfa92772
Binary files /dev/null and b/tests/teams/scenarios/activity-update-and-delete/teams_app_manifest/outline.png differ
diff --git a/tests/teams/scenarios/conversation-update/README.md b/tests/teams/scenarios/conversation-update/README.md
new file mode 100644
index 000000000..f1a48af72
--- /dev/null
+++ b/tests/teams/scenarios/conversation-update/README.md
@@ -0,0 +1,30 @@
+# EchoBot
+
+Bot Framework v4 echo bot sample.
+
+This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a simple bot that accepts input from the user and echoes it back.
+
+## Running the sample
+- Clone the repository
+```bash
+git clone https://github.com/Microsoft/botbuilder-python.git
+```
+- Activate your desired virtual environment
+- Bring up a terminal, navigate to `botbuilder-python\samples\02.echo-bot` folder
+- In the terminal, type `pip install -r requirements.txt`
+- In the terminal, type `python app.py`
+
+## Testing the bot using Bot Framework Emulator
+[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel.
+
+- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases)
+
+### Connect to bot using Bot Framework Emulator
+- Launch Bot Framework Emulator
+- Paste this URL in the emulator window - http://localhost:3978/api/messages
+
+## Further reading
+
+- [Bot Framework Documentation](https://docs.botframework.com)
+- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0)
+- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0)
diff --git a/tests/teams/scenarios/conversation-update/app.py b/tests/teams/scenarios/conversation-update/app.py
new file mode 100644
index 000000000..17590f61d
--- /dev/null
+++ b/tests/teams/scenarios/conversation-update/app.py
@@ -0,0 +1,92 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import asyncio
+import sys
+from datetime import datetime
+from types import MethodType
+
+from flask import Flask, request, Response
+from botbuilder.core import (
+    BotFrameworkAdapterSettings,
+    TurnContext,
+    BotFrameworkAdapter,
+)
+from botbuilder.schema import Activity, ActivityTypes
+
+from bots import ConversationUpdateBot
+
+# Create the loop and Flask app
+LOOP = asyncio.get_event_loop()
+APP = Flask(__name__, instance_relative_config=True)
+APP.config.from_object("config.DefaultConfig")
+
+# Create adapter.
+# See https://aka.ms/about-bot-adapter to learn more about how bots work.
+SETTINGS = BotFrameworkAdapterSettings(APP.config["APP_ID"], APP.config["APP_PASSWORD"])
+ADAPTER = BotFrameworkAdapter(SETTINGS)
+
+
+# Catch-all for errors.
+async def on_error(  # pylint: disable=unused-argument
+    self, 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)
+
+    # Send a message to the user
+    await context.send_activity("The bot encountered an error or bug.")
+    await context.send_activity(
+        "To continue to run this bot, please fix the bot source code."
+    )
+    # Send a trace activity if we're talking to the Bot Framework Emulator
+    if context.activity.channel_id == "emulator":
+        # Create a trace activity that contains the error object
+        trace_activity = Activity(
+            label="TurnError",
+            name="on_turn_error Trace",
+            timestamp=datetime.utcnow(),
+            type=ActivityTypes.trace,
+            value=f"{error}",
+            value_type="https://www.botframework.com/schemas/error",
+        )
+        # Send a trace activity, which will be displayed in Bot Framework Emulator
+        await context.send_activity(trace_activity)
+
+
+ADAPTER.on_turn_error = MethodType(on_error, ADAPTER)
+
+# Create the Bot
+BOT = ConversationUpdateBot()
+
+# Listen for incoming requests on /api/messages.s
+@APP.route("/api/messages", methods=["POST"])
+def messages():
+    # Main bot message handler.
+    if "application/json" in request.headers["Content-Type"]:
+        body = request.json
+    else:
+        return Response(status=415)
+
+    activity = Activity().deserialize(body)
+    auth_header = (
+        request.headers["Authorization"] if "Authorization" in request.headers else ""
+    )
+
+    try:
+        task = LOOP.create_task(
+            ADAPTER.process_activity(activity, auth_header, BOT.on_turn)
+        )
+        LOOP.run_until_complete(task)
+        return Response(status=201)
+    except Exception as exception:
+        raise exception
+
+
+if __name__ == "__main__":
+    try:
+        APP.run(debug=False, port=APP.config["PORT"])  # nosec debug
+    except Exception as exception:
+        raise exception
diff --git a/tests/teams/scenarios/conversation-update/bots/__init__.py b/tests/teams/scenarios/conversation-update/bots/__init__.py
new file mode 100644
index 000000000..ae2bc0930
--- /dev/null
+++ b/tests/teams/scenarios/conversation-update/bots/__init__.py
@@ -0,0 +1,6 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from .conversation_update_bot import ConversationUpdateBot
+
+__all__ = ["ConversationUpdateBot"]
diff --git a/tests/teams/scenarios/conversation-update/bots/conversation_update_bot.py b/tests/teams/scenarios/conversation-update/bots/conversation_update_bot.py
new file mode 100644
index 000000000..6522a633f
--- /dev/null
+++ b/tests/teams/scenarios/conversation-update/bots/conversation_update_bot.py
@@ -0,0 +1,56 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.core import MessageFactory, TurnContext
+from botbuilder.core.teams import TeamsActivityHandler
+from botbuilder.schema.teams import ChannelInfo, TeamInfo, TeamsChannelAccount
+
+
+class ConversationUpdateBot(TeamsActivityHandler):
+    async def on_teams_channel_created_activity(
+        self, channel_info: ChannelInfo, team_info: TeamInfo, turn_context: TurnContext
+    ):
+        return await turn_context.send_activity(
+            MessageFactory.text(
+                f"The new channel is {channel_info.name}. The channel id is {channel_info.id}"
+            )
+        )
+
+    async def on_teams_channel_deleted_activity(
+        self, channel_info: ChannelInfo, team_info: TeamInfo, turn_context: TurnContext
+    ):
+        return await turn_context.send_activity(
+            MessageFactory.text(f"The deleted channel is {channel_info.name}")
+        )
+
+    async def on_teams_channel_renamed_activity(
+        self, channel_info: ChannelInfo, team_info: TeamInfo, turn_context: TurnContext
+    ):
+        return await turn_context.send_activity(
+            MessageFactory.text(f"The new channel name is {channel_info.name}")
+        )
+
+    async def on_teams_team_renamed_activity(
+        self, team_info: TeamInfo, turn_context: TurnContext
+    ):
+        return await turn_context.send_activity(
+            MessageFactory.text(f"The new team name is {team_info.name}")
+        )
+
+    async def on_teams_members_added_activity(
+        self, teams_members_added: [TeamsChannelAccount], turn_context: TurnContext
+    ):
+        for member in teams_members_added:
+            await turn_context.send_activity(
+                MessageFactory.text(f"Welcome your new team member {member.id}")
+            )
+        return
+
+    async def on_teams_members_removed_activity(
+        self, teams_members_removed: [TeamsChannelAccount], turn_context: TurnContext
+    ):
+        for member in teams_members_removed:
+            await turn_context.send_activity(
+                MessageFactory.text(f"Say goodbye to your team member {member.id}")
+            )
+        return
diff --git a/tests/teams/scenarios/conversation-update/config.py b/tests/teams/scenarios/conversation-update/config.py
new file mode 100644
index 000000000..d66581d4c
--- /dev/null
+++ b/tests/teams/scenarios/conversation-update/config.py
@@ -0,0 +1,13 @@
+#!/usr/bin/env python3
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+
+
+class DefaultConfig:
+    """ Bot Configuration """
+
+    PORT = 3978
+    APP_ID = os.environ.get("MicrosoftAppId", "")
+    APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "")
diff --git a/tests/teams/scenarios/conversation-update/requirements.txt b/tests/teams/scenarios/conversation-update/requirements.txt
new file mode 100644
index 000000000..87eba6848
--- /dev/null
+++ b/tests/teams/scenarios/conversation-update/requirements.txt
@@ -0,0 +1,2 @@
+botbuilder-core>=4.7.1
+flask>=1.0.3
diff --git a/tests/teams/scenarios/conversation-update/teams_app_manifest/color.png b/tests/teams/scenarios/conversation-update/teams_app_manifest/color.png
new file mode 100644
index 000000000..48a2de133
Binary files /dev/null and b/tests/teams/scenarios/conversation-update/teams_app_manifest/color.png differ
diff --git a/tests/teams/scenarios/conversation-update/teams_app_manifest/manifest.json b/tests/teams/scenarios/conversation-update/teams_app_manifest/manifest.json
new file mode 100644
index 000000000..697a9a3e8
--- /dev/null
+++ b/tests/teams/scenarios/conversation-update/teams_app_manifest/manifest.json
@@ -0,0 +1,43 @@
+{
+  "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json",
+  "manifestVersion": "1.5",
+  "version": "1.0.0",
+  "id": "",
+  "packageName": "com.teams.sample.conversationUpdate",
+  "developer": {
+    "name": "ConversationUpdatesBot",
+    "websiteUrl": "https://www.microsoft.com",
+    "privacyUrl": "https://www.teams.com/privacy",
+    "termsOfUseUrl": "https://www.teams.com/termsofuser"
+  },
+  "icons": {
+    "color": "color.png",
+    "outline": "outline.png"
+  },
+  "name": {
+    "short": "ConversationUpdatesBot",
+    "full": "ConversationUpdatesBot"
+  },
+  "description": {
+    "short": "ConversationUpdatesBot",
+    "full": "ConversationUpdatesBot"
+  },
+  "accentColor": "#FFFFFF",
+  "bots": [
+    {
+      "botId": "",
+      "scopes": [
+        "groupchat",
+        "team",
+        "personal"
+      ],
+      "supportsFiles": false,
+      "isNotificationOnly": false
+    }
+  ],
+  "permissions": [
+    "identity",
+    "messageTeamMembers"
+  ],
+  "validDomains": []
+}
\ No newline at end of file
diff --git a/tests/teams/scenarios/conversation-update/teams_app_manifest/outline.png b/tests/teams/scenarios/conversation-update/teams_app_manifest/outline.png
new file mode 100644
index 000000000..dbfa92772
Binary files /dev/null and b/tests/teams/scenarios/conversation-update/teams_app_manifest/outline.png differ
diff --git a/tests/teams/scenarios/create-thread-in-channel/README.md b/tests/teams/scenarios/create-thread-in-channel/README.md
new file mode 100644
index 000000000..40e84f525
--- /dev/null
+++ b/tests/teams/scenarios/create-thread-in-channel/README.md
@@ -0,0 +1,30 @@
+# EchoBot
+
+Bot Framework v4 echo bot sample.
+
+This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a simple bot that accepts input from the user and echoes it back.
+
+## Running the sample
+- Clone the repository
+```bash
+git clone https://github.com/Microsoft/botbuilder-python.git
+```
+- Activate your desired virtual environment
+- Bring up a terminal, navigate to `botbuilder-python\samples\02.echo-bot` folder
+- In the terminal, type `pip install -r requirements.txt`
+- In the terminal, type `python app.py`
+
+## Testing the bot using Bot Framework Emulator
+[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel.
+
+- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases)
+
+### Connect to bot using Bot Framework Emulator
+- Launch Bot Framework Emulator
+- Paste this URL in the emulator window - http://localhost:3978/api/messages
+
+## Further reading
+
+- [Bot Framework Documentation](https://docs.botframework.com)
+- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0)
+- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0)
diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/app.py b/tests/teams/scenarios/create-thread-in-channel/app.py
similarity index 83%
rename from generators/app/templates/echo/{{cookiecutter.bot_name}}/app.py
rename to tests/teams/scenarios/create-thread-in-channel/app.py
index be9b70499..3c55decbe 100644
--- a/generators/app/templates/echo/{{cookiecutter.bot_name}}/app.py
+++ b/tests/teams/scenarios/create-thread-in-channel/app.py
@@ -8,12 +8,13 @@
 
 from flask import Flask, request, Response
 from botbuilder.core import (
-    BotFrameworkAdapter,
     BotFrameworkAdapterSettings,
     TurnContext,
+    BotFrameworkAdapter,
 )
 from botbuilder.schema import Activity, ActivityTypes
-from bot import MyBot
+
+from bots import CreateThreadInTeamsBot
 
 # Create the loop and Flask app
 LOOP = asyncio.get_event_loop()
@@ -25,18 +26,23 @@
 SETTINGS = BotFrameworkAdapterSettings(APP.config["APP_ID"], APP.config["APP_PASSWORD"])
 ADAPTER = BotFrameworkAdapter(SETTINGS)
 
+
 # Catch-all for errors.
-async def on_error(self, context: TurnContext, error: Exception):
+async def on_error(  # pylint: disable=unused-argument
+    self, 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)
 
     # Send a message to the user
-    await context.send_activity("The bot encounted an error or bug.")
-    await context.send_activity("To continue to run this bot, please fix the bot source code.")
+    await context.send_activity("The bot encountered an error or bug.")
+    await context.send_activity(
+        "To continue to run this bot, please fix the bot source code."
+    )
     # Send a trace activity if we're talking to the Bot Framework Emulator
-    if context.activity.channel_id == 'emulator':
+    if context.activity.channel_id == "emulator":
         # Create a trace activity that contains the error object
         trace_activity = Activity(
             label="TurnError",
@@ -44,17 +50,18 @@ async def on_error(self, context: TurnContext, error: Exception):
             timestamp=datetime.utcnow(),
             type=ActivityTypes.trace,
             value=f"{error}",
-            value_type="https://www.botframework.com/schemas/error"
+            value_type="https://www.botframework.com/schemas/error",
         )
         # Send a trace activity, which will be displayed in Bot Framework Emulator
         await context.send_activity(trace_activity)
 
+
 ADAPTER.on_turn_error = MethodType(on_error, ADAPTER)
 
-# Create the main dialog
-BOT = MyBot()
+# Create the Bot
+BOT = CreateThreadInTeamsBot(APP.config["APP_ID"])
 
-# Listen for incoming requests on /api/messages.
+# Listen for incoming requests on /api/messages.s
 @APP.route("/api/messages", methods=["POST"])
 def messages():
     # Main bot message handler.
diff --git a/tests/teams/scenarios/create-thread-in-channel/bots/__init__.py b/tests/teams/scenarios/create-thread-in-channel/bots/__init__.py
new file mode 100644
index 000000000..f5e8a121c
--- /dev/null
+++ b/tests/teams/scenarios/create-thread-in-channel/bots/__init__.py
@@ -0,0 +1,6 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from .create_thread_in_teams_bot import CreateThreadInTeamsBot
+
+__all__ = ["CreateThreadInTeamsBot"]
diff --git a/tests/teams/scenarios/create-thread-in-channel/bots/create_thread_in_teams_bot.py b/tests/teams/scenarios/create-thread-in-channel/bots/create_thread_in_teams_bot.py
new file mode 100644
index 000000000..6feca9af4
--- /dev/null
+++ b/tests/teams/scenarios/create-thread-in-channel/bots/create_thread_in_teams_bot.py
@@ -0,0 +1,24 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.core import MessageFactory, TurnContext
+from botbuilder.core.teams import (
+    teams_get_channel_id,
+    TeamsActivityHandler, 
+    TeamsInfo
+)
+
+
+class CreateThreadInTeamsBot(TeamsActivityHandler):
+    def __init__(self, id):
+        self.id = id
+
+    async def on_message_activity(self, turn_context: TurnContext):
+        message = MessageFactory.text("first message")
+        channel_id = teams_get_channel_id(turn_context.activity)
+        result = await TeamsInfo.send_message_to_teams_channel(turn_context, message, channel_id)
+
+        await turn_context.adapter.continue_conversation(result[0], self._continue_conversation_callback, self.id)
+    
+    async def _continue_conversation_callback(self, turn_context):
+        await turn_context.send_activity(MessageFactory.text("second message"))
diff --git a/generators/app/templates/empty/{{cookiecutter.bot_name}}/config.py b/tests/teams/scenarios/create-thread-in-channel/config.py
similarity index 99%
rename from generators/app/templates/empty/{{cookiecutter.bot_name}}/config.py
rename to tests/teams/scenarios/create-thread-in-channel/config.py
index 7163a79aa..6b5116fba 100644
--- a/generators/app/templates/empty/{{cookiecutter.bot_name}}/config.py
+++ b/tests/teams/scenarios/create-thread-in-channel/config.py
@@ -4,6 +4,7 @@
 
 import os
 
+
 class DefaultConfig:
     """ Bot Configuration """
 
diff --git a/tests/teams/scenarios/create-thread-in-channel/requirements.txt b/tests/teams/scenarios/create-thread-in-channel/requirements.txt
new file mode 100644
index 000000000..7e54b62ec
--- /dev/null
+++ b/tests/teams/scenarios/create-thread-in-channel/requirements.txt
@@ -0,0 +1,2 @@
+botbuilder-core>=4.4.0b1
+flask>=1.0.3
diff --git a/tests/teams/scenarios/create-thread-in-channel/teams_app_manifest/color.png b/tests/teams/scenarios/create-thread-in-channel/teams_app_manifest/color.png
new file mode 100644
index 000000000..48a2de133
Binary files /dev/null and b/tests/teams/scenarios/create-thread-in-channel/teams_app_manifest/color.png differ
diff --git a/tests/teams/scenarios/create-thread-in-channel/teams_app_manifest/manifest.json b/tests/teams/scenarios/create-thread-in-channel/teams_app_manifest/manifest.json
new file mode 100644
index 000000000..b9d5b596f
--- /dev/null
+++ b/tests/teams/scenarios/create-thread-in-channel/teams_app_manifest/manifest.json
@@ -0,0 +1,43 @@
+{
+  "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json",
+  "manifestVersion": "1.5",
+  "version": "1.0.0",
+  "id": "<>",
+  "packageName": "com.teams.sample.conversationUpdate",
+  "developer": {
+    "name": "MentionBot",
+    "websiteUrl": "https://www.microsoft.com",
+    "privacyUrl": "https://www.teams.com/privacy",
+    "termsOfUseUrl": "https://www.teams.com/termsofuser"
+  },
+  "icons": {
+    "color": "color.png",
+    "outline": "outline.png"
+  },
+  "name": {
+    "short": "MentionBot",
+    "full": "MentionBot"
+  },
+  "description": {
+    "short": "MentionBot",
+    "full": "MentionBot"
+  },
+  "accentColor": "#FFFFFF",
+  "bots": [
+    {
+      "botId": "<>",
+      "scopes": [
+        "groupchat",
+        "team",
+        "personal"
+      ],
+      "supportsFiles": false,
+      "isNotificationOnly": false
+    }
+  ],
+  "permissions": [
+    "identity",
+    "messageTeamMembers"
+  ],
+  "validDomains": []
+}
\ No newline at end of file
diff --git a/tests/teams/scenarios/create-thread-in-channel/teams_app_manifest/outline.png b/tests/teams/scenarios/create-thread-in-channel/teams_app_manifest/outline.png
new file mode 100644
index 000000000..dbfa92772
Binary files /dev/null and b/tests/teams/scenarios/create-thread-in-channel/teams_app_manifest/outline.png differ
diff --git a/tests/teams/scenarios/file-upload/README.md b/tests/teams/scenarios/file-upload/README.md
new file mode 100644
index 000000000..f68159779
--- /dev/null
+++ b/tests/teams/scenarios/file-upload/README.md
@@ -0,0 +1,119 @@
+# FileUpload
+
+Bot Framework v4 echo bot sample.
+
+This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a simple bot that accepts input from the user and echoes it back.
+
+## Prerequisites
+- Open Notepad (or another text editor) to save some values as you complete the setup.
+
+- Ngrok setup
+1. Download and install [Ngrok](https://ngrok.com/download)
+2. In terminal navigate to the directory where Ngrok is installed
+3. Run this command: ```ngrok http -host-header=rewrite 3978 ```
+4. Copy the https://xxxxxxxx.ngrok.io address and put it into notepad. **NOTE** You want the https address.
+
+- Azure setup
+1. Login to the [Azure Portal]((https://portal.azure.com) 
+2. (optional) create a new resource group if you don't currently have one
+3. Go to your resource group 
+4. Click "Create a new resource" 
+5. Search for "Bot Channel Registration" 
+6. Click Create 
+7. Enter bot name, subscription
+8. In the "Messaging endpoint url" enter the ngrok address from earlier. 
+8a. Finish the url with "/api/messages. It should look like ```https://xxxxxxxxx.ngrok.io/api/messages```
+9. Click the "Microsoft App Id and password" box 
+10. Click on "Create New" 
+11. Click on "Create App ID in the App Registration Portal" 
+12. Click "New registration" 
+13. Enter a name 
+14. Under "Supported account types" select "Accounts in any organizational directory and personal Microsoft accounts" 
+15. Click register 
+16. Copy the application (client) ID and put it in Notepad. Label it "Microsoft App ID" 
+17. Go to "Certificates & Secrets" 
+18. Click "+ New client secret" 
+19. Enter a description 
+20. Click "Add" 
+21. Copy the value and put it into Notepad. Label it "Password"
+22. (back in the channel registration view) Copy/Paste the Microsoft App ID and Password into their respective fields 
+23. Click Create 
+24. Go to "Resource groups" on the left 
+25. Select the resource group that the bot channel reg was created in 
+26. Select the bot channel registration 
+27. Go to Channels  
+28. Select the "Teams" icon under "Add a featured channel 
+29. Click Save 
+
+- Updating Sample Project Settings
+1. Open the project 
+2. Open config.py 
+3. Enter the app id under the ```MicrosoftAppId``` and the password under the ```MicrosoftAppPassword```
+4. Save the close the file 
+5. Under the teams_app_manifest folder open the manifest.json file 
+6. Update the ```botId``` with the Microsoft App ID from before 
+7. Update the ```id``` with the Microsoft App ID from before
+8. Save the close the file  
+
+- Uploading the bot to Teams
+1. In file explorer navigate to the TeamsAppManifest folder in the project 
+2. Select the 3 files and zip them 
+3. Open Teams 
+4. Click on "Apps" 
+5. Select "Upload a custom app" on the left at the bottom 
+6. Select the zip  
+7. Select for you  
+8. (optionally) click install if prompted 
+9. Click open 
+
+## To try this sample
+
+- Clone the repository
+
+    ```bash
+    git clone https://github.com/Microsoft/botbuilder-python.git
+    ```
+
+- In a terminal, navigate to `samples/python/scenarios/file-upload`
+
+  - From a terminal
+
+  ```bash
+  pip install -r requirements.txt
+  python app.py
+  ```
+  
+- Interacting with the bot
+1. Send a message to your bot in Teams
+2. Confirm you are getting a 200 back in Ngrok
+3. Click Accept on the card that is shown
+4. Confirm you see a 2nd 200 in Ngrok
+5. In Teams go to Files -> OneDrive -> Applications
+
+## Testing the bot using Bot Framework Emulator
+
+[Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel.
+
+- Install the Bot Framework Emulator version 4.3.0 or greater from [here](https://github.com/Microsoft/BotFramework-Emulator/releases)
+
+### Connect to the bot using Bot Framework Emulator
+
+- Launch Bot Framework Emulator
+- File -> Open Bot
+- Enter a Bot URL of `http://localhost:3978/api/messages`
+
+## Deploy the bot to Azure
+
+To learn more about deploying a bot to Azure, see [Deploy your bot to Azure](https://aka.ms/azuredeployment) for a complete list of deployment instructions.
+
+## Further reading
+
+- [Bot Framework Documentation](https://docs.botframework.com)
+- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0)
+- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0)
+- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0)
+- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0)
+- [Azure CLI](https://docs.microsoft.com/cli/azure/?view=azure-cli-latest)
+- [Azure Portal](https://portal.azure.com)
+- [Language Understanding using LUIS](https://docs.microsoft.com/en-us/azure/cognitive-services/luis/)
+- [Channels and Bot Connector Service](https://docs.microsoft.com/en-us/azure/bot-service/bot-concepts?view=azure-bot-service-4.0)
diff --git a/tests/teams/scenarios/file-upload/app.py b/tests/teams/scenarios/file-upload/app.py
new file mode 100644
index 000000000..17cbac17b
--- /dev/null
+++ b/tests/teams/scenarios/file-upload/app.py
@@ -0,0 +1,91 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import asyncio
+import sys
+import traceback
+from datetime import datetime
+
+from flask import Flask, request, Response
+from botbuilder.core import (
+    BotFrameworkAdapterSettings,
+    TurnContext,
+    BotFrameworkAdapter,
+)
+from botbuilder.schema import Activity, ActivityTypes
+
+from bots import TeamsFileBot
+
+# Create the loop and Flask app
+LOOP = asyncio.get_event_loop()
+APP = Flask(__name__, instance_relative_config=True)
+APP.config.from_object("config.DefaultConfig")
+
+# Create adapter.
+# See https://aka.ms/about-bot-adapter to learn more about how bots work.
+SETTINGS = BotFrameworkAdapterSettings(APP.config["APP_ID"], APP.config["APP_PASSWORD"])
+ADAPTER = BotFrameworkAdapter(SETTINGS)
+
+
+# Catch-all for errors.
+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)
+    print(traceback.format_exc())
+
+    # Send a message to the user
+    await context.send_activity("The bot encountered an error or bug.")
+    await context.send_activity(
+        "To continue to run this bot, please fix the bot source code."
+    )
+    # Send a trace activity if we're talking to the Bot Framework Emulator
+    if context.activity.channel_id == "emulator":
+        # Create a trace activity that contains the error object
+        trace_activity = Activity(
+            label="TurnError",
+            name="on_turn_error Trace",
+            timestamp=datetime.utcnow(),
+            type=ActivityTypes.trace,
+            value=f"{error}",
+            value_type="https://www.botframework.com/schemas/error",
+        )
+        # Send a trace activity, which will be displayed in Bot Framework Emulator
+        await context.send_activity(trace_activity)
+
+
+ADAPTER.on_turn_error = on_error
+
+# Create the Bot
+BOT = TeamsFileBot()
+
+# Listen for incoming requests on /api/messages.s
+@APP.route("/api/messages", methods=["POST"])
+def messages():
+    # Main bot message handler.
+    if "application/json" in request.headers["Content-Type"]:
+        body = request.json
+    else:
+        return Response(status=415)
+
+    activity = Activity().deserialize(body)
+    auth_header = (
+        request.headers["Authorization"] if "Authorization" in request.headers else ""
+    )
+
+    try:
+        task = LOOP.create_task(
+            ADAPTER.process_activity(activity, auth_header, BOT.on_turn)
+        )
+        LOOP.run_until_complete(task)
+        return Response(status=201)
+    except Exception as exception:
+        raise exception
+
+
+if __name__ == "__main__":
+    try:
+        APP.run(debug=False, port=APP.config["PORT"])  # nosec debug
+    except Exception as exception:
+        raise exception
diff --git a/samples/01.console-echo/adapter/__init__.py b/tests/teams/scenarios/file-upload/bots/__init__.py
similarity index 56%
rename from samples/01.console-echo/adapter/__init__.py
rename to tests/teams/scenarios/file-upload/bots/__init__.py
index 56d4bd2ee..ba9df627e 100644
--- a/samples/01.console-echo/adapter/__init__.py
+++ b/tests/teams/scenarios/file-upload/bots/__init__.py
@@ -1,6 +1,6 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from .console_adapter import ConsoleAdapter
-
-__all__ = ["ConsoleAdapter"]
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from .teams_file_bot import TeamsFileBot
+
+__all__ = ["TeamsFileBot"]
diff --git a/tests/teams/scenarios/file-upload/bots/teams_file_bot.py b/tests/teams/scenarios/file-upload/bots/teams_file_bot.py
new file mode 100644
index 000000000..39fb047a7
--- /dev/null
+++ b/tests/teams/scenarios/file-upload/bots/teams_file_bot.py
@@ -0,0 +1,185 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from datetime import datetime
+import os
+
+import requests
+from botbuilder.core import TurnContext
+from botbuilder.core.teams import TeamsActivityHandler
+from botbuilder.schema import (
+    Activity,
+    ChannelAccount,
+    ActivityTypes,
+    ConversationAccount,
+    Attachment,
+)
+from botbuilder.schema.teams import (
+    FileDownloadInfo,
+    FileConsentCard,
+    FileConsentCardResponse,
+    FileInfoCard,
+)
+from botbuilder.schema.teams.additional_properties import ContentType
+
+
+class TeamsFileBot(TeamsActivityHandler):
+    async def on_message_activity(self, turn_context: TurnContext):
+        message_with_file_download = (
+            False
+            if not turn_context.activity.attachments
+            else turn_context.activity.attachments[0].content_type == ContentType.FILE_DOWNLOAD_INFO
+        )
+
+        if message_with_file_download:
+            # Save an uploaded file locally
+            file = turn_context.activity.attachments[0]
+            file_download = FileDownloadInfo.deserialize(file.content)
+            file_path = "files/" + file.name
+
+            response = requests.get(file_download.download_url, allow_redirects=True)
+            open(file_path, "wb").write(response.content)
+
+            reply = self._create_reply(
+                turn_context.activity, f"Complete downloading {file.name}", "xml"
+            )
+            await turn_context.send_activity(reply)
+        else:
+            # Attempt to upload a file to Teams.  This will display a confirmation to
+            # the user (Accept/Decline card).  If they accept, on_teams_file_consent_accept
+            # will be called, otherwise on_teams_file_consent_decline.
+            filename = "teams-logo.png"
+            file_path = "files/" + filename
+            file_size = os.path.getsize(file_path)
+            await self._send_file_card(turn_context, filename, file_size)
+
+    async def _send_file_card(
+            self, turn_context: TurnContext, filename: str, file_size: int
+    ):
+        """
+        Send a FileConsentCard to get permission from the user to upload a file.
+        """
+
+        consent_context = {"filename": filename}
+
+        file_card = FileConsentCard(
+            description="This is the file I want to send you",
+            size_in_bytes=file_size,
+            accept_context=consent_context,
+            decline_context=consent_context
+        )
+
+        as_attachment = Attachment(
+            content=file_card.serialize(), content_type=ContentType.FILE_CONSENT_CARD, name=filename
+        )
+
+        reply_activity = self._create_reply(turn_context.activity)
+        reply_activity.attachments = [as_attachment]
+        await turn_context.send_activity(reply_activity)
+
+    async def on_teams_file_consent_accept(
+            self,
+            turn_context: TurnContext,
+            file_consent_card_response: FileConsentCardResponse
+    ):
+        """
+        The user accepted the file upload request.  Do the actual upload now.
+        """
+
+        file_path = "files/" + file_consent_card_response.context["filename"]
+        file_size = os.path.getsize(file_path)
+
+        headers = {
+            "Content-Length": f"\"{file_size}\"",
+            "Content-Range": f"bytes 0-{file_size-1}/{file_size}"
+        }
+        response = requests.put(
+            file_consent_card_response.upload_info.upload_url, open(file_path, "rb"), headers=headers
+        )
+
+        if response.status_code != 200:
+            await self._file_upload_failed(turn_context, "Unable to upload file.")
+        else:
+            await self._file_upload_complete(turn_context, file_consent_card_response)
+
+    async def on_teams_file_consent_decline(
+            self,
+            turn_context: TurnContext,
+            file_consent_card_response: FileConsentCardResponse
+    ):
+        """
+        The user declined the file upload.
+        """
+
+        context = file_consent_card_response.context
+
+        reply = self._create_reply(
+            turn_context.activity,
+            f"Declined. We won't upload file {context['filename']}.",
+            "xml"
+        )
+        await turn_context.send_activity(reply)
+
+    async def _file_upload_complete(
+            self,
+            turn_context: TurnContext,
+            file_consent_card_response: FileConsentCardResponse
+    ):
+        """
+        The file was uploaded, so display a FileInfoCard so the user can view the
+        file in Teams.
+        """
+
+        name = file_consent_card_response.upload_info.name
+
+        download_card = FileInfoCard(
+            unique_id=file_consent_card_response.upload_info.unique_id,
+            file_type=file_consent_card_response.upload_info.file_type
+        )
+
+        as_attachment = Attachment(
+            content=download_card.serialize(),
+            content_type=ContentType.FILE_INFO_CARD,
+            name=name,
+            content_url=file_consent_card_response.upload_info.content_url
+        )
+
+        reply = self._create_reply(
+            turn_context.activity,
+            f"File uploaded. Your file {name} is ready to download",
+            "xml"
+        )
+        reply.attachments = [as_attachment]
+
+        await turn_context.send_activity(reply)
+
+    async def _file_upload_failed(self, turn_context: TurnContext, error: str):
+        reply = self._create_reply(
+            turn_context.activity,
+            f"File upload failed. Error: {error}",
+            "xml"
+        )
+        await turn_context.send_activity(reply)
+
+    def _create_reply(self, activity, text=None, text_format=None):
+        return Activity(
+            type=ActivityTypes.message,
+            timestamp=datetime.utcnow(),
+            from_property=ChannelAccount(
+                id=activity.recipient.id, name=activity.recipient.name
+            ),
+            recipient=ChannelAccount(
+                id=activity.from_property.id, name=activity.from_property.name
+            ),
+            reply_to_id=activity.id,
+            service_url=activity.service_url,
+            channel_id=activity.channel_id,
+            conversation=ConversationAccount(
+                is_group=activity.conversation.is_group,
+                id=activity.conversation.id,
+                name=activity.conversation.name,
+            ),
+            text=text or "",
+            text_format=text_format or None,
+            locale=activity.locale,
+        )
diff --git a/tests/teams/scenarios/file-upload/config.py b/tests/teams/scenarios/file-upload/config.py
new file mode 100644
index 000000000..d66581d4c
--- /dev/null
+++ b/tests/teams/scenarios/file-upload/config.py
@@ -0,0 +1,13 @@
+#!/usr/bin/env python3
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+
+
+class DefaultConfig:
+    """ Bot Configuration """
+
+    PORT = 3978
+    APP_ID = os.environ.get("MicrosoftAppId", "")
+    APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "")
diff --git a/tests/teams/scenarios/file-upload/files/teams-logo.png b/tests/teams/scenarios/file-upload/files/teams-logo.png
new file mode 100644
index 000000000..78b0a0c30
Binary files /dev/null and b/tests/teams/scenarios/file-upload/files/teams-logo.png differ
diff --git a/tests/teams/scenarios/file-upload/requirements.txt b/tests/teams/scenarios/file-upload/requirements.txt
new file mode 100644
index 000000000..8ee86105f
--- /dev/null
+++ b/tests/teams/scenarios/file-upload/requirements.txt
@@ -0,0 +1,3 @@
+requests
+botbuilder-core>=4.7.1
+flask>=1.0.3
diff --git a/tests/teams/scenarios/file-upload/teams_app_manifest/color.png b/tests/teams/scenarios/file-upload/teams_app_manifest/color.png
new file mode 100644
index 000000000..48a2de133
Binary files /dev/null and b/tests/teams/scenarios/file-upload/teams_app_manifest/color.png differ
diff --git a/tests/teams/scenarios/file-upload/teams_app_manifest/manifest.json b/tests/teams/scenarios/file-upload/teams_app_manifest/manifest.json
new file mode 100644
index 000000000..f6941c176
--- /dev/null
+++ b/tests/teams/scenarios/file-upload/teams_app_manifest/manifest.json
@@ -0,0 +1,38 @@
+{
+  "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json",
+  "manifestVersion": "1.5",
+  "version": "1.0",
+  "id": "<>",
+  "packageName": "com.microsoft.teams.samples.fileUpload",
+  "developer": {
+    "name": "Microsoft Corp",
+    "websiteUrl": "https://example.azurewebsites.net",
+    "privacyUrl": "https://example.azurewebsites.net/privacy",
+    "termsOfUseUrl": "https://example.azurewebsites.net/termsofuse"
+  },
+  "name": {
+    "short": "V4 File Sample",
+    "full": "Microsoft Teams V4 File Sample Bot"
+  },
+  "description": {
+    "short": "Sample bot using V4 SDK to demo bot file features",
+    "full": "Sample bot using V4 Bot Builder SDK and V4 Microsoft Teams Extension SDK to demo bot file features"
+  },
+  "icons": {
+    "outline": "outline.png",
+    "color": "color.png"
+  },
+  "accentColor": "#abcdef",
+  "bots": [
+    {
+      "botId": "<>",
+      "scopes": [
+        "personal"
+      ],
+      "supportsFiles": true
+    }
+  ],
+  "validDomains": [
+    "*.azurewebsites.net"
+  ]
+}
\ No newline at end of file
diff --git a/tests/teams/scenarios/file-upload/teams_app_manifest/outline.png b/tests/teams/scenarios/file-upload/teams_app_manifest/outline.png
new file mode 100644
index 000000000..dbfa92772
Binary files /dev/null and b/tests/teams/scenarios/file-upload/teams_app_manifest/outline.png differ
diff --git a/tests/teams/scenarios/link-unfurling/README.md b/tests/teams/scenarios/link-unfurling/README.md
new file mode 100644
index 000000000..eecb8fccb
--- /dev/null
+++ b/tests/teams/scenarios/link-unfurling/README.md
@@ -0,0 +1,30 @@
+# RosterBot
+
+Bot Framework v4 teams roster bot sample.
+
+This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a simple bot that accepts input from the user and echoes it back.
+
+## Running the sample
+- Clone the repository
+```bash
+git clone https://github.com/Microsoft/botbuilder-python.git
+```
+- Activate your desired virtual environment
+- Bring up a terminal, navigate to `botbuilder-python\samples\roster` folder
+- In the terminal, type `pip install -r requirements.txt`
+- In the terminal, type `python app.py`
+
+## Testing the bot using Bot Framework Emulator
+[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel.
+
+- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases)
+
+### Connect to bot using Bot Framework Emulator
+- Launch Bot Framework Emulator
+- Paste this URL in the emulator window - http://localhost:3978/api/messages
+
+## Further reading
+
+- [Bot Framework Documentation](https://docs.botframework.com)
+- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0)
+- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0)
diff --git a/tests/teams/scenarios/link-unfurling/app.py b/tests/teams/scenarios/link-unfurling/app.py
new file mode 100644
index 000000000..709bffd0f
--- /dev/null
+++ b/tests/teams/scenarios/link-unfurling/app.py
@@ -0,0 +1,86 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import json
+import sys
+from datetime import datetime
+
+from aiohttp import web
+from aiohttp.web import Request, Response, json_response
+
+from botbuilder.core import (
+    BotFrameworkAdapterSettings,
+    TurnContext,
+    BotFrameworkAdapter,
+)
+
+from botbuilder.schema import Activity, ActivityTypes
+from bots import LinkUnfurlingBot
+from config import DefaultConfig
+
+CONFIG = DefaultConfig()
+
+# Create adapter.
+# See https://aka.ms/about-bot-adapter to learn more about how bots work.
+SETTINGS = BotFrameworkAdapterSettings(CONFIG.APP_ID, CONFIG.APP_PASSWORD)
+ADAPTER = BotFrameworkAdapter(SETTINGS)
+
+# Catch-all for errors.
+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)
+
+    # Send a message to the user
+    await context.send_activity("The bot encountered an error or bug.")
+    await context.send_activity(
+        "To continue to run this bot, please fix the bot source code."
+    )
+    # Send a trace activity if we're talking to the Bot Framework Emulator
+    if context.activity.channel_id == "emulator":
+        # Create a trace activity that contains the error object
+        trace_activity = Activity(
+            label="TurnError",
+            name="on_turn_error Trace",
+            timestamp=datetime.utcnow(),
+            type=ActivityTypes.trace,
+            value=f"{error}",
+            value_type="https://www.botframework.com/schemas/error",
+        )
+
+        # Send a trace activity, which will be displayed in Bot Framework Emulator
+        await context.send_activity(trace_activity)
+
+ADAPTER.on_turn_error = on_error
+
+# Create the Bot
+BOT = LinkUnfurlingBot()
+
+# Listen for incoming requests on /api/messages
+async def messages(req: Request) -> Response:
+    # Main bot message handler.
+    if "application/json" in req.headers["Content-Type"]:
+        body = await req.json()
+    else:
+        return Response(status=415)
+
+    activity = Activity().deserialize(body)
+    auth_header = req.headers["Authorization"] if "Authorization" in req.headers else ""
+
+    try:
+        response = await ADAPTER.process_activity(activity, auth_header, BOT.on_turn)
+        if response:
+            return json_response(data=response.body, status=response.status)
+        return Response(status=201)
+    except Exception as exception:
+        raise exception
+
+APP = web.Application()
+APP.router.add_post("/api/messages", messages)
+
+if __name__ == "__main__":
+    try:
+        web.run_app(APP, host="localhost", port=CONFIG.PORT)
+    except Exception as error:
+        raise error
diff --git a/tests/teams/scenarios/link-unfurling/bots/__init__.py b/tests/teams/scenarios/link-unfurling/bots/__init__.py
new file mode 100644
index 000000000..40e14fad9
--- /dev/null
+++ b/tests/teams/scenarios/link-unfurling/bots/__init__.py
@@ -0,0 +1,6 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from .link_unfurling_bot import LinkUnfurlingBot
+
+__all__ = ["LinkUnfurlingBot"]
diff --git a/tests/teams/scenarios/link-unfurling/bots/link_unfurling_bot.py b/tests/teams/scenarios/link-unfurling/bots/link_unfurling_bot.py
new file mode 100644
index 000000000..5dec7e21b
--- /dev/null
+++ b/tests/teams/scenarios/link-unfurling/bots/link_unfurling_bot.py
@@ -0,0 +1,57 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.core import CardFactory, MessageFactory, TurnContext
+from botbuilder.schema import ChannelAccount, ThumbnailCard, CardImage, HeroCard, Attachment
+from botbuilder.schema.teams import AppBasedLinkQuery, MessagingExtensionAttachment, MessagingExtensionQuery, MessagingExtensionResult, MessagingExtensionResponse
+from botbuilder.core.teams import TeamsActivityHandler, TeamsInfo
+
+class LinkUnfurlingBot(TeamsActivityHandler):
+    async def on_teams_app_based_link_query(self, turn_context: TurnContext, query: AppBasedLinkQuery):
+        hero_card = ThumbnailCard(
+            title="Thumnnail card",
+            text=query.url,
+            images=[
+                    CardImage(
+                        url="https://raw.githubusercontent.com/microsoft/botframework-sdk/master/icon.png"
+                    )
+                ]
+        )
+        attachments = MessagingExtensionAttachment(
+                                        content_type=CardFactory.content_types.hero_card,
+                                        content=hero_card)
+        result = MessagingExtensionResult(
+            attachment_layout="list",
+            type="result",
+            attachments=[attachments]
+        )
+        return MessagingExtensionResponse(compose_extension=result)
+    
+    async def on_teams_messaging_extension_query(self, turn_context: TurnContext, query: MessagingExtensionQuery):
+        if query.command_id == "searchQuery":
+            card = HeroCard(
+                title="This is a Link Unfurling Sample",
+                subtitle="It will unfurl links from *.botframework.com",
+                text="This sample demonstrates how to handle link unfurling in Teams. Please review the readme for more information."
+            )
+            attachment = Attachment(
+                                content_type=CardFactory.content_types.hero_card,
+                                content=card
+                            )
+            msg_ext_atc = MessagingExtensionAttachment(
+                            content=card,
+                            content_type=CardFactory.content_types.hero_card,
+                            preview=attachment
+                        )
+            msg_ext_res = MessagingExtensionResult(
+                    attachment_layout="list",
+                    type="result",
+                    attachments=[msg_ext_atc]
+                )
+            response = MessagingExtensionResponse(
+                compose_extension=msg_ext_res
+            )
+
+            return response
+        
+        raise NotImplementedError(f"Invalid command: {query.command_id}")
\ No newline at end of file
diff --git a/tests/teams/scenarios/link-unfurling/config.py b/tests/teams/scenarios/link-unfurling/config.py
new file mode 100644
index 000000000..d66581d4c
--- /dev/null
+++ b/tests/teams/scenarios/link-unfurling/config.py
@@ -0,0 +1,13 @@
+#!/usr/bin/env python3
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+
+
+class DefaultConfig:
+    """ Bot Configuration """
+
+    PORT = 3978
+    APP_ID = os.environ.get("MicrosoftAppId", "")
+    APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "")
diff --git a/tests/teams/scenarios/link-unfurling/requirements.txt b/tests/teams/scenarios/link-unfurling/requirements.txt
new file mode 100644
index 000000000..87eba6848
--- /dev/null
+++ b/tests/teams/scenarios/link-unfurling/requirements.txt
@@ -0,0 +1,2 @@
+botbuilder-core>=4.7.1
+flask>=1.0.3
diff --git a/tests/teams/scenarios/link-unfurling/teams_app_manifest/color.png b/tests/teams/scenarios/link-unfurling/teams_app_manifest/color.png
new file mode 100644
index 000000000..48a2de133
Binary files /dev/null and b/tests/teams/scenarios/link-unfurling/teams_app_manifest/color.png differ
diff --git a/tests/teams/scenarios/link-unfurling/teams_app_manifest/manifest.json b/tests/teams/scenarios/link-unfurling/teams_app_manifest/manifest.json
new file mode 100644
index 000000000..712b303b1
--- /dev/null
+++ b/tests/teams/scenarios/link-unfurling/teams_app_manifest/manifest.json
@@ -0,0 +1,67 @@
+{
+  "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json",
+  "manifestVersion": "1.5",
+  "version": "1.0",
+  "id": "<>",
+  "packageName": "com.teams.sample.linkunfurling",
+  "developer": {
+    "name": "Link Unfurling",
+    "websiteUrl": "https://www.microsoft.com",
+    "privacyUrl": "https://www.teams.com/privacy",
+    "termsOfUseUrl": "https://www.teams.com/termsofuser"
+  },
+  "icons": {
+    "color": "color.png",
+    "outline": "outline.png"
+  },
+  "name": {
+    "short": "Link Unfurling",
+    "full": "Link Unfurling"
+  },
+  "description": {
+    "short": "Link Unfurling",
+    "full": "Link Unfurling"
+  },
+  "accentColor": "#FFFFFF",
+  "bots": [
+    {
+      "botId": "<>",
+      "scopes": [ "personal", "team" ]
+    }
+  ],
+  "composeExtensions": [
+    {
+      "botId": "<>",
+      "commands": [
+        {
+          "id": "searchQuery",
+          "context": [ "compose", "commandBox" ],
+          "description": "Test command to run query",
+          "title": "Search",
+          "type": "query",
+          "parameters": [
+            {
+              "name": "searchQuery",
+              "title": "Search Query",
+              "description": "Your search query",
+              "inputType": "text"
+            }
+          ]
+        }
+      ],
+      "messageHandlers": [
+        {
+          "type": "link",
+          "value": {
+            "domains": [
+              "microsoft.com",
+              "github.com",
+              "linkedin.com",
+              "bing.com"
+            ]
+          }
+        }
+      ]
+    }
+  ]
+}
diff --git a/tests/teams/scenarios/link-unfurling/teams_app_manifest/manifest.zip b/tests/teams/scenarios/link-unfurling/teams_app_manifest/manifest.zip
new file mode 100644
index 000000000..aaedf42c4
Binary files /dev/null and b/tests/teams/scenarios/link-unfurling/teams_app_manifest/manifest.zip differ
diff --git a/tests/teams/scenarios/link-unfurling/teams_app_manifest/outline.png b/tests/teams/scenarios/link-unfurling/teams_app_manifest/outline.png
new file mode 100644
index 000000000..dbfa92772
Binary files /dev/null and b/tests/teams/scenarios/link-unfurling/teams_app_manifest/outline.png differ
diff --git a/tests/teams/scenarios/mentions/README.md b/tests/teams/scenarios/mentions/README.md
new file mode 100644
index 000000000..f1a48af72
--- /dev/null
+++ b/tests/teams/scenarios/mentions/README.md
@@ -0,0 +1,30 @@
+# EchoBot
+
+Bot Framework v4 echo bot sample.
+
+This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a simple bot that accepts input from the user and echoes it back.
+
+## Running the sample
+- Clone the repository
+```bash
+git clone https://github.com/Microsoft/botbuilder-python.git
+```
+- Activate your desired virtual environment
+- Bring up a terminal, navigate to `botbuilder-python\samples\02.echo-bot` folder
+- In the terminal, type `pip install -r requirements.txt`
+- In the terminal, type `python app.py`
+
+## Testing the bot using Bot Framework Emulator
+[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel.
+
+- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases)
+
+### Connect to bot using Bot Framework Emulator
+- Launch Bot Framework Emulator
+- Paste this URL in the emulator window - http://localhost:3978/api/messages
+
+## Further reading
+
+- [Bot Framework Documentation](https://docs.botframework.com)
+- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0)
+- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0)
diff --git a/tests/teams/scenarios/mentions/app.py b/tests/teams/scenarios/mentions/app.py
new file mode 100644
index 000000000..b7230468e
--- /dev/null
+++ b/tests/teams/scenarios/mentions/app.py
@@ -0,0 +1,92 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import asyncio
+import sys
+from datetime import datetime
+from types import MethodType
+
+from flask import Flask, request, Response
+from botbuilder.core import (
+    BotFrameworkAdapterSettings,
+    TurnContext,
+    BotFrameworkAdapter,
+)
+from botbuilder.schema import Activity, ActivityTypes
+
+from bots import MentionBot
+
+# Create the loop and Flask app
+LOOP = asyncio.get_event_loop()
+APP = Flask(__name__, instance_relative_config=True)
+APP.config.from_object("config.DefaultConfig")
+
+# Create adapter.
+# See https://aka.ms/about-bot-adapter to learn more about how bots work.
+SETTINGS = BotFrameworkAdapterSettings(APP.config["APP_ID"], APP.config["APP_PASSWORD"])
+ADAPTER = BotFrameworkAdapter(SETTINGS)
+
+
+# Catch-all for errors.
+async def on_error(  # pylint: disable=unused-argument
+    self, 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)
+
+    # Send a message to the user
+    await context.send_activity("The bot encountered an error or bug.")
+    await context.send_activity(
+        "To continue to run this bot, please fix the bot source code."
+    )
+    # Send a trace activity if we're talking to the Bot Framework Emulator
+    if context.activity.channel_id == "emulator":
+        # Create a trace activity that contains the error object
+        trace_activity = Activity(
+            label="TurnError",
+            name="on_turn_error Trace",
+            timestamp=datetime.utcnow(),
+            type=ActivityTypes.trace,
+            value=f"{error}",
+            value_type="https://www.botframework.com/schemas/error",
+        )
+        # Send a trace activity, which will be displayed in Bot Framework Emulator
+        await context.send_activity(trace_activity)
+
+
+ADAPTER.on_turn_error = MethodType(on_error, ADAPTER)
+
+# Create the Bot
+BOT = MentionBot()
+
+# Listen for incoming requests on /api/messages.s
+@APP.route("/api/messages", methods=["POST"])
+def messages():
+    # Main bot message handler.
+    if "application/json" in request.headers["Content-Type"]:
+        body = request.json
+    else:
+        return Response(status=415)
+
+    activity = Activity().deserialize(body)
+    auth_header = (
+        request.headers["Authorization"] if "Authorization" in request.headers else ""
+    )
+
+    try:
+        task = LOOP.create_task(
+            ADAPTER.process_activity(activity, auth_header, BOT.on_turn)
+        )
+        LOOP.run_until_complete(task)
+        return Response(status=201)
+    except Exception as exception:
+        raise exception
+
+
+if __name__ == "__main__":
+    try:
+        APP.run(debug=False, port=APP.config["PORT"])  # nosec debug
+    except Exception as exception:
+        raise exception
diff --git a/tests/teams/scenarios/mentions/bots/__init__.py b/tests/teams/scenarios/mentions/bots/__init__.py
new file mode 100644
index 000000000..7acf9b841
--- /dev/null
+++ b/tests/teams/scenarios/mentions/bots/__init__.py
@@ -0,0 +1,6 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from .mention_bot import MentionBot
+
+__all__ = ["MentionBot"]
diff --git a/tests/teams/scenarios/mentions/bots/mention_bot.py b/tests/teams/scenarios/mentions/bots/mention_bot.py
new file mode 100644
index 000000000..218fb735a
--- /dev/null
+++ b/tests/teams/scenarios/mentions/bots/mention_bot.py
@@ -0,0 +1,21 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.core import MessageFactory, TurnContext
+from botbuilder.core.teams import TeamsActivityHandler
+from botbuilder.schema import Mention
+
+
+class MentionBot(TeamsActivityHandler):
+    async def on_message_activity(self, turn_context: TurnContext):
+        mention_data = {
+            "mentioned": turn_context.activity.from_property,
+            "text": f"{turn_context.activity.from_property.name}",
+            "type": "mention",
+        }
+
+        mention_object = Mention(**mention_data)
+
+        reply_activity = MessageFactory.text(f"Hello {mention_object.text}")
+        reply_activity.entities = [mention_object]
+        await turn_context.send_activity(reply_activity)
diff --git a/tests/teams/scenarios/mentions/config.py b/tests/teams/scenarios/mentions/config.py
new file mode 100644
index 000000000..d66581d4c
--- /dev/null
+++ b/tests/teams/scenarios/mentions/config.py
@@ -0,0 +1,13 @@
+#!/usr/bin/env python3
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+
+
+class DefaultConfig:
+    """ Bot Configuration """
+
+    PORT = 3978
+    APP_ID = os.environ.get("MicrosoftAppId", "")
+    APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "")
diff --git a/tests/teams/scenarios/mentions/requirements.txt b/tests/teams/scenarios/mentions/requirements.txt
new file mode 100644
index 000000000..87eba6848
--- /dev/null
+++ b/tests/teams/scenarios/mentions/requirements.txt
@@ -0,0 +1,2 @@
+botbuilder-core>=4.7.1
+flask>=1.0.3
diff --git a/tests/teams/scenarios/mentions/teams_app_manifest/color.png b/tests/teams/scenarios/mentions/teams_app_manifest/color.png
new file mode 100644
index 000000000..48a2de133
Binary files /dev/null and b/tests/teams/scenarios/mentions/teams_app_manifest/color.png differ
diff --git a/tests/teams/scenarios/mentions/teams_app_manifest/manifest.json b/tests/teams/scenarios/mentions/teams_app_manifest/manifest.json
new file mode 100644
index 000000000..035808898
--- /dev/null
+++ b/tests/teams/scenarios/mentions/teams_app_manifest/manifest.json
@@ -0,0 +1,43 @@
+{
+  "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json",
+  "manifestVersion": "1.5",
+  "version": "1.0.0",
+  "id": "<>",
+  "packageName": "com.teams.sample.conversationUpdate",
+  "developer": {
+    "name": "MentionBot",
+    "websiteUrl": "https://www.microsoft.com",
+    "privacyUrl": "https://www.teams.com/privacy",
+    "termsOfUseUrl": "https://www.teams.com/termsofuser"
+  },
+  "icons": {
+    "color": "color.png",
+    "outline": "outline.png"
+  },
+  "name": {
+    "short": "MentionBot",
+    "full": "MentionBot"
+  },
+  "description": {
+    "short": "MentionBot",
+    "full": "MentionBot"
+  },
+  "accentColor": "#FFFFFF",
+  "bots": [
+    {
+      "botId": "<>",
+      "scopes": [
+        "groupchat",
+        "team",
+        "personal"
+      ],
+      "supportsFiles": false,
+      "isNotificationOnly": false
+    }
+  ],
+  "permissions": [
+    "identity",
+    "messageTeamMembers"
+  ],
+  "validDomains": []
+}
\ No newline at end of file
diff --git a/tests/teams/scenarios/mentions/teams_app_manifest/outline.png b/tests/teams/scenarios/mentions/teams_app_manifest/outline.png
new file mode 100644
index 000000000..dbfa92772
Binary files /dev/null and b/tests/teams/scenarios/mentions/teams_app_manifest/outline.png differ
diff --git a/tests/teams/scenarios/message-reactions/README.md b/tests/teams/scenarios/message-reactions/README.md
new file mode 100644
index 000000000..f1a48af72
--- /dev/null
+++ b/tests/teams/scenarios/message-reactions/README.md
@@ -0,0 +1,30 @@
+# EchoBot
+
+Bot Framework v4 echo bot sample.
+
+This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a simple bot that accepts input from the user and echoes it back.
+
+## Running the sample
+- Clone the repository
+```bash
+git clone https://github.com/Microsoft/botbuilder-python.git
+```
+- Activate your desired virtual environment
+- Bring up a terminal, navigate to `botbuilder-python\samples\02.echo-bot` folder
+- In the terminal, type `pip install -r requirements.txt`
+- In the terminal, type `python app.py`
+
+## Testing the bot using Bot Framework Emulator
+[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel.
+
+- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases)
+
+### Connect to bot using Bot Framework Emulator
+- Launch Bot Framework Emulator
+- Paste this URL in the emulator window - http://localhost:3978/api/messages
+
+## Further reading
+
+- [Bot Framework Documentation](https://docs.botframework.com)
+- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0)
+- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0)
diff --git a/tests/teams/scenarios/message-reactions/activity_log.py b/tests/teams/scenarios/message-reactions/activity_log.py
new file mode 100644
index 000000000..c12276bb0
--- /dev/null
+++ b/tests/teams/scenarios/message-reactions/activity_log.py
@@ -0,0 +1,30 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.core import MemoryStorage
+from botbuilder.schema import Activity
+
+
+class ActivityLog:
+    def __init__(self, storage: MemoryStorage):
+        self._storage = storage
+
+    async def append(self, activity_id: str, activity: Activity):
+        if not activity_id:
+            raise TypeError("activity_id is required for ActivityLog.append")
+
+        if not activity:
+            raise TypeError("activity is required for ActivityLog.append")
+
+        obj = {}
+        obj[activity_id] = activity
+
+        await self._storage.write(obj)
+        return
+
+    async def find(self, activity_id: str) -> Activity:
+        if not activity_id:
+            raise TypeError("activity_id is required for ActivityLog.find")
+
+        items = await self._storage.read([activity_id])
+        return items[activity_id] if len(items) >= 1 else None
diff --git a/tests/teams/scenarios/message-reactions/app.py b/tests/teams/scenarios/message-reactions/app.py
new file mode 100644
index 000000000..93b78e957
--- /dev/null
+++ b/tests/teams/scenarios/message-reactions/app.py
@@ -0,0 +1,94 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import sys
+from datetime import datetime
+from types import MethodType
+
+from flask import Flask, request, Response
+from botbuilder.core import (
+    BotFrameworkAdapterSettings,
+    TurnContext,
+    BotFrameworkAdapter,
+    MemoryStorage,
+)
+from botbuilder.schema import Activity, ActivityTypes
+from activity_log import ActivityLog
+from bots import MessageReactionBot
+from threading_helper import run_coroutine
+
+# Create the Flask app
+APP = Flask(__name__, instance_relative_config=True)
+APP.config.from_object("config.DefaultConfig")
+
+# Create adapter.
+# See https://aka.ms/about-bot-adapter to learn more about how bots work.
+SETTINGS = BotFrameworkAdapterSettings(APP.config["APP_ID"], APP.config["APP_PASSWORD"])
+ADAPTER = BotFrameworkAdapter(SETTINGS)
+
+
+# Catch-all for errors.
+async def on_error(  # pylint: disable=unused-argument
+    self, 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)
+
+    # Send a message to the user
+    await context.send_activity("The bot encountered an error or bug.")
+    await context.send_activity(
+        "To continue to run this bot, please fix the bot source code."
+    )
+    # Send a trace activity if we're talking to the Bot Framework Emulator
+    if context.activity.channel_id == "emulator":
+        # Create a trace activity that contains the error object
+        trace_activity = Activity(
+            label="TurnError",
+            name="on_turn_error Trace",
+            timestamp=datetime.utcnow(),
+            type=ActivityTypes.trace,
+            value=f"{error}",
+            value_type="https://www.botframework.com/schemas/error",
+        )
+        # Send a trace activity, which will be displayed in Bot Framework Emulator
+        await context.send_activity(trace_activity)
+
+
+ADAPTER.on_turn_error = MethodType(on_error, ADAPTER)
+
+MEMORY = MemoryStorage()
+ACTIVITY_LOG = ActivityLog(MEMORY)
+# Create the Bot
+BOT = MessageReactionBot(ACTIVITY_LOG)
+
+# Listen for incoming requests on /api/messages.s
+@APP.route("/api/messages", methods=["POST"])
+def messages():
+    # Main bot message handler.
+    if "application/json" in request.headers["Content-Type"]:
+        body = request.json
+    else:
+        return Response(status=415)
+
+    activity = Activity().deserialize(body)
+    auth_header = (
+        request.headers["Authorization"] if "Authorization" in request.headers else ""
+    )
+
+    try:
+        print("about to create task")
+        print("about to run until complete")
+        run_coroutine(ADAPTER.process_activity(activity, auth_header, BOT.on_turn))
+        print("is now complete")
+        return Response(status=201)
+    except Exception as exception:
+        raise exception
+
+
+if __name__ == "__main__":
+    try:
+        APP.run(debug=False, port=APP.config["PORT"])  # nosec debug
+    except Exception as exception:
+        raise exception
diff --git a/tests/teams/scenarios/message-reactions/bots/__init__.py b/tests/teams/scenarios/message-reactions/bots/__init__.py
new file mode 100644
index 000000000..39b49a20c
--- /dev/null
+++ b/tests/teams/scenarios/message-reactions/bots/__init__.py
@@ -0,0 +1,6 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from .message_reaction_bot import MessageReactionBot
+
+__all__ = ["MessageReactionBot"]
diff --git a/tests/teams/scenarios/message-reactions/bots/message_reaction_bot.py b/tests/teams/scenarios/message-reactions/bots/message_reaction_bot.py
new file mode 100644
index 000000000..5b585e270
--- /dev/null
+++ b/tests/teams/scenarios/message-reactions/bots/message_reaction_bot.py
@@ -0,0 +1,60 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from typing import List
+from botbuilder.core import MessageFactory, TurnContext, ActivityHandler
+from botbuilder.schema import MessageReaction
+from activity_log import ActivityLog
+
+
+class MessageReactionBot(ActivityHandler):
+    def __init__(self, activity_log: ActivityLog):
+        self._log = activity_log
+
+    async def on_reactions_added(
+        self, message_reactions: List[MessageReaction], turn_context: TurnContext
+    ):
+        for reaction in message_reactions:
+            activity = await self._log.find(turn_context.activity.reply_to_id)
+            if not activity:
+                await self._send_message_and_log_activity_id(
+                    turn_context,
+                    f"Activity {turn_context.activity.reply_to_id} not found in log",
+                )
+            else:
+                await self._send_message_and_log_activity_id(
+                    turn_context,
+                    f"You added '{reaction.type}' regarding '{activity.text}'",
+                )
+        return
+
+    async def on_reactions_removed(
+        self, message_reactions: List[MessageReaction], turn_context: TurnContext
+    ):
+        for reaction in message_reactions:
+            activity = await self._log.find(turn_context.activity.reply_to_id)
+            if not activity:
+                await self._send_message_and_log_activity_id(
+                    turn_context,
+                    f"Activity {turn_context.activity.reply_to_id} not found in log",
+                )
+            else:
+                await self._send_message_and_log_activity_id(
+                    turn_context,
+                    f"You removed '{reaction.type}' regarding '{activity.text}'",
+                )
+        return
+
+    async def on_message_activity(self, turn_context: TurnContext):
+        await self._send_message_and_log_activity_id(
+            turn_context, f"echo: {turn_context.activity.text}"
+        )
+
+    async def _send_message_and_log_activity_id(
+        self, turn_context: TurnContext, text: str
+    ):
+        reply_activity = MessageFactory.text(text)
+        resource_response = await turn_context.send_activity(reply_activity)
+
+        await self._log.append(resource_response.id, reply_activity)
+        return
diff --git a/tests/teams/scenarios/message-reactions/config.py b/tests/teams/scenarios/message-reactions/config.py
new file mode 100644
index 000000000..aec900d57
--- /dev/null
+++ b/tests/teams/scenarios/message-reactions/config.py
@@ -0,0 +1,13 @@
+#!/usr/bin/env python3
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+
+
+class DefaultConfig:
+    """ Bot Configuration """
+
+    PORT = 3978
+    APP_ID = os.environ.get("MicrosoftAppId", "e4c570ca-189d-4fee-a81b-5466be24a557")
+    APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "bghqYKJV3709;creKFP8$@@")
diff --git a/tests/teams/scenarios/message-reactions/requirements.txt b/tests/teams/scenarios/message-reactions/requirements.txt
new file mode 100644
index 000000000..87eba6848
--- /dev/null
+++ b/tests/teams/scenarios/message-reactions/requirements.txt
@@ -0,0 +1,2 @@
+botbuilder-core>=4.7.1
+flask>=1.0.3
diff --git a/tests/teams/scenarios/message-reactions/teams_app_manifest/color.png b/tests/teams/scenarios/message-reactions/teams_app_manifest/color.png
new file mode 100644
index 000000000..48a2de133
Binary files /dev/null and b/tests/teams/scenarios/message-reactions/teams_app_manifest/color.png differ
diff --git a/tests/teams/scenarios/message-reactions/teams_app_manifest/manifest.json b/tests/teams/scenarios/message-reactions/teams_app_manifest/manifest.json
new file mode 100644
index 000000000..2b53de7e0
--- /dev/null
+++ b/tests/teams/scenarios/message-reactions/teams_app_manifest/manifest.json
@@ -0,0 +1,43 @@
+{
+  "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json",
+  "manifestVersion": "1.5",
+  "version": "1.0.0",
+  "id": "<>",
+  "packageName": "com.teams.sample.conversationUpdate",
+  "developer": {
+    "name": "MessageReactions",
+    "websiteUrl": "https://www.microsoft.com",
+    "privacyUrl": "https://www.teams.com/privacy",
+    "termsOfUseUrl": "https://www.teams.com/termsofuser"
+  },
+  "icons": {
+    "color": "color.png",
+    "outline": "outline.png"
+  },
+  "name": {
+    "short": "MessageReactions",
+    "full": "MessageReactions"
+  },
+  "description": {
+    "short": "MessageReactions",
+    "full": "MessageReactions"
+  },
+  "accentColor": "#FFFFFF",
+  "bots": [
+    {
+      "botId": "<>",
+      "scopes": [
+        "groupchat",
+        "team",
+        "personal"
+      ],
+      "supportsFiles": false,
+      "isNotificationOnly": false
+    }
+  ],
+  "permissions": [
+    "identity",
+    "messageTeamMembers"
+  ],
+  "validDomains": []
+}
\ No newline at end of file
diff --git a/tests/teams/scenarios/message-reactions/teams_app_manifest/outline.png b/tests/teams/scenarios/message-reactions/teams_app_manifest/outline.png
new file mode 100644
index 000000000..dbfa92772
Binary files /dev/null and b/tests/teams/scenarios/message-reactions/teams_app_manifest/outline.png differ
diff --git a/tests/teams/scenarios/message-reactions/threading_helper.py b/tests/teams/scenarios/message-reactions/threading_helper.py
new file mode 100644
index 000000000..04dd20ee7
--- /dev/null
+++ b/tests/teams/scenarios/message-reactions/threading_helper.py
@@ -0,0 +1,169 @@
+import asyncio
+import itertools
+import logging
+import threading
+
+# pylint: disable=invalid-name
+# pylint: disable=global-statement
+try:
+    # Python 3.8 or newer has a suitable process watcher
+    asyncio.ThreadedChildWatcher
+except AttributeError:
+    # backport the Python 3.8 threaded child watcher
+    import os
+    import warnings
+
+    # Python 3.7 preferred API
+    _get_running_loop = getattr(asyncio, "get_running_loop", asyncio.get_event_loop)
+
+    class _Py38ThreadedChildWatcher(asyncio.AbstractChildWatcher):
+        def __init__(self):
+            self._pid_counter = itertools.count(0)
+            self._threads = {}
+
+        def is_active(self):
+            return True
+
+        def close(self):
+            pass
+
+        def __enter__(self):
+            return self
+
+        def __exit__(self, exc_type, exc_val, exc_tb):
+            pass
+
+        def __del__(self, _warn=warnings.warn):
+            threads = [t for t in list(self._threads.values()) if t.is_alive()]
+            if threads:
+                _warn(
+                    f"{self.__class__} has registered but not finished child processes",
+                    ResourceWarning,
+                    source=self,
+                )
+
+        def add_child_handler(self, pid, callback, *args):
+            loop = _get_running_loop()
+            thread = threading.Thread(
+                target=self._do_waitpid,
+                name=f"waitpid-{next(self._pid_counter)}",
+                args=(loop, pid, callback, args),
+                daemon=True,
+            )
+            self._threads[pid] = thread
+            thread.start()
+
+        def remove_child_handler(self, pid):
+            # asyncio never calls remove_child_handler() !!!
+            # The method is no-op but is implemented because
+            # abstract base class requires it
+            return True
+
+        def attach_loop(self, loop):
+            pass
+
+        def _do_waitpid(self, loop, expected_pid, callback, args):
+            assert expected_pid > 0
+
+            try:
+                pid, status = os.waitpid(expected_pid, 0)
+            except ChildProcessError:
+                # The child process is already reaped
+                # (may happen if waitpid() is called elsewhere).
+                pid = expected_pid
+                returncode = 255
+                logger.warning(
+                    "Unknown child process pid %d, will report returncode 255", pid
+                )
+            else:
+                if os.WIFSIGNALED(status):
+                    returncode = -os.WTERMSIG(status)
+                elif os.WIFEXITED(status):
+                    returncode = os.WEXITSTATUS(status)
+                else:
+                    returncode = status
+
+                if loop.get_debug():
+                    logger.debug(
+                        "process %s exited with returncode %s", expected_pid, returncode
+                    )
+
+            if loop.is_closed():
+                logger.warning("Loop %r that handles pid %r is closed", loop, pid)
+            else:
+                loop.call_soon_threadsafe(callback, pid, returncode, *args)
+
+            self._threads.pop(expected_pid)
+
+    # add the watcher to the loop policy
+    asyncio.get_event_loop_policy().set_child_watcher(_Py38ThreadedChildWatcher())
+
+__all__ = ["EventLoopThread", "get_event_loop", "stop_event_loop", "run_coroutine"]
+
+logger = logging.getLogger(__name__)
+
+
+class EventLoopThread(threading.Thread):
+    loop = None
+    _count = itertools.count(0)
+
+    def __init__(self):
+        name = f"{type(self).__name__}-{next(self._count)}"
+        super().__init__(name=name, daemon=True)
+
+    def __repr__(self):
+        loop, r, c, d = self.loop, False, True, False
+        if loop is not None:
+            r, c, d = loop.is_running(), loop.is_closed(), loop.get_debug()
+        return (
+            f"<{type(self).__name__} {self.name} id={self.ident} "
+            f"running={r} closed={c} debug={d}>"
+        )
+
+    def run(self):
+        self.loop = loop = asyncio.new_event_loop()
+        asyncio.set_event_loop(loop)
+
+        try:
+            loop.run_forever()
+        finally:
+            try:
+                shutdown_asyncgens = loop.shutdown_asyncgens()
+            except AttributeError:
+                pass
+            else:
+                loop.run_until_complete(shutdown_asyncgens)
+            loop.close()
+            asyncio.set_event_loop(None)
+
+    def stop(self):
+        loop, self.loop = self.loop, None
+        if loop is None:
+            return
+        loop.call_soon_threadsafe(loop.stop)
+        self.join()
+
+
+_lock = threading.Lock()
+_loop_thread = None
+
+
+def get_event_loop():
+    global _loop_thread
+    with _lock:
+        if _loop_thread is None:
+            _loop_thread = EventLoopThread()
+            _loop_thread.start()
+        return _loop_thread.loop
+
+
+def stop_event_loop():
+    global _loop_thread
+    with _lock:
+        if _loop_thread is not None:
+            _loop_thread.stop()
+            _loop_thread = None
+
+
+def run_coroutine(coro):
+    return asyncio.run_coroutine_threadsafe(coro, get_event_loop())
diff --git a/tests/teams/scenarios/roster/README.md b/tests/teams/scenarios/roster/README.md
new file mode 100644
index 000000000..eecb8fccb
--- /dev/null
+++ b/tests/teams/scenarios/roster/README.md
@@ -0,0 +1,30 @@
+# RosterBot
+
+Bot Framework v4 teams roster bot sample.
+
+This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a simple bot that accepts input from the user and echoes it back.
+
+## Running the sample
+- Clone the repository
+```bash
+git clone https://github.com/Microsoft/botbuilder-python.git
+```
+- Activate your desired virtual environment
+- Bring up a terminal, navigate to `botbuilder-python\samples\roster` folder
+- In the terminal, type `pip install -r requirements.txt`
+- In the terminal, type `python app.py`
+
+## Testing the bot using Bot Framework Emulator
+[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel.
+
+- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases)
+
+### Connect to bot using Bot Framework Emulator
+- Launch Bot Framework Emulator
+- Paste this URL in the emulator window - http://localhost:3978/api/messages
+
+## Further reading
+
+- [Bot Framework Documentation](https://docs.botframework.com)
+- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0)
+- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0)
diff --git a/tests/teams/scenarios/roster/app.py b/tests/teams/scenarios/roster/app.py
new file mode 100644
index 000000000..ba575e0bf
--- /dev/null
+++ b/tests/teams/scenarios/roster/app.py
@@ -0,0 +1,92 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import asyncio
+import sys
+from datetime import datetime
+from types import MethodType
+
+from flask import Flask, request, Response
+from botbuilder.core import (
+    BotFrameworkAdapterSettings,
+    TurnContext,
+    BotFrameworkAdapter,
+)
+from botbuilder.schema import Activity, ActivityTypes
+
+from bots import RosterBot
+
+# Create the loop and Flask app
+LOOP = asyncio.get_event_loop()
+APP = Flask(__name__, instance_relative_config=True)
+APP.config.from_object("config.DefaultConfig")
+
+# Create adapter.
+# See https://aka.ms/about-bot-adapter to learn more about how bots work.
+SETTINGS = BotFrameworkAdapterSettings(APP.config["APP_ID"], APP.config["APP_PASSWORD"])
+ADAPTER = BotFrameworkAdapter(SETTINGS)
+
+
+# Catch-all for errors.
+async def on_error(  # pylint: disable=unused-argument
+        self, 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)
+
+    # Send a message to the user
+    await context.send_activity("The bot encountered an error or bug.")
+    await context.send_activity(
+        "To continue to run this bot, please fix the bot source code."
+    )
+    # Send a trace activity if we're talking to the Bot Framework Emulator
+    if context.activity.channel_id == "emulator":
+        # Create a trace activity that contains the error object
+        trace_activity = Activity(
+            label="TurnError",
+            name="on_turn_error Trace",
+            timestamp=datetime.utcnow(),
+            type=ActivityTypes.trace,
+            value=f"{error}",
+            value_type="https://www.botframework.com/schemas/error",
+        )
+        # Send a trace activity, which will be displayed in Bot Framework Emulator
+        await context.send_activity(trace_activity)
+
+
+ADAPTER.on_turn_error = MethodType(on_error, ADAPTER)
+
+# Create the Bot
+BOT = RosterBot()
+
+# Listen for incoming requests on /api/messages.s
+@APP.route("/api/messages", methods=["POST"])
+def messages():
+    # Main bot message handler.
+    if "application/json" in request.headers["Content-Type"]:
+        body = request.json
+    else:
+        return Response(status=415)
+
+    activity = Activity().deserialize(body)
+    auth_header = (
+        request.headers["Authorization"] if "Authorization" in request.headers else ""
+    )
+
+    try:
+        task = LOOP.create_task(
+            ADAPTER.process_activity(activity, auth_header, BOT.on_turn)
+        )
+        LOOP.run_until_complete(task)
+        return Response(status=201)
+    except Exception as exception:
+        raise exception
+
+
+if __name__ == "__main__":
+    try:
+        APP.run(debug=False, port=APP.config["PORT"])  # nosec debug
+    except Exception as exception:
+        raise exception
diff --git a/tests/teams/scenarios/roster/bots/__init__.py b/tests/teams/scenarios/roster/bots/__init__.py
new file mode 100644
index 000000000..44ab91a4b
--- /dev/null
+++ b/tests/teams/scenarios/roster/bots/__init__.py
@@ -0,0 +1,6 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from .roster_bot import RosterBot
+
+__all__ = ["RosterBot"]
diff --git a/tests/teams/scenarios/roster/bots/roster_bot.py b/tests/teams/scenarios/roster/bots/roster_bot.py
new file mode 100644
index 000000000..31cf75608
--- /dev/null
+++ b/tests/teams/scenarios/roster/bots/roster_bot.py
@@ -0,0 +1,66 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from typing import List
+from botbuilder.core import MessageFactory, TurnContext
+from botbuilder.schema import ChannelAccount
+from botbuilder.core.teams import TeamsActivityHandler, TeamsInfo
+
+class RosterBot(TeamsActivityHandler):
+    async def on_members_added_activity(
+        self, members_added: [ChannelAccount], turn_context: TurnContext
+    ):
+        for member in members_added:
+            if member.id != turn_context.activity.recipient.id:
+                await turn_context.send_activity(
+                    "Hello and welcome!"
+                )
+
+    async def on_message_activity(
+        self, turn_context: TurnContext
+    ):
+        await turn_context.send_activity(MessageFactory.text(f"Echo: {turn_context.activity.text}"))
+
+        text = turn_context.activity.text.strip()
+        if "members" in text:
+            await self._show_members(turn_context)
+        elif "channels" in text:
+            await self._show_channels(turn_context)
+        elif "details" in text:
+            await self._show_details(turn_context)
+        else:
+            await turn_context.send_activity(MessageFactory.text(f"Invalid command. Type \"Show channels\" to see a channel list. Type \"Show members\" to see a list of members in a team. Type \"Show details\" to see team information."))
+
+    async def _show_members(
+        self, turn_context: TurnContext
+    ):
+        members = await TeamsInfo.get_team_members(turn_context)
+        reply = MessageFactory.text(f"Total of {len(members)} members are currently in team")
+        await turn_context.send_activity(reply)
+        messages = list(map(lambda m: (f'{m.aad_object_id} --> {m.name} --> {m.user_principal_name}'), members))
+        await self._send_in_batches(turn_context, messages)
+
+    async def _show_channels(
+        self, turn_context: TurnContext
+    ):
+        channels = await TeamsInfo.get_team_channels(turn_context)
+        reply = MessageFactory.text(f"Total of {len(channels)} channels are currently in team")
+        await turn_context.send_activity(reply)
+        messages = list(map(lambda c: (f'{c.id} --> {c.name}'), channels))
+        await self._send_in_batches(turn_context, messages)
+        
+    async def _show_details(self, turn_context: TurnContext):
+        team_details = await TeamsInfo.get_team_details(turn_context)
+        reply = MessageFactory.text(f"The team name is {team_details.name}. The team ID is {team_details.id}. The AADGroupID is {team_details.aad_group_id}.")
+        await turn_context.send_activity(reply)    
+        
+    async def _send_in_batches(self, turn_context: TurnContext, messages: List[str]):
+        batch = []
+        for msg in messages:
+            batch.append(msg)
+            if len(batch) == 10:
+                await turn_context.send_activity(MessageFactory.text("
".join(batch)))
+                batch = []
+
+        if len(batch) > 0:
+            await turn_context.send_activity(MessageFactory.text("
".join(batch)))
\ No newline at end of file
diff --git a/tests/teams/scenarios/roster/config.py b/tests/teams/scenarios/roster/config.py
new file mode 100644
index 000000000..d66581d4c
--- /dev/null
+++ b/tests/teams/scenarios/roster/config.py
@@ -0,0 +1,13 @@
+#!/usr/bin/env python3
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+
+
+class DefaultConfig:
+    """ Bot Configuration """
+
+    PORT = 3978
+    APP_ID = os.environ.get("MicrosoftAppId", "")
+    APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "")
diff --git a/tests/teams/scenarios/roster/requirements.txt b/tests/teams/scenarios/roster/requirements.txt
new file mode 100644
index 000000000..87eba6848
--- /dev/null
+++ b/tests/teams/scenarios/roster/requirements.txt
@@ -0,0 +1,2 @@
+botbuilder-core>=4.7.1
+flask>=1.0.3
diff --git a/tests/teams/scenarios/roster/teams_app_manifest/color.png b/tests/teams/scenarios/roster/teams_app_manifest/color.png
new file mode 100644
index 000000000..48a2de133
Binary files /dev/null and b/tests/teams/scenarios/roster/teams_app_manifest/color.png differ
diff --git a/tests/teams/scenarios/roster/teams_app_manifest/manifest.json b/tests/teams/scenarios/roster/teams_app_manifest/manifest.json
new file mode 100644
index 000000000..c6b6582b0
--- /dev/null
+++ b/tests/teams/scenarios/roster/teams_app_manifest/manifest.json
@@ -0,0 +1,42 @@
+{
+  "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json",
+  "manifestVersion": "1.5",
+  "version": "1.0.0",
+  "id": "00000000-0000-0000-0000-000000000000",
+  "packageName": "com.teams.sample.roster",
+  "developer": {
+    "name": "TeamsRosterBot",
+    "websiteUrl": "https://www.microsoft.com",
+    "privacyUrl": "https://www.teams.com/privacy",
+    "termsOfUseUrl": "https://www.teams.com/termsofuser"
+  },
+  "icons": {
+    "color": "color.png",
+    "outline": "outline.png"
+  },
+  "name": {
+    "short": "TeamsRosterBot",
+    "full": "TeamsRosterBot"
+  },
+  "description": {
+    "short": "TeamsRosterBot",
+    "full": "TeamsRosterBot"
+  },
+  "accentColor": "#FFFFFF",
+  "bots": [
+    {
+      "botId": "00000000-0000-0000-0000-000000000000",
+      "scopes": [
+        "groupchat",
+        "team"
+      ],
+      "supportsFiles": false,
+      "isNotificationOnly": false
+    }
+  ],
+  "permissions": [
+    "identity",
+    "messageTeamMembers"
+  ],
+  "validDomains": []
+}
\ No newline at end of file
diff --git a/tests/teams/scenarios/roster/teams_app_manifest/outline.png b/tests/teams/scenarios/roster/teams_app_manifest/outline.png
new file mode 100644
index 000000000..dbfa92772
Binary files /dev/null and b/tests/teams/scenarios/roster/teams_app_manifest/outline.png differ
diff --git a/tests/teams/scenarios/search-based-messaging-extension/README.md b/tests/teams/scenarios/search-based-messaging-extension/README.md
new file mode 100644
index 000000000..eecb8fccb
--- /dev/null
+++ b/tests/teams/scenarios/search-based-messaging-extension/README.md
@@ -0,0 +1,30 @@
+# RosterBot
+
+Bot Framework v4 teams roster bot sample.
+
+This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a simple bot that accepts input from the user and echoes it back.
+
+## Running the sample
+- Clone the repository
+```bash
+git clone https://github.com/Microsoft/botbuilder-python.git
+```
+- Activate your desired virtual environment
+- Bring up a terminal, navigate to `botbuilder-python\samples\roster` folder
+- In the terminal, type `pip install -r requirements.txt`
+- In the terminal, type `python app.py`
+
+## Testing the bot using Bot Framework Emulator
+[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel.
+
+- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases)
+
+### Connect to bot using Bot Framework Emulator
+- Launch Bot Framework Emulator
+- Paste this URL in the emulator window - http://localhost:3978/api/messages
+
+## Further reading
+
+- [Bot Framework Documentation](https://docs.botframework.com)
+- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0)
+- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0)
diff --git a/tests/teams/scenarios/search-based-messaging-extension/app.py b/tests/teams/scenarios/search-based-messaging-extension/app.py
new file mode 100644
index 000000000..62c00ce20
--- /dev/null
+++ b/tests/teams/scenarios/search-based-messaging-extension/app.py
@@ -0,0 +1,83 @@
+import json
+import sys
+from datetime import datetime
+
+from aiohttp import web
+from aiohttp.web import Request, Response, json_response
+
+from botbuilder.core import (
+    BotFrameworkAdapterSettings,
+    TurnContext,
+    BotFrameworkAdapter,
+)
+
+from botbuilder.schema import Activity, ActivityTypes
+from bots import SearchBasedMessagingExtension
+from config import DefaultConfig
+
+CONFIG = DefaultConfig()
+
+# Create adapter.
+# See https://aka.ms/about-bot-adapter to learn more about how bots work.
+SETTINGS = BotFrameworkAdapterSettings(CONFIG.APP_ID, CONFIG.APP_PASSWORD)
+ADAPTER = BotFrameworkAdapter(SETTINGS)
+
+# Catch-all for errors.
+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)
+
+    # Send a message to the user
+    await context.send_activity("The bot encountered an error or bug.")
+    await context.send_activity(
+        "To continue to run this bot, please fix the bot source code."
+    )
+    # Send a trace activity if we're talking to the Bot Framework Emulator
+    if context.activity.channel_id == "emulator":
+        # Create a trace activity that contains the error object
+        trace_activity = Activity(
+            label="TurnError",
+            name="on_turn_error Trace",
+            timestamp=datetime.utcnow(),
+            type=ActivityTypes.trace,
+            value=f"{error}",
+            value_type="https://www.botframework.com/schemas/error",
+        )
+
+        # Send a trace activity, which will be displayed in Bot Framework Emulator
+        await context.send_activity(trace_activity)
+
+ADAPTER.on_turn_error = on_error
+
+# Create the Bot
+BOT = SearchBasedMessagingExtension()
+
+# Listen for incoming requests on /api/messages
+async def messages(req: Request) -> Response:
+    # Main bot message handler.
+    if "application/json" in req.headers["Content-Type"]:
+        body = await req.json()
+    else:
+        return Response(status=415)
+
+    activity = Activity().deserialize(body)
+    auth_header = req.headers["Authorization"] if "Authorization" in req.headers else ""
+
+    try:
+        response = await ADAPTER.process_activity(activity, auth_header, BOT.on_turn)
+        if response:
+            return json_response(data=response.body, status=response.status)
+        return Response(status=201)
+    except Exception as exception:
+        raise exception
+
+APP = web.Application()
+APP.router.add_post("/api/messages", messages)
+
+if __name__ == "__main__":
+    try:
+        web.run_app(APP, host="localhost", port=CONFIG.PORT)
+    except Exception as error:
+        raise error
diff --git a/tests/teams/scenarios/search-based-messaging-extension/bots/__init__.py b/tests/teams/scenarios/search-based-messaging-extension/bots/__init__.py
new file mode 100644
index 000000000..9311de37a
--- /dev/null
+++ b/tests/teams/scenarios/search-based-messaging-extension/bots/__init__.py
@@ -0,0 +1,6 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from .search_based_messaging_extension import SearchBasedMessagingExtension
+
+__all__ = ["SearchBasedMessagingExtension"]
diff --git a/tests/teams/scenarios/search-based-messaging-extension/bots/search_based_messaging_extension.py b/tests/teams/scenarios/search-based-messaging-extension/bots/search_based_messaging_extension.py
new file mode 100644
index 000000000..27db99646
--- /dev/null
+++ b/tests/teams/scenarios/search-based-messaging-extension/bots/search_based_messaging_extension.py
@@ -0,0 +1,175 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.core import CardFactory, MessageFactory, TurnContext
+from botbuilder.schema import ChannelAccount, ThumbnailCard, CardImage, HeroCard, Attachment, CardAction
+from botbuilder.schema.teams import AppBasedLinkQuery, MessagingExtensionAttachment, MessagingExtensionQuery, MessagingExtensionResult, MessagingExtensionResponse
+from botbuilder.core.teams import TeamsActivityHandler, TeamsInfo
+
+from typing import List
+import requests
+
+class SearchBasedMessagingExtension(TeamsActivityHandler):
+    async def on_message_activity(self, turn_context: TurnContext):
+        await turn_context.send_activities(MessageFactory.text(f"Echo: {turn_context.activity.text}"))
+   
+    async def on_teams_messaging_extension_query(self, turn_context: TurnContext, query: MessagingExtensionQuery):
+        search_query = str(query.parameters[0].value)
+        response = requests.get(f"http://registry.npmjs.com/-/v1/search",params={"text":search_query})
+        data = response.json()
+
+        attachments = []
+        
+        for obj in data["objects"]:
+            hero_card = HeroCard(
+                title=obj["package"]["name"],
+                tap=CardAction(
+                    type="invoke",
+                    value=obj["package"]
+                ),
+                preview=[CardImage(url=obj["package"]["links"]["npm"])]
+            )
+
+            attachment = MessagingExtensionAttachment(
+                content_type=CardFactory.content_types.hero_card,
+                content=HeroCard(title=obj["package"]["name"]),
+                preview=CardFactory.hero_card(hero_card)
+            )
+            attachments.append(attachment)
+        return MessagingExtensionResponse(
+            compose_extension=MessagingExtensionResult(
+                type="result",
+                attachment_layout="list",
+                attachments=attachments
+            )
+        )
+       
+       
+
+    async def on_teams_messaging_extension_select_item(self, turn_context: TurnContext, query) -> MessagingExtensionResponse: 
+        hero_card = HeroCard(
+            title=query["name"],
+            subtitle=query["description"],
+            buttons=[
+                CardAction(
+                    type="openUrl",
+                    value=query["links"]["npm"]
+                )
+            ]
+        )
+        attachment = MessagingExtensionAttachment(
+            content_type=CardFactory.content_types.hero_card,
+            content=hero_card
+        )
+
+        return MessagingExtensionResponse(
+            compose_extension=MessagingExtensionResult(
+                type="result",
+                attachment_layout="list",
+                attachments=[attachment]
+            )
+        )
+
+    def _create_messaging_extension_result(self, attachments: List[MessagingExtensionAttachment]) -> MessagingExtensionResult:
+        return MessagingExtensionResult(
+            type="result",
+            attachment_layout="list",
+            attachments=attachments
+        )
+    
+    def _create_search_result_attachment(self, search_query: str) -> MessagingExtensionAttachment:
+        card_text = f"You said {search_query}"
+        bf_logo = "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU"
+
+        button = CardAction(
+            type="openUrl",
+            title="Click for more Information",
+            value="https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/bots/bots-overview"
+        )
+
+        images = [CardImage(url=bf_logo)]
+        buttons = [button]
+
+        hero_card = HeroCard(
+            title="You searched for:",
+            text=card_text,
+            images=images,
+            buttons=buttons
+        )
+
+        return MessagingExtensionAttachment(
+            content_type=CardFactory.content_types.hero_card,
+            content=hero_card,
+            preview=CardFactory.hero_card(hero_card)
+        )
+    
+    def _create_dummy_search_result_attachment(self) -> MessagingExtensionAttachment:
+        card_text = "https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/bots/bots-overview"
+        bf_logo = "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU"
+
+        button = CardAction(
+                type = "openUrl",
+                title = "Click for more Information",
+                value = "https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/bots/bots-overview"
+            )
+
+        images = [CardImage(url=bf_logo)]
+        
+        buttons = [button]
+            
+
+        hero_card = HeroCard(
+            title="Learn more about Teams:", 
+            text=card_text, images=images, 
+            buttons=buttons
+        )
+
+        preview = HeroCard(
+            title="Learn more about Teams:", 
+            text=card_text, 
+            images=images
+        )
+
+        return MessagingExtensionAttachment(
+            content_type = CardFactory.content_types.hero_card,
+            content = hero_card,
+            preview = CardFactory.hero_card(preview)
+        )
+    
+    def _create_select_items_result_attachment(self, search_query: str) -> MessagingExtensionAttachment:
+        card_text = f"You said {search_query}"
+        bf_logo = "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU"
+
+        buttons = CardAction(
+            type="openUrl",
+            title="Click for more Information",
+            value="https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/bots/bots-overview"
+        )
+
+        images = [CardImage(url=bf_logo)]
+        buttons = [buttons]
+
+        select_item_tap = CardAction(
+            type="invoke",
+            value={"query": search_query}
+        )
+
+        hero_card = HeroCard(
+            title="You searched for:",
+            text=card_text,
+            images=images,
+            buttons=buttons
+        )
+
+        preview = HeroCard(
+            title=card_text,
+            text=card_text,
+            images=images,
+            tap=select_item_tap
+            )
+
+        return MessagingExtensionAttachment(
+            content_type=CardFactory.content_types.hero_card,
+            content=hero_card,
+            preview=CardFactory.hero_card(preview)
+        )
\ No newline at end of file
diff --git a/tests/teams/scenarios/search-based-messaging-extension/config.py b/tests/teams/scenarios/search-based-messaging-extension/config.py
new file mode 100644
index 000000000..d66581d4c
--- /dev/null
+++ b/tests/teams/scenarios/search-based-messaging-extension/config.py
@@ -0,0 +1,13 @@
+#!/usr/bin/env python3
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+
+
+class DefaultConfig:
+    """ Bot Configuration """
+
+    PORT = 3978
+    APP_ID = os.environ.get("MicrosoftAppId", "")
+    APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "")
diff --git a/tests/teams/scenarios/search-based-messaging-extension/requirements.txt b/tests/teams/scenarios/search-based-messaging-extension/requirements.txt
new file mode 100644
index 000000000..87eba6848
--- /dev/null
+++ b/tests/teams/scenarios/search-based-messaging-extension/requirements.txt
@@ -0,0 +1,2 @@
+botbuilder-core>=4.7.1
+flask>=1.0.3
diff --git a/tests/teams/scenarios/search-based-messaging-extension/teams_app_manifest/color.png b/tests/teams/scenarios/search-based-messaging-extension/teams_app_manifest/color.png
new file mode 100644
index 000000000..48a2de133
Binary files /dev/null and b/tests/teams/scenarios/search-based-messaging-extension/teams_app_manifest/color.png differ
diff --git a/tests/teams/scenarios/search-based-messaging-extension/teams_app_manifest/manifest.json b/tests/teams/scenarios/search-based-messaging-extension/teams_app_manifest/manifest.json
new file mode 100644
index 000000000..98bb01282
--- /dev/null
+++ b/tests/teams/scenarios/search-based-messaging-extension/teams_app_manifest/manifest.json
@@ -0,0 +1,49 @@
+{
+    "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json",
+    "manifestVersion": "1.5",
+    "version": "1.0",
+    "id": "<>",
+    "packageName": "com.microsoft.teams.samples.searchExtension",
+    "developer": {
+      "name": "Microsoft Corp",
+      "websiteUrl": "https://example.azurewebsites.net",
+      "privacyUrl": "https://example.azurewebsites.net/privacy",
+      "termsOfUseUrl": "https://example.azurewebsites.net/termsofuse"
+    },
+    "name": {
+      "short": "search-extension-settings",
+      "full": "Microsoft Teams V4 Search Messaging Extension Bot and settings"
+    },
+    "description": {
+      "short": "Microsoft Teams V4 Search Messaging Extension Bot and settings",
+      "full": "Sample Search Messaging Extension Bot using V4 Bot Builder SDK and V4 Microsoft Teams Extension SDK"
+    },
+    "icons": {
+      "outline": "icon-outline.png",
+      "color": "icon-color.png"
+    },
+    "accentColor": "#abcdef",
+    "composeExtensions": [
+      {
+        "botId": "<>",
+        "canUpdateConfiguration": true,
+        "commands": [
+          {
+            "id": "searchQuery",
+            "context": [ "compose", "commandBox" ],
+            "description": "Test command to run query",
+            "title": "Search",
+            "type": "query",
+            "parameters": [
+              {
+                "name": "searchQuery",
+                "title": "Search Query",
+                "description": "Your search query",
+                "inputType": "text"
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  }
\ No newline at end of file
diff --git a/tests/teams/scenarios/search-based-messaging-extension/teams_app_manifest/outline.png b/tests/teams/scenarios/search-based-messaging-extension/teams_app_manifest/outline.png
new file mode 100644
index 000000000..dbfa92772
Binary files /dev/null and b/tests/teams/scenarios/search-based-messaging-extension/teams_app_manifest/outline.png differ
diff --git a/tests/teams/scenarios/task-module/app.py b/tests/teams/scenarios/task-module/app.py
new file mode 100644
index 000000000..b5abfad28
--- /dev/null
+++ b/tests/teams/scenarios/task-module/app.py
@@ -0,0 +1,93 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import json
+import sys
+from datetime import datetime
+
+from aiohttp import web
+from aiohttp.web import Request, Response, json_response
+from botbuilder.core import (
+    BotFrameworkAdapterSettings,
+    TurnContext,
+    BotFrameworkAdapter,
+)
+from botbuilder.schema import Activity, ActivityTypes
+from bots import TaskModuleBot
+from config import DefaultConfig
+
+CONFIG = DefaultConfig()
+
+# Create adapter.
+# See https://aka.ms/about-bot-adapter to learn more about how bots work.
+SETTINGS = BotFrameworkAdapterSettings(CONFIG.APP_ID, CONFIG.APP_PASSWORD)
+ADAPTER = BotFrameworkAdapter(SETTINGS)
+
+
+# Catch-all for errors.
+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)
+
+    # Send a message to the user
+    await context.send_activity("The bot encountered an error or bug.")
+    await context.send_activity(
+        "To continue to run this bot, please fix the bot source code."
+    )
+    # Send a trace activity if we're talking to the Bot Framework Emulator
+    if context.activity.channel_id == "emulator":
+        # Create a trace activity that contains the error object
+        trace_activity = Activity(
+            label="TurnError",
+            name="on_turn_error Trace",
+            timestamp=datetime.utcnow(),
+            type=ActivityTypes.trace,
+            value=f"{error}",
+            value_type="https://www.botframework.com/schemas/error",
+        )
+        # Send a trace activity, which will be displayed in Bot Framework Emulator
+        await context.send_activity(trace_activity)
+
+
+ADAPTER.on_turn_error = on_error
+
+# Create the Bot
+BOT = TaskModuleBot()
+
+
+# Listen for incoming requests on /api/messages
+async def messages(req: Request) -> Response:
+    # Main bot message handler.
+    if "application/json" in req.headers["Content-Type"]:
+        body = await req.json()
+    else:
+        return Response(status=415)
+
+    activity = Activity().deserialize(body)
+    auth_header = req.headers["Authorization"] if "Authorization" in req.headers else ""
+
+    try:
+        invoke_response = await ADAPTER.process_activity(
+            activity, auth_header, BOT.on_turn
+        )
+        if invoke_response:
+            return json_response(
+                data=invoke_response.body, status=invoke_response.status
+            )
+        return Response(status=201)
+    except PermissionError:
+        return Response(status=401)
+    except Exception:
+        return Response(status=500)
+
+
+APP = web.Application()
+APP.router.add_post("/api/messages", messages)
+
+if __name__ == "__main__":
+    try:
+        web.run_app(APP, host="localhost", port=CONFIG.PORT)
+    except Exception as error:
+        raise error
diff --git a/tests/teams/scenarios/task-module/bots/__init__.py b/tests/teams/scenarios/task-module/bots/__init__.py
new file mode 100644
index 000000000..550d3aaf8
--- /dev/null
+++ b/tests/teams/scenarios/task-module/bots/__init__.py
@@ -0,0 +1,6 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from .teams_task_module_bot import TaskModuleBot
+
+__all__ = ["TaskModuleBot"]
diff --git a/tests/teams/scenarios/task-module/bots/teams_task_module_bot.py b/tests/teams/scenarios/task-module/bots/teams_task_module_bot.py
new file mode 100644
index 000000000..3c4cbde5d
--- /dev/null
+++ b/tests/teams/scenarios/task-module/bots/teams_task_module_bot.py
@@ -0,0 +1,90 @@
+# Copyright (c) Microsoft Corp. All rights reserved.
+# Licensed under the MIT License.
+
+import json
+from typing import List
+import random
+from botbuilder.core import CardFactory, MessageFactory, TurnContext
+from botbuilder.schema import (
+    ChannelAccount,
+    HeroCard,
+    CardAction,
+    CardImage,
+    Attachment,
+)
+from botbuilder.schema.teams import (
+    MessagingExtensionAction,
+    MessagingExtensionActionResponse,
+    MessagingExtensionAttachment,
+    MessagingExtensionResult,
+    TaskModuleResponse,
+    TaskModuleResponseBase,
+    TaskModuleContinueResponse,
+    TaskModuleMessageResponse,
+    TaskModuleTaskInfo,
+    TaskModuleRequest,
+)
+from botbuilder.core.teams import TeamsActivityHandler, TeamsInfo
+from botbuilder.azure import CosmosDbPartitionedStorage
+from botbuilder.core.teams.teams_helper import deserializer_helper
+
+class TaskModuleBot(TeamsActivityHandler):
+    async def on_message_activity(self, turn_context: TurnContext):
+        reply = MessageFactory.attachment(self._get_task_module_hero_card())
+        await turn_context.send_activity(reply)
+
+    def _get_task_module_hero_card(self) -> Attachment:
+        task_module_action = CardAction(
+            type="invoke",
+            title="Adaptive Card",
+            value={"type": "task/fetch", "data": "adaptivecard"},
+        )
+        card = HeroCard(
+            title="Task Module Invocation from Hero Card",
+            subtitle="This is a hero card with a Task Module Action button.  Click the button to show an Adaptive Card within a Task Module.",
+            buttons=[task_module_action],
+        )
+        return CardFactory.hero_card(card)
+
+    async def on_teams_task_module_fetch(
+        self, turn_context: TurnContext, task_module_request: TaskModuleRequest
+    ) -> TaskModuleResponse:
+        reply = MessageFactory.text(
+            f"OnTeamsTaskModuleFetchAsync TaskModuleRequest: {json.dumps(task_module_request.data)}"
+        )
+        await turn_context.send_activity(reply)
+
+        # base_response = TaskModuleResponseBase(type='continue')
+        card = CardFactory.adaptive_card(
+            {
+                "version": "1.0.0",
+                "type": "AdaptiveCard",
+                "body": [
+                    {"type": "TextBlock", "text": "Enter Text Here",},
+                    {
+                        "type": "Input.Text",
+                        "id": "usertext",
+                        "placeholder": "add some text and submit",
+                        "IsMultiline": "true",
+                    },
+                ],
+                "actions": [{"type": "Action.Submit", "title": "Submit",}],
+            }
+        )
+
+        task_info = TaskModuleTaskInfo(
+            card=card, title="Adaptive Card: Inputs", height=200, width=400
+        )
+        continue_response = TaskModuleContinueResponse(type="continue", value=task_info)
+        return TaskModuleResponse(task=continue_response)
+
+    async def on_teams_task_module_submit(
+         self, turn_context: TurnContext, task_module_request: TaskModuleRequest
+    ) -> TaskModuleResponse:
+        reply = MessageFactory.text(
+            f"on_teams_messaging_extension_submit_action_activity MessagingExtensionAction: {json.dumps(task_module_request.data)}"
+        )
+        await turn_context.send_activity(reply)
+
+        message_response = TaskModuleMessageResponse(type="message", value="Thanks!")
+        return TaskModuleResponse(task=message_response)
diff --git a/tests/teams/scenarios/task-module/config.py b/tests/teams/scenarios/task-module/config.py
new file mode 100644
index 000000000..42a571bcf
--- /dev/null
+++ b/tests/teams/scenarios/task-module/config.py
@@ -0,0 +1,15 @@
+#!/usr/bin/env python3
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+
+
+class DefaultConfig:
+    """ Bot Configuration """
+
+    PORT = 3978
+    APP_ID = os.environ.get("MicrosoftAppId", "")
+    APP_PASSWORD = os.environ.get(
+        "MicrosoftAppPassword", ""
+    )
diff --git a/tests/teams/scenarios/task-module/requirements.txt b/tests/teams/scenarios/task-module/requirements.txt
new file mode 100644
index 000000000..87eba6848
--- /dev/null
+++ b/tests/teams/scenarios/task-module/requirements.txt
@@ -0,0 +1,2 @@
+botbuilder-core>=4.7.1
+flask>=1.0.3
diff --git a/tests/teams/scenarios/task-module/teams_app_manifest/icon-color.png b/tests/teams/scenarios/task-module/teams_app_manifest/icon-color.png
new file mode 100644
index 000000000..48a2de133
Binary files /dev/null and b/tests/teams/scenarios/task-module/teams_app_manifest/icon-color.png differ
diff --git a/tests/teams/scenarios/task-module/teams_app_manifest/icon-outline.png b/tests/teams/scenarios/task-module/teams_app_manifest/icon-outline.png
new file mode 100644
index 000000000..dbfa92772
Binary files /dev/null and b/tests/teams/scenarios/task-module/teams_app_manifest/icon-outline.png differ
diff --git a/tests/teams/scenarios/task-module/teams_app_manifest/manifest.json b/tests/teams/scenarios/task-module/teams_app_manifest/manifest.json
new file mode 100644
index 000000000..21600fcd6
--- /dev/null
+++ b/tests/teams/scenarios/task-module/teams_app_manifest/manifest.json
@@ -0,0 +1,42 @@
+{
+  "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json",
+  "manifestVersion": "1.5",
+  "version": "1.0.0",
+  "id": "<>",
+  "packageName": "com.microsoft.teams.samples",
+  "developer": {
+    "name": "Microsoft",
+    "websiteUrl": "https://example.azurewebsites.net",
+    "privacyUrl": "https://example.azurewebsites.net/privacy",
+    "termsOfUseUrl": "https://example.azurewebsites.net/termsofuse"
+  },
+  "icons": {
+    "color": "color.png",
+    "outline": "outline.png"
+  },
+  "name": {
+    "short": "Task Module",
+    "full": "Simple Task Module"
+  },
+  "description": {
+    "short": "Test Task Module Scenario",
+    "full": "Simple Task Module Scenario Test"
+  },
+  "accentColor": "#FFFFFF",
+  "bots": [
+    {
+      "botId": "<>",
+      "scopes": [
+        "personal",
+        "team",
+        "groupchat"
+      ],
+      "supportsFiles": false,
+      "isNotificationOnly": false
+    }
+  ],
+  "permissions": [
+    "identity",
+    "messageTeamMembers"
+  ]
+}
\ No newline at end of file